프로젝트 완료(2021.01.12)
minishell을 구현할때는 보통 파싱과 실행부분으로 나누어 구현을 한다고 한다. 그리하여 팀원분과 상의해서 나는 실행부분을 구현하는 것으로 했다. bash를 기준으로 구현했다.
코드 링크(private)
순수 개발시간은 3주정도 걸렸다. 주말엔 안했다.
우리 미니쉘은 구문트리형식으로 파싱을 했다.
파이프랑 리다이렉션 기준으로 구분이 되어있고, 전위순회 탐색을 하며 명령어를 실행시켰다.
전체적인 실행흐름은
1. 먼저 파이프가 있는지 검사.
1-1. 파이프가 없다면 부모 프로세스에서 트리를 순회하면서 해당 명령어에 맞는 함수를 실행.
1-2. 파이프가 있다면 fork를 하여 자식 프로세스에서 전위순회하면서 계속 파이프가 있는지 확인을 한다.
가장먼저 env를 구현했다. echo cd unset export는 전부 환경변수와 관련이 있기 때문이었는데, 확실히 env부터 구현하고나니 나머지 명령어를 구현할때는 꽤 수월하게 구현했다.
옵션이 따로 필요하지 않아서 env를 출력하는건 간단했는데, 환경변수 데이터를 어떤 방식으로 관리할지 조금 고민했었다. 하지만 어차피 unset, export때문에 계속해서 데이터 추가/삭제를 반복을 해야해서 연결리스트로 구현했다. 하지만 막판에 execve때문에 char **형의 환경변수 데이터가 필요해서 이중포인터도 상관없을 것 같긴 했다.
unset 인자
인자로 숫자가 들어오면
bash: unset: `인자': not a valid identifier
라는 오류를 출력한다.
인자는 여러개가 들어갈 수 있는데 한개라도 오류가 뜨면 echo $?했을때 1이 출력된다. 다른 명령어도 마찬가지다.
export 인자
인자는 key=value
형식으로 이루어져야한다. shell은 파싱을 공백을 기준으로 하기때문에 인자에 공백이 들어가서는 안된다. 공백이 들어가면 다 다른 문자열로 처리가 된다.
key = 123
이런식으로 인자가 들어오면
bash: export: `=': not a valid identifier
bash: export: `123': not a valid identifier
이렇게 오류메시지가 출력이 된다.
키값에는 무조건 문자열로만 이루어져야한다. '='나 숫자로 시작하면 오류다.
export 1a=1
bash: export: `1a=1': not a valid identifier
value값이 '='로 끝나는건 된다.
그냥 자신이 위치하고 있는 디렉토리 경로를 가져오면 되니 getcwd
명령어를 사용해서 간단하게 구현할 수 있다.
cd 인자
없는 경로로 이동하려고 하면
bash: cd: 인자: No such file or directory
이런 오류를 출력한다.
인자는 여러개가 들어갈 수 있는데 bash는 인자가 여러개가 들어와도 맨 앞 하나의 인자만을 쓴다.
cd ~
, cd
하면 HOME경로로 이동하는데, unset HOME
한후 cd $HOME
하면
bash: cd: HOME not set
이런 오류메시지가 출력이 돼서, 사전에 HOME경로를 백업해주었다.
그리고 cd작업이 정삭적으로 수행이되면 환경변수중에 OLDPWD, PWD가 변경이 된다.
exit 인자
인자는 없어도 되고 만약 있다면, 부모 프로세스에 그 값을 넘겨주고 프로세스를 종료시킨다.
인자가 숫자가 아니거나 여러개가 있으면 각각 오류메시지를 출력한다.
bash-3.2$ exit 123 123
exit
bash: exit: too many arguments (종료 x)
bash-3.2$ exit 1zz
exit
bash: exit: 1zz: numeric argument required (종료 o, echo $? => 255)
만약 미니쉘안에서 미니쉘을 실행한 후 exit 123
하면 부모 미니쉘에 123을 넘겨주고 종료가 되는데, 부모 미니쉘에서 echo $?
하면 123을 출력한다.
waitpid(pid, $status, 0);
$?변수 = status / 256;
이렇게하면 자식 프로세스가 반환한 상태를 가져올 수 있다.
echo 인자
인자를 출력해주면 된다.
파싱단계에서 따옴표처리랑 공백처리, 환경변수 처리를 다 해줘서 딱 -n옵션만 해주면 되서 생각보다 까다롭진 않았다.
근데 옵션처리 할때 좀 신기했던점이 있었는데
bash-3.2$ echo -nnnnnn -n 123
123bash-3.2$
같은 옵션이 몇개 들어와도 상관이 없고 n이 몇개가 들어가도 상관이 없었다. (중간에 n말고 다른 문자열이 들어가면 옵션이 아니라 문자열 취급)
그래서 이거 처리하는데 조금 까다롭긴 했다.
사실 리다이렉션은 <<(히어독)빼고는 구현방법이 거의 똑같다.
리다이렉션은 dup2로 파일 디스크립터를 복제해야하기 때문에 fork를 쓰거나 기존 표준 입출력을 백업시키거나 해야한다. 나는 fork를 썼다.
실행과정
1. 먼저 인자가 있는지 확인을 한다.
1-1. 인자가 없으면 bash: syntax error near unexpected token `newline' 메시지 출력
1-2. 인자가 있으면 리다이렉션 이후에 오는 명령어를 찾고 그 후 fork()
2. 해당 리다이렉션 기호에 맞춰서 리다이렉션 함수로 이동
3. 파일을 open하고 dup2로 fd복제
4. 다음 명령어 실행
5. exit로 부모프로세스에 상태 반환
히어독을 구현할때는 이 페이지를 참고했다.
위 명령어와 리다이렉션 빼고는 execve()로 실행을 한다.
fork로 자식 프로세스를 실행시킨뒤 execve로 외부 프로그램을 실행시킨다.
stat으로 해당경로에 파일이 있는지 확인하고 있으면 실행시켰다.
그래도 없으면 command not found 오류 출력.
우리 미니쉘은 처음부터 파이프가 있는지 검사를 해서 시작부터 파이프 함수를 실행을 시킨다.
파이프 노드가 있으면 fork하고 없으면 fork를 하지 않는데, fork를 하고 파일 디스크립터를 dup2하는 과정에서 표준 입출력까지 전부 닫아버리는 문제가 있었다.
이 부분은 팀원과 함께 스스로 해결을 하였다. 다른 분들께 도움이 될수있도록 42swim과 슬랙에도 해당 부분을 공유했다.
처음 미니쉘을 공부하기 시작했을때는 도대체 이걸 어떻게 구현해야하나 막막했는데, 막상 코드 짜고보니 생각보다 기능들을 짧고 간단하게 구현할 수 있어서 꽤나 뿌듯했다.
기회가 되면 레포를 퍼블릭으로 돌리고 싶으나 이건 팀원분의 허락이 있어야 할 것 같다.