nsh 커스텀 쉘 개발 - Lexer

상태로 토큰 구분하는 Lexer 만들기

편집

windows, linux, macOS에서 쓸 수 있는 커스텀 쉘 nsh를 만들고 있어요. 언어는 Rust, C, C++ 중에 고민하다가 크로스 컴파일과 배포 편의성 때문에 Rust로 정했어요.

Lexer란

Lexer(렉서, 어휘 분석기)는 문자들의 나열(그냥 하나의 긴 문자열)을 받아서, 의미 있는 단위인 토큰들의 나열로 바꿔주는 역할을 해요. 컴파일러나 인터프리터, 쉘 같은 프로그램이 입력을 처리하는 가장 첫 단계고, 이 다음 단계인 Parser가 토큰을 보고 실제 명령어 구조를 조립하게 돼요.

예를 들어

echo "hello world" foo

이런 입력이 있다면:

사람 눈에는 당연히 세 부분(echo, hello world, foo)으로 보이지만, 컴퓨터 입장에서는 그냥 문자들이 쭉 이어진 것뿐이에요. "가 어디서 열리고 닫히는지, 공백이 단어를 구분하는 기호인지 아닌지 전혀 모르죠. Lexer가 이걸 사람이 이해하는 단위로 쪼개주는 거예요.

처음엔 split_whitespace()로 대충 공백 기준으로만 쪼개봤어요. 근데 이러면 echo "hello world" foo가 echo, "hello, world", foo 네 조각으로 잘못 쪼개져요. 따옴표 안의 공백은 무시해야 하는데, 이 방식은 따옴표라는 개념 자체를 몰랐던 거죠.

그래서 상태를 기억하는 Lexer로 다시 만들었어요. 문자를 하나씩 순회하면서, "지금 따옴표 안에 있는지"를 상태로 기억하는 방식으로 다시 짰어요.

  1. 상태와 토큰 정의

따옴표 상태는 bool(안/밖) 하나로는 표현이 안 돼요. 큰따옴표 안에서는 작은따옴표가 그냥 평범한 문자로 취급되고 그 반대도 마찬가지라서, "따옴표 밖(None) / 작은따옴표 안(Single) / 큰따옴표 안(Double)" 세 가지 상태가 필요했어요.

  1. Lexer 구조체

input은 분석할 원본 문자열, quote는 위에서 정의한 상태, pos는 현재 위치를 담아둘 자리예요.

  1. 따옴표 문자 처리

따옴표 문자를 만나면, 만나기 직전의 self.quote 값이 뭐였는지를 보고 세 가지로 나눠서 처리해요.

  • 직전 상태가 None(따옴표 밖)이었다면, 지금 이 문자는 여는 따옴표라는 뜻이에요. 이때 self.quote 값을 그 따옴표 종류에 맞게(작은따옴표면 Single, 큰따옴표면 Double) 바꿔서, 앞으로 만나는 문자들이 "따옴표 안"으로 처리되게 해요.

  • 직전 상태가 지금 만난 것과 같은 종류의 따옴표 안이었다면, 닫는 따옴표라는 뜻이에요. self.quote를 다시 None으로 되돌려서, 이제부터는 "따옴표 밖" 상태로 돌아가게 해요.

  • 직전 상태가 다른 종류의 따옴표 안이었다면, 지금 이 문자는 특별한 의미 없이 그냥 평범한 글자예요. 상태는 안 바꾸고 버퍼에 그대로 추가해요.

예를 들어

git commit -m "fix: timeout error"

라는 입력이 있다면:

  • git, commit, -m: 각각 일반 문자들이 버퍼에 쌓이다가 공백을 만나면 하나씩 토큰으로 확정

  • 큰따옴표를 만날 때: 직전 상태가 None, 여는 따옴표, Double로 전환 (이 따옴표 자체는 버퍼에 안 들어감)

  • fix:, 공백, timeout, 공백, error: Double 상태 안이라 공백까지 포함해서 전부 버퍼에 쌓임

  • 마지막 큰따옴표를 만날 때: 직전 상태가 Double(같은 종류), 닫는 따옴표, None으로 전환

결과는 git, commit, -m, fix: timeout error 네 개의 토큰이에요. 따옴표 덕분에 fix: timeout error가 공백을 포함해도 하나의 인자로 묶인 걸 확인할 수 있어요.

  1. 공백 처리

공백을 만났을 때 quote가 None(따옴표 밖)이면 지금까지 모은 글자들을 하나의 토큰으로 확정해요.

단, 버퍼가 비어있으면(공백이 연속으로 여러 개인 경우) 빈 토큰을 만들지 않도록 체크했어요. 따옴표 안이면 공백도 그냥 문자로 취급해서 버퍼에 쌓아요. 그 외 일반 문자는 항상 버퍼에 쌓기만 해요.

테스트

결과: [Word("echo"), Word("hello world"), Word("foo")]

hello world가 공백을 포함한 채로 하나의 토큰으로 잘 묶이는 걸 확인했어요.

댓글 0

아직 댓글이 없어요. 첫 댓글을 남겨보세요.