미니쉘 처음 만들 때 순서대로 구현하는 방법

처음 미니쉘을 만들 때는 화면에 프롬프트가 뜨고 echo가 실행되는 것만으로도 꽤 뿌듯했습니다. 그런데 따옴표와 파이프를 넣는 순간, 잘 돌아가던 코드가 갑자기 복잡해지더군요. 42의 미니쉘 과제를 기준으로 보면 가장 편한 출발점은 입력을 읽고, 해석하고, 실행하는 단계를 나눠서 한 기능씩 붙이는 것입니다. 과제 버전에 따라 세부 요구사항은 다를 수 있으니 구현 전에 자신이 받은 과제 문서도 확인하는 편이 좋습니다.
명령을 실행하기 전에 입력부터 나누기
가장 먼저 만들 것은 프롬프트와 입력 반복입니다. 입력 한 줄을 받아 처리한 뒤 다시 프롬프트를 띄우고, 입력이 끝나면 종료하는 흐름이죠. 여기까지 되면 echo hello처럼 공백으로 구분한 간단한 명령을 토큰으로 나눌 수 있습니다.
하지만 공백만 기준으로 자르면 echo "hello world"가 두 인자로 쪼개집니다. 따라서 문자를 읽는 동안 현재 위치가 따옴표 밖인지, 작은따옴표 안인지, 큰따옴표 안인지 기억해야 합니다. 따옴표 밖의 공백은 구분자지만 따옴표 안의 공백은 문자열의 일부입니다. 같은 이유로 echo "a|b"의 |는 파이프가 아니라 출력할 글자입니다.
이 단계에서 토큰의 종류를 단어, 파이프, 입력·출력 리다이렉션처럼 구분해 두면 뒤가 편합니다. 그리고 토큰을 만들자마자 실행하지 말고 문법을 먼저 검사하세요. 맨 앞의 |, 대상 파일이 없는 >처럼 잘못된 입력을 걸러낼 수 있습니다. 오류가 난 줄만 중단하고 다음 입력을 받을 수 있어야 쉘답게 동작합니다.
단일 명령과 내장 명령부터 실행하기
파싱한 결과가 명령 하나라면 실행 흐름이 비교적 단순합니다. 외부 명령은 자식 프로세스를 만들고, 실행 파일을 찾아 실행한 다음, 부모 프로세스가 종료 상태를 기다립니다. ls처럼 이름만 입력한 명령은 PATH에 등록된 디렉터리를 차례로 확인하고, /bin/ls처럼 경로가 적힌 명령은 해당 경로를 사용합니다. 명령을 찾지 못했을 때와 실행 권한이 없을 때도 구분해서 처리해야 합니다.
내장 명령은 echo, cd, pwd, export, unset, env, exit을 각각 구현합니다. 여기서 cd와 export는 조금 특별합니다. cd를 자식 프로세스에서만 실행하면 자식이 끝난 뒤 부모 쉘의 현재 위치는 그대로입니다. export와 unset도 마찬가지로 부모 쉘의 환경을 바꿔야 다음 명령에 반영됩니다. 단일 내장 명령을 실행할 때 부모에서 처리할지 먼저 판단하면 이 문제를 피할 수 있습니다.
예를 들어 cd /tmp를 실행한 뒤 pwd를 입력해 보세요. 두 번째 명령에서 /tmp가 나오면 디렉터리 변경이 유지된 것입니다. export COLOR=blue 다음에 echo $COLOR를 입력해 환경 변수 저장과 확장도 확인할 수 있습니다. 작은 사례지만 부모와 자식의 역할을 점검하기에 좋습니다.
리다이렉션과 파이프는 파일 디스크립터로 연결하기
단일 명령이 안정적으로 실행되면 입출력 방향을 바꿔 봅니다. <는 파일을 표준 입력으로 연결하고, >는 표준 출력을 파일로 보내며, >>는 기존 내용 뒤에 덧붙입니다. <<는 지정한 구분자가 나올 때까지 입력을 받는 방식입니다. 이때 파일을 열지 못했다면 명령 실행으로 넘어가지 않고 오류를 처리해야 합니다.
파이프는 앞 명령의 표준 출력을 다음 명령의 표준 입력에 연결합니다. printf hello | wc -c를 실행하면 뒤 명령은 앞 명령이 만든 글자 5개를 셉니다. 명령마다 필요한 파이프 끝을 연결한 뒤, 쓰지 않는 파일 디스크립터는 부모와 자식 모두에서 닫아야 합니다. 하나라도 불필요하게 열린 채 남으면 다음 명령이 입력 종료를 기다리며 멈춘 것처럼 보일 수 있습니다.
리다이렉션과 파이프가 한 줄에 함께 있을 때도 확인하세요. 예를 들어 cat < input.txt | wc -l은 파일에서 읽은 줄을 파이프로 넘깁니다. 어느 입력과 출력이 실제로 연결되는지는 명령별 리다이렉션을 적용한 결과에 달려 있습니다. GNU Bash 설명서도 리다이렉션을 적은 순서가 동작에 영향을 줄 수 있다고 안내하므로, 여러 연산자를 함께 쓴 경우를 별도로 시험하는 게 좋습니다.
따옴표와 종료 상태를 사례별로 확인하기
기능이 늘수록 미니쉘과 Bash에 같은 명령을 넣어 결과를 비교하는 방식이 유용합니다. echo '$HOME'은 글자 그대로 출력되고, echo "$HOME"은 변수 값으로 바뀌는지 확인해 보세요. $?는 직전 명령의 종료 상태를 나타냅니다. 성공한 명령 뒤와 실패한 명령 뒤에 각각 출력해 보면 상태 저장이 제대로 되는지 알 수 있습니다.
신호 처리도 프롬프트에서 입력을 기다릴 때와 명령이 실행 중일 때를 나눠 확인해야 합니다. Ctrl-C를 눌렀을 때 현재 작업은 중단되더라도 쉘 자체는 계속 입력을 받을 수 있어야 합니다. Ctrl-D는 입력 종료로, Ctrl-\는 상황에 맞게 다뤄야 하죠. 실행 결과만 맞추기보다 입력을 취소한 뒤 프롬프트가 정상적으로 돌아오는지도 살펴야 합니다.
저는 기능을 하나 붙일 때마다 정상 입력, 잘못된 입력, 따옴표가 섞인 입력을 한두 개씩 바로 시험하는 편이 효율적이라고 느꼈습니다. 미니쉘은 코드의 양보다 각 단계가 서로 어떤 값을 넘기는지 분명할 때 훨씬 다루기 쉬워집니다.
