본문 바로가기

프로그래밍

[42서울] 미니쉘(minishell) 파싱, 그 후회와 참회의 기록.

서론

라 피씬 이후에 처음으로, 코드에 한이 맺히고 내가 이 길을 가는 게 맞나 하는 생각이 한 번씩 들 정도로 힘든 과제였다. 평소와 달리 코드의 깔끔함을 추구하지 못하고 3천 줄이 넘는 코드를 리팩터링 할 엄두가 안 나서 그냥 제출했다.

 

가장 크게 실수했다고 생각한 부분은 코드를 짤 때 처음부터 norminette 규정을 생각하고 만들었어야 했는데, 먼저 코드를 만든 다음에 노미넷을 지키려고 한 부분과 구조에서 문제가 발생했을 때 바로 갈아엎었어야 했는데 계속 하나씩 수정하다가 나중에는 갈아엎기도 애매하고 계속 수정하기도 애매한 구석에 밀렸던 게 후회된다.

 

그런데 지나고 나서 생각을 해보니 이런 부분들이 결국 내가 스스로 불러온 재앙이었다는 걸 알게 되었다. 그래서 그 부분들을 중심으로 미니쉘 과제의 일부를 설명하는 글을 적어야겠다고 생각했다.

 

 

과제 설명

미니쉘은 서브젝트의 이름처럼 bash나 zsh 같은 쉘 스크립트를 만드는 과제이다. 모든 걸 다 구현할 필요는 없지만 구현해야 하는 항목들은 bash와 최대한 동일하게 구현하는 것을 목표로 한다.

 

구현해야 하는 항목이 크다 보니 아무래도 허용 함수도 많은데, 이 글에선 전부 정리하기보단 나올 때마다 정리하려고 한다.

 

팀 과제로 문자열을 파싱 하는 사람 한 명, 그걸 전달받아서 실행하는 사람 한 명으로 나눠서 구현하였는데, 쉽게 말하면 나는 아래 그림의 왼쪽 부분들을 구현했다고 생각하면 된다.

출처는 https://github.com/TEAM-DaeGae/minishell

과제를 하면서 느낀 게 이 과제는 미니톡을 한 사람 한 명과 파이프엑스를 한 사람 한 명이 모여서 하도록 구상된 과제라는 생각이 들었다. 우리 팀은 둘 다 파이프엑스를 하는 바람에 시그널 등에서 조금 헤맸다. 오른쪽 부분들에 대한 간략한 설명은 https://techdebt.tistory.com/31 파이프엑스 가이드를 참조하면 될 것 같다.

 

 

프롬프트 구현

int	is_whitespace(char *line)
{
	while (*line)
	{
		if (*line != 32 && !(*line >= 9 && *line <= 13))
		{
			return (0);
		}
		line++;
	}
	return (1);
}

void	main_init(int argc, char *argv[])
{
	struct termios	term;

	if (argc != 1)
		exit_with_err("argument input error", NULL, 126);
	tcgetattr(STDIN_FILENO, &term);
	term.c_lflag &= ~(ECHOCTL);
	tcsetattr(STDIN_FILENO, TCSANOW, &term);
	set_signal(SHE, SHE); // 시그널은 후술
	g_exit_code = 0;
	(void)argc;
	(void)argv;
}

int	main(int argc, char *argv[], char *envp[])
{
	char			*line;
	t_cmd			*cmd;
	t_env			env_head;
	struct termios		term;

	tcgetattr(STDIN_FILENO, &term);
	main_init(argc, argv);
	while (1)
	{
		line = readline("minishell $ ");
		if (!line)
			break ;
		if (*line != '\0')
			add_history(line);
		if (*line != '\0' && !is_whitespace(line))
		{
			// 실제 동작이 들어가는 지점
		}
		free(line);
		//system("leaks minishell | grep leaked"); //
	}
	tcsetattr(STDIN_FILENO, TCSANOW, &term);
}

tcgetattr 함수와 tcsetattr 함수는 터미널의 속성을 제어하는 함수들이다. 자세한 건 https://80000coding.oopy.io/13bd7bb7-3a7f-4b51-b84a-905c47368277 참조

 

터미널의 속성을 변경한다. ctrl + c 를 입력하였을 때 그냥 새로운 줄이 입력되어야 하는데, ctrl + c에 대한 반향(echo) '^C' 이 출력되는 현상이 있기 때문에 echo 를 출력하지 않도록 막아두고 필요할 때마다 수동으로 출력한다. 

 

그런데 이 변경이 한번 적용되면 미니쉘을 나가서도 풀리지 않기 때문에, 미리 현재 속성을 저장해 두고 미니쉘을 종료할 때 되돌린다.

 

그런 다음 argc 와 argv의 컴파일 에러를 피하기 위해 형변환을 이용해 해당 변수를 '사용' 해준다. 처음엔 이게 도대체 무슨 짓인가 싶었는데 다른 방법을 못 찾았다.

 

readline 함수를 사용하여 프롬프트에 'minishell $' 을 출력하고 입력값을 line에 저장한다. 함수에 대한 자세한 설명은 https://intrepidgeeks.com/tutorial/minishell-readline 참조

 

그다음 line이 아예 없을 때는 EOF(ctrl + d) 이므로 해당 반복문을 종료해야 하며 아무것도 입력받지 않았을 때를 빼고는 add_hisrory함수를 사용하여 history 기능을 구현한다. 처음에 zsh에서 해당 기능을 확인해보고 공백이나 탭이 들어왔을 때에도 history가 안되어서 그렇게 구현했는데, 나중에 bash에서 확인해보니 bash는 공백도 저장해서 살짝 당황했다.

 

반복문 마지막에서는 line을 반드시 free 해줘야 한다. 이거 안 하면 입력할 때마다 leak이 생긴다.

 

 

메이크파일 작성

시그널을 다루기 전에 Makefile을 만들어야 한다. 자세한 건 https://velog.io/@sham/minishell%EA%B3%BC-readline 참조(샴님.. 군대 잘 다녀오세요...)

 

우선 brew install readline을 통해 readline 라이브러리를 설치하고 brew info readline 을 입력해서 어디에 설치되었는지를 확인한다.

 

CC = gcc
CFLAGS = -Werror -Wall -Wextra
READLINE_LIB 	= -lreadline -L/opt/homebrew/opt/readline/lib
READLINE_INC	= -I/opt/homebrew/opt/readline/include
# READLINE_LIB 	= -lreadline -L${HOME}/.brew/opt/readline/lib
# READLINE_INC	= -I${HOME}/.brew/opt/readline/include

all			:	$(NAME)

$(NAME)		:	$(OBJS)
		$(CC) $(CFLAGS) -o $(NAME) $(OBJS) $(READLINE_LIB)

%.o: %.c
	$(CC) $(CFLAGS) $(READLINE_INC) -c $< -o $@

그다음 목적 파일을 만들 때에 include를 포함시키고, 컴파일할 때에 라이브러리를 포함시키면 된다. 클러스터 맥에서는 주석 처리된 부분처럼 입력하면 팀원마다 따로 작성할 필요가 없어서 편리하다.

 

시그널 탈취

ctrl + c, ctrl + d, ctrl + \ 에 대한 시그널 동작을 구현해야 한다.

 

ctrl + d는 프롬프트에서 만든 걸로 구현 끝이지만, c와 \ 는 몇 가지 작업이 필요하다.

# define SHE 0
# define DFL 1
# define IGN 2

void	signal_handler(int signo)
{
	if (signo == SIGINT)
	{
		write(1, "\n", 1);
		rl_on_new_line();
		rl_replace_line("", 0);
		rl_redisplay();
	}
	if (signo == SIGQUIT)
	{
		rl_on_new_line();
		rl_redisplay();
	}
}

void	set_signal(int sig_int, int sig_quit)
{
	if (sig_int == IGN)
		signal(SIGINT, SIG_IGN);
	if (sig_int == DFL)
		signal(SIGINT, SIG_DFL);
	if (sig_int == SHE)
		signal(SIGINT, signal_handler);
	if (sig_quit == IGN)
		signal(SIGQUIT, SIG_IGN);
	if (sig_quit == DFL)
		signal(SIGQUIT, SIG_DFL);
	if (sig_quit == SHE)
		signal(SIGQUIT, signal_handler);
}

우선 쉘에서 실행했을 때에 새로운 줄을 입력하는 부분 등을 만든다. 그리고 시그널을 ignore 와 default 와 쉘에서의 동작을 쉽게 전환할 수 있는 함수를 만들어서 사용한다.

 

	set_signal(DFL, DFL);
	pid = ft_fork();
	if (pid == 0)
	{
		redirect(cmd);
		close_unused_fd(cmd, pid);
		exit_code = execute_cmd(cmd, env_head);
		exit (exit_code);
	}
	else
	{
		close_unused_fd(cmd, pid);
		set_signal(IGN, IGN);
	}

그러고 나서 명령어를 실행할 때 fork를 하게 된다면 포크 바로 전에 시그널을 디폴트로 바꾼 다음 포크를 실행하고 부모 프로세스에서는 무시하도록 변경해둔다. 그리고 명령어 실행이 끝나고 다음 입력값을 받도록 준비할 때 쉘에서의 동작으로 변경하면 된다. 

 

이렇게만 해두면 cat, grep 등의 명령어 실행 도중에 ctrl+c를 입력하면 종료는 되겠지만 ^C 가 나오지 않는다. 위에서 프롬프트를 설정할 때 반향을 없애버렸기 때문.

 

void	wait_child(void)
{
	int		status;
	int		signo;
	int		i;

	i = 0;
	while (wait(&status) != -1)
	{
		if (WIFSIGNALED(status))
		{
			signo = WTERMSIG(status);
			if (signo == SIGINT && i++ == 0)
				ft_putstr_fd("^C\n", STDERR_FILENO);
			else if (signo == SIGQUIT && i++ == 0)
				ft_putstr_fd("^\\Quit: 3\n", STDERR_FILENO);
			g_exit_code = 128 + signo;
		}
		else
			g_exit_code = WEXITSTATUS(status);
	}
}

그 부분은 wait 할 때 처리하도록 한다. wait의 인자로 int 변수를 넣으면 <wait.h> 의 매크로들을 사용할 수 있는데, WIFSIGNALED 매크로를 사용하면 시그널에 의해 종료되었는지를 확인할 수 있고, WTERMSIG 매크로를 사용하면 어떤 시그널에 의한 건지를 알 수 있다. 

 

이걸 활용하여 시그널에 의해 종료되었을 때, 그 시그널에 맞는 출력이 나오도록 구현하면 된다. 그리고 cat | cat | cat 가 종료되었을 때 ^C가 세 번 나오지 않도록 설정해둬야 한다.

 

 

인자 파싱 테스트

char	*ft_strjoin_char(char const *s1, char s2)
{
	char	*ret;
	size_t	s1_len;

	if (!s1 && !s2)
		return (0);
	else if (!s1)
		return (ft_strdup(&s2));
	else if (!s2)
		return (ft_strdup(s1));
	s1_len = ft_strlen(s1);
	ret = (char *)malloc(sizeof(char) * (s1_len + 2));
	if (!ret)
		return (0);
	ft_strlcpy(ret, s1, s1_len + 1);
	ft_strlcpy(ret + s1_len, &s2, 2);
	return (ret);
}

int	parse_set_quotes(char line, int quotes)
{
	int	result;

	result = quotes;
	if (line == '\'')
	{
		if (quotes == 1)
			result = 0;
		else if (quotes == 2)
			result = 2;
		else
			result = 1;
	}
	else if (line == '\"')
	{
		if (quotes == 2)
			result = 0;
		else if (quotes == 1)
			result = 1;
		else
			result = 2;
	}
	return (result);
}

void	test_parse(char *line)
{
	char	*str = NULL;
	int		quotes = 0;
	int		index = 0;
	int		space = 1;
	int		pipe = 0;
	// 제출 안할 함수여서 구분을 위해 여기서 초기화

	while (*line)
	{
		quotes = parse_set_quotes(*line, quotes); // line 이 \' 혹은 \" 일때 예외 처리를 위해 구분
		
		if (*line == ' ' && space == 0 && quotes == 0)
		{
			printf("[%d] : %s\n", index, str);
			str = ft_free(str);
			space = 1;
			index++;
		}
		else if (*line == '|' && quotes == 0)
		{
			if (space == 0) //공백이 아닐때만 구조체에 넣고 free
			{
				printf("[%d] : %s\n", index, str);
				str = ft_free(str);
			}
			if (pipe == 1) // 기존 값이 파이프일때(파이프가 연속으로 나왔을때) 예외처리
			{
				printf("test exit: ||\n");
				exit(1);
			}
			// 이 자리에 현재 구조체의 is_pipe를 true로 바꾸고 다음 리스트로 넘어가는 동작 넣을 예정
			index = 0;
			space = 1;
			pipe = 1;
		}
		else
		{
			// 해석하지 않는 특수문자 예외처리
			if ((*line == ';' || *line == '\\') && quotes == 0)
			{
				printf("test exit: %c\n", *line);
				exit(1);
			}
			// 연달아서 공백이 나오는 경우 예외처리
			if (!(*line == ' ' && space == 1))
			{
				str = ft_strjoin_char(str, line[0]);
				space = 0;
				pipe = 0;
			}
		}
		line++;
	}
	if (quotes != 0) // 닫히지 않은 따옴표 예외처리
	{
		printf("test exit: quotes error\n");
		exit(1);
	}
	if (str != NULL) // 마지막에 출력하지 않은 문자열이 남은 경우 처리
	{
		printf("[%d] : %s\n", index, str);
		str = ft_free(str);
	}
}

그다음 프롬프트에 입력한 문자열이 line 에 어떻게 입력되는지를 확인하는 함수를 만들고 그 함수를 계속 고쳐가면서 어떻게 저장해서 넘길지를 생각해본다. 위의 함수는 예시이고 실제로는 직접 만들어보길 바란다.

 

 

구조체 만들기

위에서 입력해본 걸 기초로 어떻게 값을 담아서 전달하면 좋을지를 고민해본다. 끝나고 나서 느낀 건 여기서 길게 고민하는 게 큰 의미는 없는 것 같다. 어차피 지금 깊게 고민해봐야 좋은 구조가 나오지는 않는다. 따라서 짧게 고민하고 빠르게 갈아엎는 게 더 효율이 높을 것이다.

typedef struct s_cmd
{
	char			**argv;
	int				argc;
	bool			is_pipe;
	bool			is_dollar;
	int				fd[2];
	int				infile;
	int				outfile;
	char			*cmd_path;
	struct s_cmd	*prev;
	struct s_cmd	*next;
}						t_cmd;

여기에 문제점이 있는데, 인자로 넘겨주는 값을 단순한 문자열만 가지고 전부 처리할 수 있을 거라고 생각한 게 문제였고 더 큰 문제는 그걸 끝날 때까지 갈아엎지 않고 꾸역꾸역 끌고 간 게 문제였다.

 

어떤 인자가 어떤 속성을 가지고 있는지를 담고 있는 변수가 하나 더 필요했다고 생각한다. 예를 들어 "<" 와 < 는 실제 동작이 다른데, 이걸 실행부에 넘겨줄 때는 둘 다 < 로 넘어가게 되어있었다. 이 구조에서는 해결할 방법을 찾지 못해서 파싱 할 때 해당 부분을 음수로 바꿔서 넘기고 그걸 실행부에서 다시 해석하는 식으로 동작하게 만들었다.

 

이걸 그냥 해당 부분의 속성을 같이 넘겨주면 됐을 텐데, 지금까지 만든 걸 갈아엎기가 귀찮다는 이유로 계속 안 하고 넘어가다가 문제를 마주칠 때마다 힘들게 해결해야 했다. 다음에 만들면 이거보다는 잘 만들어야지. 

 

 

구조체에 인자 담기

void	parse(char *line, t_cmd *cmd)
{
	t_cmd	*next;
	char	*str;
	int		quotes;
	int		pipe;

	str = NULL;
	quotes = 0;
	pipe = 0;
	while (*line)
	{
		quotes = parse_set_quotes(*line, quotes); // line 이 \' 혹은 \" 일때 예외 처리를 위해 구분
		if (*line == '|' && quotes == 0)
		{
			if (pipe == 1) // 기존 값이 파이프일때(파이프가 연속으로 나왔을때) 예외처리
				exit_with_err("argv error", "||", 1);;
			// 현재 구조체에 값을 입력하고 다음 리스트로 넘어감
			cmd->is_pipe = true;
			cmd->argv = ft_split_argc(str, ' ', &(cmd->argc));
			next = ft_list_init();
			cmd->next = next;
			next->prev = cmd;
			cmd = next;
			str = ft_free(str);
			pipe = 1;
		}
		else
		{
			// 특수문자 예외처리
			if ((*line == ';' || *line == '\\') && quotes == 0)
				exit_with_err("symbol error", line, 1);
			else if (quotes != 0 && *line == ' ')
			{
				str = ft_strjoin_char(str, -32);
			}
			else
			{
				str = ft_strjoin_char(str, line[0]);
				pipe = 0;
			}
		}
		line++;
	}
	if (quotes != 0) // 닫히지 않은 따옴표 예외처리
		exit_with_err("quotes error", NULL, 1);
	if (str != NULL) // 마지막에 문자열이 남은 경우 처리
	{
		cmd->argv = ft_split_argc(str, ' ', &(cmd->argc));
		str = ft_free(str);
	}
}

위에서 만든 구조체와 테스트 내용을 가지고 구조체에 인자를 담는 함수를 만든다.

 

파이프가 나오기 전까지 전체 문자열을 담은 다음 파이프가 나오면 파이프가 나오면 새로운 리스트를 만들어서 연결해주고 기존의 문자열은 공백을 기준으로 argv에 문자열을 나눠서 담았다.

 

그런데 따옴표 안의 공백은 해석하지 않고 그대로 남아야 하면서, 문자열을 나누는 기준으로 사용할 수 없으므로 여기서는 해당 부분을 -32로 변경해서 담아준다. 이후에 인자 변환에서 다시 처리할 것이다.

 

 

테스트 작성

void	test_print_cmd(t_cmd *cmd)
{
	int	index = 0;
	int	i = 0;
	// 제출 안할 함수여서 구분을 위해 여기서 초기화
	
	while (cmd)
	{
		printf("[%d] argc: %d\n", index, cmd->argc);
		while (i < cmd->argc)
		{
			printf("[%d] argv[%d]: %s\n", index, i, cmd->argv[i]);
			i++;
		}
		if (cmd->is_pipe)
		{
			printf("[%d] is_pipe: true\n", index);
		}
		else
		{
			printf("[%d] is_pipe: false\n", index);
		}
		i = 0;
		index++;
		cmd = cmd->next;
	}
}

위에서 저장한 구조체가 잘 저장되었는지를 확인하는 함수를 만들어서 사용한다. 여기서만 사용하지 않고 이후에도 계속 사용하게 되는 함수이므로 귀찮더라도 만들면 좋다.

 

 

환경변수 다루기

char	*get_env_key(char *key_value)
{
	size_t	i;
	size_t	len;
	char	*key;

	len = 0;
	while (key_value[len] != 0 && key_value[len] != '=')
		++len;
	if (key_value[len] == '\0')
		return (NULL);
	key = (char *)ft_malloc(sizeof(char), len + 1);
	i = 0;
	while (i < len)
	{
		key[i] = key_value[i];
		++i;
	}
	key[i] = 0;
	return (key);
}

char	*get_env_value(char *key_value)
{
	size_t	i;
	size_t	len;
	char	*value;

	len = 0;
	while (key_value[len] != 0 && key_value[len] != '=')
		++key_value;
	if (key_value[len] == 0)
		return (NULL);
	len = ft_strlen(++key_value);
	value = (char *)ft_malloc(sizeof(char), len + 1);
	i = 0;
	while (i < len)
	{
		value[i] = key_value[i];
		++i;
	}
	value[i] = 0;
	return (value);
}

t_env	*compare_env_key(t_env *env_head, char *key)
{
	t_env	*cur;

	cur = env_head;
	while (cur->key != 0 && ft_strncmp(key, cur->key, ft_strlen(cur->key)))
		cur = cur->next;
	return (cur);
}

t_env	*new_env(char *key_value)
{
	t_env	*new;

	new = (t_env *)ft_malloc(sizeof(t_env), 1);
	if (key_value == NULL)
	{
		new->key = NULL;
		new->value = NULL;
		new->next = NULL;
		new->prev = NULL;
	}
	else
	{
		new->key = get_env_key(key_value);
		if (new->key == NULL)
			return (NULL);
		new->value = get_env_value(key_value);
		if (new->value == NULL)
			return (NULL);
		new->next = NULL;
		new->prev = NULL;
	}
	return (new);
}

int	init_env_list(t_env *cur, char **envp)
{
	size_t	i;
	t_env	*new;

	i = 0;
	cur->key = get_env_key(envp[i]);
	if (cur->key == NULL)
		return (-1);
	cur->value = get_env_value(envp[i]);
	if (cur->value == NULL)
		return (-1);
	cur->next = 0;
	cur->prev = 0;
	while (envp[++i])
	{
		new = new_env(envp[i]);
		if (new == NULL)
			return (-1);
		cur->next = new;
		new->prev = cur;
		cur = cur->next;
	}
	new = new_env(NULL);
	new->prev = cur;
	cur->next = new;
	return (0);
}

달러 사인($)의 기능을 구현하기 위해 envp 를 가져와서 리스트에 저장하고 이후에 필요한 값을 찾아서 쓸 수 있는 함수들을 만들어야 한다.

 

메인 문 시작할 때 envp 를 가져오며 빌트인 함수들을 구현할 때(env, export, unset) 해당 리스트를 사용할 수 있어야 한다. 저장하는 구조체의 내용은 키와 밸류와 next prev만 가지고 있는 구조체이다.

 

static char	**get_envp(t_env *head)
{
	int		i;
	int		size;
	char	*key;
	t_env	*tmp;
	char	**result;

	i = 0;
	size = 0;
	tmp = head;
	while (tmp)
	{
		size++;
		tmp = tmp->next;
	}
	result = malloc(sizeof(char *) * size);
	tmp = head;
	while (i < size - 1)
	{
		key = ft_strjoin(tmp->key, "=");
		result[i] = ft_strjoin(key, tmp->value);
		i++;
		tmp = tmp->next;
		free(key);
	}
	result[i] = NULL;
	return (result);
}

int	cmd(t_cmd *cmd, t_env *env_head)
{
	char	*env_path;
	char	**now_env;

	env_path = ft_getenv(env_head, "PATH");
	now_env = get_envp(env_head);
	execve(cmd->cmd_path, cmd->argv, now_env);
	return (EXIT_FAILURE);
}

평가에 있어서 필수적인 부분은 아닌데, (평가 항목이 없다) 하위 프로그램에 envp를 전달해야 한다. 즉, minishell 에서 export한 환경변수를 minishell에서 bash를 실행시킬 때 전달해야 한다. 따라서 해당 리스트를 다시 envp로 전환하는 함수도 있으면 좋다.

 

 

인자 최종 변환

void	replace(t_cmd *cmd, t_env *head)
{
	int		i;
	int		j;
	int		size;
	char	*new;
	char	*env;
	int		dollar;
	int		quotes;

	new = NULL;
	env = NULL;
	quotes = 0;
	dollar = 0;
	while (cmd)
	{
		i = 0;
		while (i < cmd->argc)
		{
			j = 0;
			size = ft_strlen(cmd->argv[i]);
			while (j <= size)
			{
				quotes = parse_set_quotes(cmd->argv[i][j], quotes);
				if (cmd->argv[i][j] == '$' && quotes != 1 && dollar == 0)
				{
					dollar = 1; // 작은 따옴표가 아닐때 $ 상태에 돌입
				}
				else if (dollar == 1)
				{
					if (ft_isalnum(cmd->argv[i][j]) || cmd->argv[i][j] == '_')
					{
						env = ft_strjoin_char(env, cmd->argv[i][j]); // 특수문자 혹은 띄어쓰기가 아니면 env 문자열에 차곡차곡 저장
					}
					else if (cmd->argv[i][j] == '?' && env == NULL)
					{
						// $? 일때 에러 코드 반환
						env = ft_itoa(g_exit_code);
						new = ft_strjoin(new, env);
						dollar = 0;
					}
					else
					{
						new = ft_strjoin(new, ft_getenv(head, env));
						env = ft_free(env);
						if (cmd->argv[i][j] != '$')
							dollar = 0;
					}
				}
				else
				{
					if (cmd->argv[i][j] == -32)
						new = ft_strjoin_char(new, ' ');
					// 따옴표 안에 들어가있지 않은 따옴표는 입력하지않음
					else if (!(cmd->argv[i][j] == '\"' && quotes != 1) && !(cmd->argv[i][j] == '\'' && quotes != 2))
						new = ft_strjoin_char(new, cmd->argv[i][j]);
				}
				j++;
			}
			cmd->argv[i] = ft_free(cmd->argv[i]);
			cmd->argv[i] = new;
			new = NULL;
			i++;
		}
		cmd = cmd->next;
	}
}

이게 최종 버전은 아닌데, 노미넷 규정을 위해 함수를 마구 쪼개 놓은 버전은 알아보기가 힘들어서 글로 남기기에는 부적합할 것 같아서 이전 버전으로 가져왔다. 아마 이건 정상적으로 동작하지 않는 케이스가 몇 가지 있을 것이다.

 

쉽게 요약하면 받아온 인자 중 $, ', " 이렇게 세 개의 동작을 구현하면 된다. 달러 사인($)은 환경변수와 $? 의 경우만 구현하면 되며, 따옴표도 마찬가지로 서브젝트에 나온 것처럼 안의 문자열을 해석하지 않도록 구현하면 된다.(작은따옴표는 달러사인도 해석하지 않음)

 

이후에 argv가 하나도 없는 경우에 그 리스트를 삭제하도록 만들어줬다. 

 

 

결론

지나고 나서 글을 쓰려니 정작 어디서 막혔었고 어디에서 헤맸는지가 잘 기억이 나질 않는다. 그때그때 정리하고 가장 깔끔한 해결책을 찾으려고 노력한 게 아니라 눈앞에 닥치는 대로 해결했더니 어디에 적어둔 것도 없고, 기억도 잘 나지 않는 게 너무 아쉽다. 그런 부분들을 잘 정리해둬야 나중에 다시 고생하지 않을 텐데...

 

개인적으로 운이 좋았다고 생각한다. 우선 팀원을 잘 만났다. 그리고 리트를 두 번 했는데, 처음에는 불금이라 그런지 평가가 없어서 다른 카뎃에게 모의평가를 받고 문제가 된 부분들을 수정한 뒤 다시 평가를 받았고, 그다음의 평가에는 굉장히 꼼꼼하게 봐주시는 카뎃을 만나서 바로 고칠 점을 찾고 리트라이를 할 수 있었다.

 

그리고 마지막 평가에는 꼼꼼하게 봐주신 분도 있지만 미니쉘에 대한 정보가 거의 없는 카뎃들을 만나서 평가 한 번에 한 시간 반씩 설명해야 했는데, 뭔가 만들기만 했을 때보다 구조적인 부분을 잘 정리할 수 있었던 것 같다. 맨 위 사진에 남겨진 평가자분들 모두 고맙습니다! 그리고 mher, dongkim, smun 함께 하거나 성심성의껏 도와주셔서 감사합니다!!