minishell

junpkim·2022년 6월 24일
0

bash의 기능을 C를 사용해 구현해야하는 과제인 minishell이다.

명령어 echo(n 옵션 포함), cd, pwd, export, unset, env, exit과 redirection <, <<, >, >>, pipe | 로 명령어의 출력값이 다음 명령어의 입력값으로 들어가야 하며 $?를 통한 foreground(종료 상태), ctrl+C, ctrl+D, ctrl+\ signal 등을 구현해야 했다.

하지만 위 내용을 구현하는 것 보다 입력 값을 parsing 하여 파이프와 redirection, 명령어, 옵션을 구분하는 것이 훨씬 까다롭고 중요했다.

parse

내가 구현해낸 방법은 명령어를 받으면 PIPE(|)가 있는지 확인하고, 해당 PIPE를 기준으로 PIPE의 좌측 명령어와 우측 명령어를 구분한다.
좌측 명령어에서는 redirection symbol(<, >) 가 존재하는지 확인하고, 있다면 해당 부분을 left 노드로, 나머지 명령어를 right 노드로 분리한다.
나머지 명령어에서 명령어 부분과 옵션 부분으로 나누어주었다. 옵션 부분의 경우 명령어도 포함하고 있어야만 execve 함수를 이용해 명령어를 실행할 때 원활히 실행할 수 있다.
정리하자면 규칙은 다음과 같다.
PIPE -> left: CMD, right: PIPE
REDIRECTS -> left: REDIRECT right: REDIRECTS
CMD -> left: REDIRECTS, right: SIMPLECMD
REDIRECT -> left: type, right: file_name
SIMPLECMD -> left: file_path, right: ARGV

문제점

위 parsing은 꽤나 금방 구현하였으나 여러 케이스를 시험해보던 중 bash와 다른 점들을 발견할 수 있었다.

>file1 ls 의 경우 ls 명령어를 실행하고 출력값을 file1에 저장해야 한다.

export a="'asdf'" 실행 후 $a를 출력하면 'asdf'가 출력되어야 한다.

"echo asdf"는 echo asdf 를 통째로 명령어로 인식해야 한다.

echo                 a             b 는 echo a b로 인식하여야 한다.

뭐 기타 등등.. 아무튼 가장 큰 문제는 따옴표를 해석하는 위치에서 왔다. 따옴표를 포함 하고 있는 반환값을 입력값으로 받을 때 이 따옴표가 표준입력으로 들어온 따옴표인지 반환값으로 들어온 따옴표인지....
그래서 파싱 과정을 나눌 필요가 있었는데, 일단 따옴표로 묶어져있는 것들은 하나의 요소로 묶어서 저장하고 해당 명령어를 실행할 때 따옴표를 제거해주는 작업을 해주었다.
그리고 공백이 여러 개 들어가 있는 명령어의 경우 트리에 저장할 때 하나로 공백 하나로 변경한 후 저장해주었다.

여러 예외를 처리하는데 꽤 많은 시간이 들였지만.. 사실 세미콜론과 \ 의 해석을 하지 않아야 한다는 항목을 놓치지 않았다면 시간이 대폭 단축되었을 것 같다. 역슬래쉬에 대한 항목을 굳이 굳이 적자면

\\ 는 \로 해석된다.

string\
a\
b\
c		는 stringabc로 해석되고, history 역시 stringabc로 저장된다.

string\\\
a			는 string\a로 해석되고, history는 string\\\; a로 저장된다.

\"a		는 "a 로 해석된다.

대충 기억나는건 이정도 인데, 위 내용을 일주일에 걸쳐 구현을 마치고 나서 해당 문자는 해석하지 않아야 한다는 항목을 보게되었고 해당 코드를 삭제하였다..

빌트인 함수

우리가 구현해야 하는 빌트인 함수는 echo, pwd, cd, env, export, unset, exit이다.
당장 구현하기는 어렵지 않다. 하지만 bash와 똑같이 동작하도록 만드는데는 꽤나 까다롭더라

cd

cd를 사용해 현재 디렉토리를 이동할 때, 환경변수 PWD와 OLDPWD를 각각 현재 위치와 이전 위치로 변경해주어야 한다.

echo

얘는 딱히 없다. 굳이 꼽자면 -n 옵션에 대해 -nnnnnnn 이런식으로 넣어줘도 인식해야 하는데 구현하진 않았다.

pwd

얘도 없다.

env

env 함수에 매개변수가 들어가면 too many argument 에러가 출력되어야 한다.

export

얘가 좀 까다로웠다.
우선 매개변수 없이 export 명령어만 실행하는 경우 환경변수 목록을 출력해주어야 하는데 형식은 declare -x key=value 의 형식이어야 하고, 오름차순으로 정렬되어야 한다.
환경변수 key값의 첫 번째 글자에는 _ 와 영어만 올 수 있다.
이미 선언되어 있는 환경변수를 key로 하는 경우 해당 환경변수의 value를 변경한다.

unset

특이사항 없음.

exit

매개변수로 주어진 값을 unsigned char 형식으로 exit한다.
long long 형의 범위를 넘어가는 값이나 숫자가 아닌 값을 매개변수로 주면 numberic argument required 라는 에러를 출력하고 에러코드 255로 종료된다.
2개 이상의 매개변수를 주면 too many argument 에러를 출력하고 종료되지 않는다.

redirect

입출력 위치를 바꿔주는 명령이다.
예를 들어 echo a > a.txt 라고 하면 a.txt에 echo a의 출력값인 a가 저장된다.

bash의 redirect mark는 총 4가지(<, <<, >, >>)가 있다.
< 는 뒤에 오는 파일을 표준입력으로 받아온다.
<< 는 뒤에 오는 인자값을 EOF로 인식하고, 해당 문자(혹은 문자열)이 입력될 때까지 입력을 받는다.
예를 들어 cat << a
asdf
asd
a
라고 입력하면
asdf
asd 를 입력값으로 받는다.
> 는 출력값을 뒤에 오는 파일명으로 저장한다.
>> 는 출력값을 뒤에 오는 파일명으로 저장하지만 이미 파일이 존재한다면 그 파일의 뒤에 이어 붙인다.

pipe

명령어가 pipe(|)로 구분 되는 경우 앞에 오는 명령의 출력값을 뒤에 오는 명령의 입력값으로 받는다.
앞에 오는 명령은 자식 프로세스를 생성하여 실행하고, 맨 뒤에 오는 명령만 본 프로세스에서 실행된다. (아니었다! 파이프가 존재하면 모든 명령은 자식 프로세스에서 실행된다. 때문에 cd .. | cd .. 를 입력하였을 땐 이동해서는 안된다)
출력값을 뒤에 오는 명령의 입력값으로 넘겨주기 때문에 자식 프로세스에서 실행된 명령의 출력값은 출력되서는 안된다. 하지만 에러는 출력되어야 한다.
나는 자식 프로세스에서 에러코드를 반환하면 부모 프로세스에서 에러코드를 받아 출력해주는 식으로 구현했는데, 생각해보니 애초에 표준에러로 출력했으면 표준출력이랑 표준에러가 구분돼서 출력값은 알아서 부모프로세스의 입력값으로 넘어가고 에러는 프롬프트 창에 출력됐을것 같다.
(결국 수정했다. asdf | /bin/cat 를 실행하면 에러 메시지가 두 번 출력되는 현상 때문에)

0개의 댓글