MIGNON BLOG

minishell bonus를 곁들인..

목차

125인증샷

다사다난했던..minishell이지만 결국 125로 산뜻하게 마무리 지을 수 있었습니다.

우선 minishell은 간단하게 bash를 구현하는 프로젝트인데,

일부 기능은 저희가 직접 구현을 하고 대부분의 경우는 execve()라는 함수를 사용하게 됩니다.
그리고 이 프로젝트에서 처음으로 사용하게 되는게, readline이라는 라이브러리인데, 프로젝트 내에서 명령어를 입력 받는 용도로 사용하게 됩니다.

참고로 저는 heredoc을 구현할 때도 readline을 사용해주었습니다. readline을 사용하지 않는 친구들은 get_next_line()을 사용하는 것을 보았습니다.

아무튼..아마 이 글을 보실 대부분의 분들은 이미 프로젝트 문서를 봤을 것이라 예상되기에 세세한 내용보다는 러프하게 저와 제 팀원이 어떻게 각 부분을 구현했는지 설명해보도록 하겠습니다.

우선 저는 파싱 부분과 heredoc / redirection/ pipe/ execution loop/ &&/ || 부분을 맡아서 진행하였고, 제 팀원은 빌트인 함수 구현과 excution부분, signal / wildcard부분을 진행하였습니다.

저는 minishell의 절반은 파싱으로 완성된다고 보는데요..아무래도 파싱에 문제가 있으면 프로젝트 전체가 뻐그러지기 때문에 그렇습니다. 특히나 minishell은 정말 파도파도 나오는 미묘하게 다른 부분 때문에 꽤 스트레스 받는 부분이였습니다.

파싱

– 입력 받기(readline)
– 공백과 “” ” () 가 잘 닫혔는지 확인
– 토큰화 진행(저는 일단 공백과 &|<>(로 나누는 것과 동시에 “” 혹은 ”안에 있는 내용이라면 전체를 하나의 토큰으로 보고 가져왔습니다. ex) echo “ls -l hello world” && “echo check is right” -> echo/ “ls -l hello world”/&&/”echo check is right”)

물론 && >> << > < || | 모두 문자와 붙어있어도 따로 분리 가능해야 합니다.

– 이후에는 $env 환경 변수가 있는 경우에는 해당 값을 찾아서 전환해주는 것을 진행하였습니다.

– 이 부분에서 redirection과 heredoc을 체크하면서 관련된 구문 오류 확인도 함께 체크해주었습니다.(heredoc은 여러개가 있다면 그걸 모두 처음에 다 입력을 받고 나중에 사용하는 형식이여서 저는 이름 저장을 위해 t_list를 사용하고, 이름을 두 부분으로 나눠서 사이에 변하는 값으로 현재 리스트의 사이즈를 입력해서 여러개의 파일에 입력을 받는 식으로 진행하였습니다. 저는 나중에 안 사실인데, 리다이렉션의 경우 사용하지 않는 리다이렉션이라도 파일은 전부 생성되어야 하니 참고하세요!)

-마지막으로 “”와 ”를 제거해주었습니다.($만 입력되는 경우도 확인하세요! 저는 계속 어떠한 데이터로 변환이 되었는데, 다행히 그 값이 랜덤하게 변하는 것은 아니여서 평가자가 감안하고 평가 진행해주었습니다.)

– 그리고 여기까지 한 후에 wildcard를 체크해서 값을 변환해주는 부분을 진행하였습니다.

여기까지 토큰화는 모두 t_list를 사용하여 진행하였습니다. (libft bonus part)

– 이후에도 t_list형태로 진행할 수도 있었겠지만, 저희는 char **타입으로 변환해서 진행하였습니다. 물론 이후에도 다시 t_list로 바꿨다가 char **타입으로 변경해가며 진행한 부분이 있는데, 음 당시에 저는 t_list형태로 끝까지 진행할 수 있는 깔끔한 방법을 찾지 못해서 이렇게 진행하였지만, 다른 방법이 있다면 정말 실행 직전에만 데이터 타입을 바꿀 수 있으면 깔끔할 것 같습니다.

이렇게 파싱을 마무리 한 후에는 특별히 에러케이스가 없는 경우에 execute_loop부분으로 넘어가게 됩니다.

Execute Loop

int	execute_total(t_data *data, int start, int end)
{
	int	index;
	int	left_res;
	int	redirec;

	if (start > end)
		return (-1);
	redirec = check_redirec(data->arg, start, end, 0);
	index = close_index(data->arg, start, end);
	if (index != -1 && (index == end || end == redirec -1)
		&& !ft_strcmp(data->arg[start], "(")
		&& !ft_strcmp(data->arg[index], ")"))
		return (execute_subre(data, start + 1, index - 1, redirec));
	index = lowest_op(data->arg, start, end);
	if (index != -1)
	{
		left_res = execute_total(data, start, index - 1);
		return (right_res(data, end, index, left_res));
	}
	index = find_pipe(data->arg, start, end);
	if (index != -1)
		return (execute_pipe(start, index, end, data));
	data->exitcode = launch_router(data, start, end, index);
	return ((int)data->exitcode);
}

이게 제가 재귀적으로 모든 명령어가 실행되도록 사용한 가장 중심이 되는 함수인데, 우선 첫번째로 redirec에는 ()바깥에 있는 리다이렉션과 heredoc을 처리하기 위해 사용하였습니다.
ex) (ls && echo hi) > hello

왜냐하면 이렇게 따로 체크를 해주지 않으면 ()관련 함수는 오직 (으로 시작해서 )으로 끝나는 경우에만 start인덱스와 end인덱스를 가져가기 때문에 놓치게 되기 때문입니다.

그 다음으로는 &&와 ||부분을 체크하는 부분인데, () 바깥에 &&나 ||가 있다면 왼쪽 부분과 오른쪽 부분으로 나눠서 실행을 하게 되고, 만약에 &&연산의 경우인데, 왼쪽 부분이 실패한다면 오른쪽 부분에서 &&가 아닌 || 부분이 나올 때까지 확인을 해서 있다면 나머지 || 뒤쪽 부분을 실행하고 그게 아니라면 종료를 하게 코드를 작성하였습니다.
ex) echo hello && fail &&echo world || echo bye
위 예제를 bash에서 실행해보시면 제가 무슨 말을 하는지 아실 수 있습니다..ㅎㅎ 반대의 경우도 마찬가지 입니다.

그리고 가장 마지막으로 pipe를 확인해서 실행해줍니다. 이때 저희는 cat이 EOF신호를 못 받아서 엔터를 눌러도 종료가 되지 않는 문제가 있었는데, 왼쪽과 오른쪽 파트를 각각 fork해주는 것으로 해결을 했습니다.

정말 파도파도 끝이없고, 지금도 뭐 태클 걸려면 태클 걸 부분이 없는 건 아니지만..그렇게 되면 minishell이 더 이상 mini하지 않기 때문에 적정선에서 잘 마무리 지었다고 생각됩니다.

제가 설명은 잘 못해서 이 글이 도움이 될지는 모르겠지만, 미약하게라도 보너스를 생각 중이신 분들께 도움이 되었으면 좋겠습니다. 추가로 질문이 있으시다면 댓글 남겨주시면 참고해서 업데이트 해보겠습니다!

관련글