터미널 프로그램들이 따르는 "규칙"
최근 터미널에서 일어나는 모든 일이 결국 다음 요소들의 조합으로 이루어진다는 생각을 자주 했습니다.
- 운영체제가 담당하는 부분
- 셸이 담당하는 부분
- 터미널 에뮬레이터가 담당하는 부분
- 현재 실행 중인 프로그램이 담당하는 부분(
top이나vim,cat같은 프로그램)
앞의 세 가지(운영체제, 셸, 터미널 에뮬레이터)는 어느 정도 알려진 영역입니다. 리눅스에서 GNOME Terminal과 bash를 쓰고 있다면 이들이 어떻게 상호작용하는지 대략 추론할 수 있고, 그 동작 중 일부는 POSIX로 표준화되어 있기도 합니다.
하지만 네 번째 요소(“현재 실행 중인 프로그램”)는 정말 무슨 일이든 할 수 있을 것 같습니다. 도대체 어떤 프로그램이 어떻게 동작할지 어떻게 알 수 있을까요?
글이 좀 긴 편이라 간단히 목차를 정리해 두었습니다.
프로그램들은 놀랍도록 일관되게 동작한다
제가 아는 한, 터미널 프로그램이 어떻게 동작해야 하는지에 대한 제대로 된 표준은 없습니다. 그나마 가장 가까운 것이라고 한다면 다음 정도입니다.
- POSIX는 대부분 터미널 에뮬레이터/OS/셸이 함께 어떻게 동작해야 하는지를 규정합니다.
cp같은 핵심 유틸리티가 어떻게 동작해야 하는지에 대해서는 몇 가지를 규정하고 있는 것으로 알고 있지만, 제가 알기로는 예를 들어htop이 어떻게 동작해야 하는지에 대해서는 언급이 없습니다. - 이 커맨드라인 인터페이스 가이드라인
하지만 표준이 없음에도 불구하고, 제 경험상 터미널 프로그램들은 꽤 일관된 방식으로 동작합니다. 그래서 제가 경험한 프로그램들이 대체로 따르는 “규칙”들을 목록으로 정리해 보고 싶었습니다.
이 규칙들은 규범이 아니라 관찰입니다
이 글의 목적은 터미널 프로그램 작성자들에게 이 규칙들을 꼭 따라야 한다고 설득하려는 것이 아닙니다. 이 규칙들에는 예외가 많고, 그 예외에는 대개 타당한 이유가 있습니다.
하지만 제가 처음 써 보는 터미널 프로그램이 어떻게 동작할지 예측할 수 있다는 것은 매우 유용합니다. “프로그램이 도대체 무슨 짓을 할지 알 수 없어”라는 상태 대신, “일단 기본적으로는 이런 규칙을 따를 거라고 기대하고, 예외는 따로 짧게 기억해 두자”라고 생각할 수 있기 때문입니다.
그래서 저는 20년 동안 터미널을 쓰면서 관찰한 프로그램들의 동작 방식과, 왜 그렇게 동작한다고 생각하는지, 그리고 그 규칙이 “깨지는” 몇 가지 사례를 정리해 보려 합니다.
어떤 “규칙”이 프로그램이 직접 구현해야 할 책임인지가 항상 분명한 것은 아닙니다
프로그램이 직접 구현해야 할 책임이 비교적 분명한 관습도 많습니다. 예를 들면 다음과 같습니다.
- 설정 파일은
~/.BLAHrc나~/.config/BLAH/FILE,/etc/BLAH/같은 곳에 위치해야 한다 --help를 실행하면 도움말이 출력되어야 한다- 프로그램은 일반적인 출력은 stdout으로, 오류는 stderr로 출력해야 한다
하지만 이 글에서는 프로그램의 책임인지가 100% 분명하지 않은 것들에 집중하려 합니다. 예를 들어 제게는 Ctrl-D를 누르면 REPL이 종료되는 것이 일종의 “자연 법칙”처럼 느껴지지만, 사실 프로그램이 이를 명시적으로 구현해야 하는 경우가 많습니다. cat은 Ctrl-D 지원을 직접 구현할 필요가 없지만, ipython은 직접 구현해야 합니다. (자세한 내용은 아래 “규칙 3”에서 다루겠습니다)
어떤 것이 프로그램의 책임인지를 이해하면, 프로그램마다 구현이 조금씩 다른 것이 훨씬 덜 당황스럽게 느껴집니다.
규칙 1: 비대화형 프로그램은 Ctrl-C를 누르면 종료되어야 한다
이 규칙이 존재하는 가장 큰 이유는, 비대화형 프로그램이 SIGINT 시그널 핸들러를 따로 설정하지 않았다면 기본적으로 Ctrl-C를 누를 때 종료되기 때문입니다. 그래서 일종의 “기본 동작대로 행동하라”는 규칙이라고 할 수 있습니다.
많은 사람이 헷갈려 하는 점은, 이 규칙이 python3나 bc, less 같은 대화형 프로그램에는 적용되지 않는다는 것입니다. 대화형 프로그램에서는 Ctrl-C의 역할이 다르기 때문입니다. 프로그램이 어떤 작업(예를 들어 less에서의 검색이나 python3에서의 파이썬 코드 실행)을 수행 중이라면, Ctrl-C는 그 작업을 중단시킬 뿐 프로그램 자체를 종료하지는 않습니다.
대화형 프로그램에서 이것이 어떻게 동작하는지 예를 들면, Ctrl-C를 눌렀을 때 검색을 중단시키는 prompt-toolkit(iPython이 입력 처리에 사용하는 라이브러리)의 코드는 다음과 같습니다.
규칙 2: TUI 프로그램은 q를 누르면 종료되어야 한다
TUI 프로그램(less나 htop 같은)은 보통 q를 누르면 종료됩니다.
이 규칙은 tmux나 텍스트 에디터처럼 q로 종료하는 것이 말이 되지 않는 프로그램에는 적용되지 않습니다.
규칙 3: REPL은 빈 줄에서 Ctrl-D를 누르면 종료되어야 한다
REPL(python3나 ed 같은)은 보통 빈 줄에서 Ctrl-D를 누르면 종료됩니다. 이 규칙은 Ctrl-C 규칙과 비슷합니다. “cooked 모드”에서 cat 같은 프로그램을 실행 중일 때 빈 줄에서 Ctrl-D를 누르면 운영체제가 기본적으로 EOF를 반환하기 때문입니다.
제가 주로 사용하는 REPL들(sqlite3, python3, fish, bash 등)은 실제로 cooked 모드를 사용하지 않지만, 기본 동작을 모방하기 위해 모두 이 단축키를 구현하고 있습니다.
예를 들어 Ctrl-D를 눌렀을 때 종료되는 prompt-toolkit의 코드는 여기에서, readline의 같은 코드는 여기에서 볼 수 있습니다.
사실 저는 이 규칙이 거의 깨지는 것을 본 적이 없어서 얼마 전까지만 해도 이를 “터미널 물리 법칙”이라고 생각했습니다. 하지만 위 링크에서 볼 수 있듯, 이는 결국 각 입력 라이브러리가 개별적으로 구현해야 하는 것에 불과합니다.
누군가 Erlang REPL은 Ctrl-D를 눌러도 종료되지 않는다고 알려 주었는데, 역시 모든 REPL이 이 “규칙”을 따르는 것은 아닌 모양입니다.
규칙 4: 16색 이상을 사용하지 말 것
터미널 프로그램들은 기본 16가지 ANSI 색상 외의 색상을 거의 사용하지 않습니다. 색상을 헥스 코드로 지정하면 일부 사용자의 배경색과 충돌할 가능성이 매우 높기 때문입니다. 예를 들어 텍스트를 #EEEEEE로 출력하면 어두운 배경에서는 괜찮아 보여도 흰 배경에서는 거의 보이지 않게 됩니다.
하지만 기본 16색만 사용하면 사용자가 터미널 에뮬레이터에서 배경색과 어느 정도 잘 어울리도록 색상을 설정해 두었을 가능성이 훨씬 높습니다. 기본 16색을 고수해야 하는 또 다른 이유는, 터미널 에뮬레이터가 어떤 색상을 지원하는지에 대해 가정을 덜 해도 되기 때문입니다.
제가 보기에 이 “규칙”을 깨는 경우는 보통 텍스트 에디터뿐입니다. 예를 들어 Helix는 기본적으로 기본 ANSI 색상이 아닌 보라색 배경을 사용합니다. Helix는 “핵심” 프로그램이 아니고, 이 색상 구성이 마음에 들지 않는 사용자라면 그냥 테마를 바꾸면 될 것이므로, 이 규칙을 어기는 것이 괜찮아 보입니다.
규칙 5: readline 키 바인딩을 어렴풋이나마 지원할 것
제가 사용하는 거의 모든 프로그램은 의미가 있다면 readline 키 바인딩을 지원합니다. 예를 들어 여러 프로그램에서 줄 끝으로 이동하는 Ctrl-E를 어떻게 정의하는지 링크로 모아 보면 다음과 같습니다.
- ipython (Ctrl-E 정의 부분)
- atuin (Ctrl-E 정의 부분)
- fzf (Ctrl-E 정의 부분)
- zsh (Ctrl-E 정의 부분)
- fish (Ctrl-E 정의 부분)
- tmux의 명령 프롬프트 (Ctrl-E 정의 부분)
이 프로그램들 중 어느 것도 실제로 readline을 직접 사용하는 것은 아니고, 그냥 emacs/readline 키 바인딩을 나름대로 모방할 뿐입니다. 항상 정확히 모방하는 것은 아닙니다. 예를 들어 atuin은 Ctrl-A를 prefix로 사용하는 것으로 보여 Ctrl-A가 줄 시작으로 이동하지 않습니다.
또 이 프로그램들은 모두 내부적으로 잘라내기/붙여넣기 버퍼를 따로 구현하고 있는 듯합니다. 그래서 Ctrl-U로 한 줄을 삭제한 뒤 Ctrl-Y로 붙여넣을 수 있습니다.
예외는 다음과 같습니다.
- 일부 프로그램(
git,cat,nc같은)은 줄 편집 기능을 전혀 지원하지 않습니다(백스페이스와Ctrl-W,Ctrl-U제외) - 역시나 텍스트 에디터는 예외입니다. 모든 텍스트 에디터는 텍스트 편집에 대해 각자의 방식을 가지고 있습니다
“프로그램이 어떤 키 바인딩을 지원하는가?”라는 질문에 대해서는 터미널에서 텍스트 입력은 복잡하다라는 글에서 더 자세히 다루었습니다.
규칙 5.1: Ctrl-W는 마지막 단어를 삭제해야 한다
저는 (텍스트 에디터 외에는) Ctrl-W가 마지막 단어를 삭제하지 않는 프로그램을 본 적이 없습니다. 이는 Ctrl-C 규칙과 비슷합니다. 프로그램이 “cooked 모드”에 있을 때 기본적으로 운영체제는 Ctrl-W를 누르면 마지막 단어를, Ctrl-U를 누르면 전체 줄을 삭제합니다. 그래서 보통 프로그램들은 이 동작을 모방합니다.
텍스트 에디터 외에는 이 규칙의 예외를 떠올릴 수 없는데, 혹시 있다면 꼭 알려 주시면 좋겠습니다!
규칙 6: 파이프로 출력할 때는 색상을 비활성화할 것
대부분의 프로그램은 파이프로 출력할 때 색상을 비활성화합니다. 예를 들면 다음과 같습니다.
rg blah는 출력에서blah의 모든 등장 부분을 하이라이트하지만, 출력 대상이 파이프나 파일이라면 하이라이트를 끕니다.ls --color=auto는 터미널에 출력할 때는 색상을 사용하지만 파이프에 쓸 때는 사용하지 않습니다
두 프로그램 모두 터미널에 출력할 때는 출력 형식도 다르게 표시합니다. ls는 파일을 열로 정렬해 보여 주고, ripgrep은 일치하는 결과를 헤더와 함께 그룹화합니다.
색상을 강제로 사용하고 싶다면(예를 들어 색상을 직접 보고 싶을 때), unbuffer를 사용해 프로그램 출력이 tty인 것처럼 속일 수 있습니다. 예를 들면 다음과 같습니다.
unbuffer rg blah | less -R
이 규칙을 “어기는” 프로그램도 분명 있을 테지만 지금 당장 떠오르는 예는 없습니다. 일부 프로그램은 색상을 강제로 켜는 --color 플래그를 제공하기도 합니다. 위 예시라면 rg --color=always | less -R처럼 실행할 수도 있습니다.
규칙 7: -는 stdin/stdout을 의미한다
보통 프로그램에 파일 이름 대신 -를 전달하면 상황에 따라 stdin에서 읽거나 stdout으로 씁니다. 예를 들어 클립보드에 있는 파이썬 코드를 black으로 포맷한 뒤 다시 복사하고 싶다면 다음과 같이 실행할 수 있습니다.
pbpaste | black - | pbcopy
(pbpaste는 Mac용 프로그램이며, 리눅스에서는 xclip으로 비슷한 작업을 할 수 있습니다)
제 느낌으로는 의미가 있는 경우 대부분 프로그램이 이 관습을 구현하고 있으며, 지금 당장 떠오르는 예외는 없지만 분명 많은 예외가 있을 것입니다.
이런 “규칙”들을 익히는 데는 오랜 시간이 걸립니다
이 규칙들을 익히는 데 오랜 시간이 걸렸던 이유는 다음과 같은 과정을 거쳐야 했기 때문입니다.
- 규칙이 어디선가 통한다는 사실 자체를 배우고(“
Ctrl-C를 누르면 프로그램이 종료된다”) - 몇 가지 예외를 알아차리며(“좋아,
Ctrl-C는find는 종료시키지만less는 종료시키지 않는구나”) - 무의식적으로 패턴을 파악하고(“
Ctrl-C는 대체로 비대화형 프로그램은 종료시키지만, 대화형 프로그램에서는 프로그램을 종료하는 대신 현재 작업을 중단시킬 수도 있구나”) - 마침내 이를 내가 아는 명시적인 규칙으로 정리하는 과정이 필요했습니다
솔직히 말해 터미널에 대한 제 이해 중 상당 부분은 아직도 “무의식적인 패턴 인식” 단계에 머물러 있습니다. 제가 굳이 시간을 들여 이를 명시적으로 정리하고 있는 유일한 이유는 다른 사람들에게 어떻게 동작하는지 설명하려 애써 왔기 때문입니다. 이 “규칙”들을 이렇게 명시적으로 적어 두면 다른 분들이 이런 내용을 조금 더 빨리 익히는 데 도움이 되기를 바랍니다.
글을 무작위로 읽기