minishell(3) - 나의 작은 쉘(BASH)

yeham·2023년 4월 23일
0

42Seoul

목록 보기
16/18
post-thumbnail

들어가기전에

  • 멀티 프로세스와 리눅스 bash의 내부 동작원리와 팀 프로젝트에 대해 배웁니다.

  • 30개가 넘는 c 파일이어서 이번에는 세세하게 설명하지 않습니다.

  • Built_in 명령어 아래의 명령어들을 직접 구현합니다.

    • export
    • env
    • unset
    • exit
    • cd
    • echo
    • pwd
  • 자료구조는 리스트(list)와 동적 배열(vector)를 만들어 사용했습니다.

🖥️ Mandatory

📌 Makefile

NAME = minishell

CFLAGS = -Wall -Wextra -Werror

CMPFLAGS = libft/libft.a -lreadline -L${HOME}/.brew/opt/readline/lib
INFLAGS	= -I${HOME}/.brew/opt/readline/include

BUILTIN_SRCS = $(addprefix builtin/, builtin_util.c cd.c echo.c env_split.c env.c exit.c export.c export2.c pwd.c unset.c)
ETC_SRCS = $(addprefix etc/, signal.c signal2.c vector.c vector2.c)
EXEC_SRCS = $(addprefix exec/, run_builtin.c run_child_checkaccess.c run_child_errcheck.c run_child.c run_command.c run_single_builtin.c run_split.c run_split_sep.c run_util_general.c run_util_quote.c)
MAIN_SRCS = $(addprefix main/, main.c main_heredoc1.c main_heredoc2.c main_while_init.c)
PARSE_SRCS = $(addprefix parsing/, parse.c parse_env_expansion.c parse_env_expansion2.c parse_qm_expansion.c parse_sep.c parse_tokenize.c parse_tokenize2.c parse_util.c parse_util2.c)
HEADER	= main.h

SRCS	:= $(BUILTIN_SRCS) $(ETC_SRCS) $(EXEC_SRCS) $(MAIN_SRCS) $(PARSE_SRCS)

OBJS = $(SRCS:.c=.o)

all : $(NAME)

$(NAME) : $(OBJS) $(HEADER) libft/libft.a 
	cc $(CFLAGS) $(OBJS) -o $(NAME) $(CMPFLAGS)

libft/libft.a :
	make -C libft bonus

%.o : %.c $(HEADER)
	cc $(CFLAGS) -c $< -o $@ $(INFLAGS)

clean :
	make -C libft clean
	rm -f $(OBJS)

fclean :
	make clean
	make -C libft fclean
	rm -f $(NAME)

re : 
	make fclean
	make all

.PHONY : all clean fclean re

📌 코드

int	main(int argc, char **argv, char **envp)
{
	t_list	*list;
	t_copy	env;
	char	*line;
	int		result;

	result = 0;
	line = 0;
	list = NULL;
	main_init_env(&env, envp, argv, argc); // 환경변수 복사본 만들기
	while (1)
	{
		if (main_while_init(&list, &line, &result))
		{
			free(line);
			continue ;
		}
		if (!(main_single_builtin_check(&list, &result, &env)))
			result = command_run(list->next, &env, result);
		delete_local_file(list->next);
		free(line);
		free_list(list);
	}
}

매개변수로 받은 환경 변수를 exportenv로 활용하기 위해 복사본을 2개 만들어줍니다.

그리고 while(1)로 계속 프롬프트를 띄워주는 방식으로 진행합니다.

int	main_while_init(t_list **list, char **line, int *result)
{
	handle_signal(); // 시그널 입력받기
	g_result = 0;
	(*line) = reading(); // 프롬프트 띄우고 라인 입력받기
	if (g_result)
		(*result) = g_result; // 시그널 들어오면 errno값 변경
	if ((*line) == 0 || (**line) == 0)
	{
		free(*line);
		(*line) = 0;
		return (1);
	}
	(*list) = first_parsing(line); // 입력값 line을 parsing해서 토큰단위로 list에 담기
	if ((*list) == 0)
		return (1);
	if (command_check((*list))) // list에 담긴 토큰이 명령어인지 확인
		(*result) = 258;
	else if (heredoc((*list)->next)) // list에 담긴 토큰이 here_doc인지 확인
		(*result) = 130;
	else
		return (0);
	free((*line));
	free_list((*list));
	(*line) = 0;
	return (1);
}

while(1) 안에서 자식 프로세스가 없는 메인 프로세스 상태일 때 시그널 입력을 받고, 그에 따른 errno 값을 변경해 줍니다.

read line으로 문자열 입력값을 받은 상태에서 토큰 단위로 파악할 수 있게 파싱을 해줍니다.

', "로 들어오면 잘 잘라내주고, 공백이나 이 섞여 들어오면 잘 잘라주고, $환경변수로 들어왔는지 등등 잘 다듬어줍니다.

생선 가시 발라먹듯 잘 parsing 해주고 실행 단위로 자른 첫 번째 토큰이 명령어인지 here_doc인 지 확인해 줍니다.

int	main_single_builtin_check(t_list **list, int *result, t_copy *env)
{
	if (pipe_exists((*list)->next)) // 파이프가 존재하는지 파악합니다.
		return (0); // 존재한다면 if문으로 들어가 command_run을 실행합니다.
	else
		if (main_builtin(list, result, env))
			return (1); // 파이프가 존재하지 않는다면 built_in인지 확인하기 위해 main_builtin 함수를 실행합니다.
	return (0);
}

우선 파이프가 존재해서 fork()로 프로세스를 만들어야 하는지
파이프가 존재하지 않으면 메인 프로세스에서 처리가 가능하고 해당 명령어가 bulit_in 인지 확인합니다.

int	command_run(t_list *list, t_copy *e, int result)
{
	int		pipefd[2][2];
	int		pid;

	pipefd[NEXT][READ] = 0;
	pipefd[NEXT][WRITE] = 0;
	while (list)
	{
		command_run_fd_prev(list, pipefd);
		pid = fork(); // 프로세스 생성
		if (pid < 0)
        {
			perror("fork failed");
			exit (1);
		}
		else if (pid == 0)
			child_process(&list, e, pipefd, result); // 자식 프로세스
		command_run_fd_post(pipefd);
		while (list && ft_strncmp(((char *)list->content), "|", 2) != 0)
			list = list->next; // |가 나올때까지 리스트를 넘겨줍니다.
		if (list)
			list = list->next;
	}
	return (status_return(pid));
}

파이프가 존재하면 파이프 개수만큼 프로세스를 생성해서 명령어를 실행시켜 줍니다.

int	main_builtin(t_list **list, int *result, t_copy *env)
{
	char	**command;
	char	*temp_string;
	int		fd[3][2];
	int		tnum;

	main_builtin_init(fd, &command, &temp_string); // fd 초기화 및 연결
	tnum = command_split((*list)->next, fd, &command, &temp_string);
	if (tnum)
	{
		vector_free(command);
		free(temp_string);
		(*result) = tnum;
		return (tnum);
	}
	parse_expand(&command, *result, env); // 벡터로 들어온 인자들을 확인 및 정리 해줍니다.
	if (!((*list)->next) || !command || !(builtin_check(command[0])))
	{
		free(temp_string);
		vector_free(command);
		return (0);
	}
	main_builtin_fd_mid(fd); // fd값 변경
	(*result) = builtin_exec(command, env); // built_in 실행 및 errno 설정
	free(temp_string);
	return (main_builtin_fd_post(fd), 1);
}

파이프가 존재하지 않고 built_in 명령어일 경우 메인 프로세스에서 처리해 줍니다.

fd값을 파이프가 있을 땐 fd[0]과 fd[1]을 사용해 통신했으므로 built_in만 존재할 땐 fd[2]에서 처리해줍니다.

void	parse_expand(char ***command, int result, t_copy *env)
{
	t_list	*temp_list;

	temp_list = vector_to_list(command);
	free(*command);
	ft_lstadd_front(&temp_list, ft_lstnew(0));
	qmark_expansion(temp_list, result);
	env_expansion(temp_list, env->cp_envp);
	quote_trim(temp_list);
	free_empty(temp_list);
	list_tie(temp_list);
	free_space(temp_list);
	free_empty(temp_list);
	(*command) = list_to_vector(temp_list);
	free_list(temp_list);
}

2차원 백터의 주소값으로 받아 3중포인터로 매개변수를 받습니다.

parsing된 인자들을 따옴표들을 제거해주고, 비어있거나 공백문자만 남겼으면 free해주는 등등 처리를 거쳐 토큰으로 만들어줍니다.

void	ft_signal(int signum)
{
	if (signum != SIGINT)
		return ;
	if (rl_on_new_line() == -1)
		exit(1);
	printf("\n");
	rl_replace_line("", 1);
	rl_redisplay();
	g_result = 130; // errno 
}

void	handle_signal(void)
{
	struct sigaction	new;

	new.sa_flags = 0;
	sigemptyset(&new.sa_mask);
	new.__sigaction_u.__sa_handler = ft_signal;
	sigaction(SIGINT, &new, 0); // ctrl + c 만 처리
	new.__sigaction_u.__sa_handler = SIG_IGN;
	sigaction(SIGQUIT, &new, 0); // ctrl + \ 는 무시합니다
}

fork()를 진행하지 않은 메인 프로세스에서 시그널 신호가 들어오면 sigaction으로 처리해 줍니다.

프로세스를 따로 만들지 않고, signal 함수가 void형 함수만 인자로 받기 때문에 전역변수로 errno를 관리했습니다.

✅ 배운점

지금까지 진행한 프로젝트 중 가장 오랜 시간이 걸린 프로젝트입니다.

내용이 많아 블로그에 모두 적기엔 한계가 있어 큰 틀만 설명했습니다.

리눅스 bash가 내부적으로 어떻게 동작하고 어떤 기능이 있는지 많이 알게 되었습니다.
해당 프로젝트로 구현 실력이 크게 상승하고 자료구조의 활용, 멀티 프로세스와 시그널 등 다양한 개념을 배우고 팀 프로젝트를 진행하며 고려해야 할 사항 등 많은 성장을 했다 생각합니다.

약 한 달간 진행한 프로젝트로 여러 번 갈아엎고 다시 코드를 짜고, 많은 토의를 거쳐 완성한 프로젝트로 아직 부족함이 있을지는 모르지만 뜻깊은 시간이었습니다.

https://github.com/zerowin96/supershell_team

profile
정통과 / 정처기 & 정통기 / 42seoul 7기 Cardet / 임베디드 SW 개발자

0개의 댓글