터미널 프로그램들이 따르는 "규칙"
원문은 Julia Evans님이 에 게재했습니다. 이 블로그 구독하기
요즘 나는 터미널에서 일어나는 모든 일이 다음과 같은 것들의 조합이라는 생각을 자주 한다.
- 운영체제가 하는 일
- 셸이 하는 일
- 터미널 에뮬레이터가 하는 일
- 지금 실행 중인 프로그램이 하는 일 (
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를 누르면 종료된다.
이 규칙은 q로 종료하는 것이 말이 안 되는 프로그램, 예를 들어 tmux나 텍스트 에디터에는 적용되지 않는다.
규칙 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는 "핵심" 프로그램이 아니고, 그 컬러 스킴이 마음에 들지 않는 Helix 사용자라면 그냥 테마를 바꾸면 되므로, 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를 누르면 OS가 마지막 단어를 지우고, 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는 일반적으로 비대화형 프로그램은 종료시키지만, 대화형 프로그램에서는 프로그램을 종료하는 대신 현재 작업을 중단시킬 수도 있겠군") - 결국 그것을 내가 아는 명시적인 규칙으로 정식화하기
솔직히 터미널에 대한 내 이해 중 상당 부분은 아직도 "무의식적인 패턴 인식" 단계에 머물러 있다. 내가 지금까지 굳이 이런 것들을 명시적으로 정리하려고 시간을 들인 유일한 이유는 다른 사람들에게 그 작동 방식을 설명하려고 했기 때문이다. 부디 이런 "규칙"들을 명시적으로 적어 둠으로써 다른 사람들이 이 중 일부를 배우는 속도가 조금이라도 빨라지길 바란다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기