Lobsters

Brut, the Brutal Router for Unix Tools

Brut: Unix 도구를 위한 명령 라우터

Brut은 약 120줄의 POSIX 셸 스크립트로 CLI 명령을 찾아 실행하는 라우터입니다. 명령을 작은 실행 파일로 나누고 네임스페이스와 공유 스크립트로 묶어, 런타임 의존성을 최소화한 유닉스 프로그램을 구성하는 방식을 설명합니다.

AI 요약

Brut은 Git처럼 하나의 명령 이름 뒤에 하위 명령을 붙여 실행하는 프로그램을 POSIX 셸로 구성하는 도구입니다. 단일 셸 스크립트가 커지면 기능별 실행 파일로 나누고, 공통 함수와 데이터는 별도 디렉터리에 둡니다. 라우터는 실행 파일의 이름과 인자를 살펴 하위 명령을 찾습니다. 작성자는 기본 유틸리티 외에 런타임 의존성을 두지 않는 구성을 목표로 합니다.

디렉터리 규칙과 명령 탐색

Brut 프로그램은 bin/에 라우터를 두고, lib/에는 공유 스크립트와 초기화 파일을, libexec/에는 명령 실행 파일을 둡니다. 문서와 정적 데이터는 share/에 배치하고, 번들 런타임은 그 아래 vendor/에 넣습니다. 라우터는 실행 파일의 실제 경로에서 프로그램 루트를 계산한 뒤 LIB, LIBEXEC, SHARE, VENDOR를 설정합니다.

PATH 앞부분에는 LIB, LIBEXEC, VENDOR를 차례로 추가합니다. 공유 스크립트도 $PATH를 이용해 불러올 수 있기 때문에 LIB를 가장 앞에 둡니다. 명령은 libexec/에만 한정되지 않습니다. PATH 어디에든 myprog-lint라는 실행 파일을 두면 myprog lint로 실행되고, 명령 목록에도 나타납니다. 프로그램 자체 명령이 외부 명령보다 먼저 검색되도록 프로그램의 libexec/가 앞쪽에 놓입니다.

실행 구조와 네임스페이스

Brut은 셸 함수 몇 개와 환경 변수로 동작합니다. find를 이용해 PATH의 디렉터리를 탐색하고, 현재 명령 이름에 맞는 실행 파일을 찾습니다. resolve는 현재 네임스페이스에 속한 명령만 고릅니다. 같은 이름의 실행 파일이 여러 경로에 있으면 먼저 찾은 파일을 사용합니다. 찾은 실행 파일은 exec로 실행하므로 라우터 프로세스가 명령 프로세스로 대체됩니다.

네임스페이스는 라우터를 가리키는 심볼릭 링크로 만듭니다. 예를 들어 myprog-env를 라우터에 연결하면 myprog env list 호출 시 라우터가 다시 실행되고, myprog-env-list를 찾아 실행합니다. 네임스페이스마다 별도의 lib/ 디렉터리와 _init.sh 초기화 훅을 둘 수 있습니다. 실행 파일 이름에 --를 넣으면 비공개 실행 파일로 취급합니다. 라우터의 명령 목록이나 일반 명령 탐색에는 나타나지 않지만, 다른 명령에서 직접 호출할 수 있습니다.

인자 처리와 확장 지점

라우터는 --version과 특수 인자 ---를 제외하면 옵션 문법을 강제하지 않습니다. 명령 이름을 찾을 때 하이픈으로 시작하는 인자는 건너뛰고, 그 밖의 첫 인자를 명령으로 선택합니다. 따라서 -v build처럼 옵션이 명령 앞에 올 수 있지만, --file out.txt build처럼 옵션값이 명령보다 앞에 오면 out.txt가 명령으로 선택될 수 있습니다. 작성자는 이런 경우 프로그램의 _init.sh에서 인자 목록을 직접 처리하라고 안내합니다.

_init.sh는 명령 실행 전에 설정을 바꾸거나 일찍 종료할 때 쓰고, _unhandled.sh는 명령을 찾지 못했을 때 기본 동작을 구현합니다. ---는 라우터 안에 정의된 error, usage, version 같은 헬퍼를 다시 호출하는 통로입니다. 라우터는 시작 시 호출 환경을 OLDENV에 저장하며, 프로그램이 이를 내보내면 하위 명령에서 환경을 복원할 수도 있습니다.

이식성과 실행 비용

Brut의 배포물은 POSIX 셸 스크립트 한 파일입니다. 문서는 소스 코드 주석에서 생성하며, 내용 주소 기반 버전은 주석과 빈 줄을 제외한 소스의 SHA-256 해시 앞 여섯 자리로 계산합니다. 각 버전 주소는 고정되어 있어 프로그램에 포함한 라우터와 같은 버전의 문서를 확인할 수 있습니다.

대신 실행할 때마다 파일 시스템을 검색하므로 라우팅 비용이 듭니다. 작성자는 최신 플래시 저장 장치에서 수십 분의 1초보다 작은 수준이라고 설명하지만, 네임스페이스가 중첩되면 비용도 누적됩니다. 초당 여러 번 도는 반복문이라면 라우터를 매번 거치지 말고, 반복 처리를 하나의 명령 안에 두라고 권합니다.

Lobsters 반응

  • @andyc — POSIX 셸만 쓰면 된다는 주장에 동의하기 어렵습니다. sh -c "echo 'a\nb'"는 셸에 따라 결과가 달라집니다. POSIX가 이 문제를 해결해 주지 않습니다. 이식성을 진지하게 원하면 modernish처럼 안전한 확장과 반복문 문법을 제공하는 라이브러리로 셸을 사실상 다른 언어로 만들어야 합니다. 저는 극도로 이식성이 필요한 경우에도 OSH와 Bash가 공유하는 큰 부분집합을 씁니다.
    • @LeahNeukirchen — POSIX는 일관되게 동작하는 printf를 제공합니다.
    • @andyc — 그러면 매번 printf '%s\n' "$foo"라고 써야 합니다. 친절한 방식은 아닙니다. 공통 함수를 만들려고 별도 셸 함수를 불러오면 결국 modernish를 만들기 시작하는 셈입니다.
    • @bdesham — macOS에서 기본 대화형 셸은 macOS 10.15부터 Bash에서 zsh로 바뀌었습니다. 하지만 macOS 15에서도 /bin/sh는 Bash 3.2입니다. 기본 대화형 셸을 바꾸더라도 /bin/sh가 Bash 3이라고 가정하는 기존 스크립트는 계속 작동하게 한 셈입니다.
  • @Verfeuil — 이 도구가 의도한 목표를 더 분명히 설명했으면 합니다. 셸 스크립트를 쓰기 위한 “프레임워크”인가요?
  • @hunger — 별로인 프로그래밍 환경을 쓰고, 파일을 잔뜩 만들어 제자리에 배치하고, 플랫폼마다 다른 기본 POSIX 유틸리티의 특이점을 피해 가야 합니다. 그러고도 실행은 느리고 프로세스는 잔뜩 생깁니다. 저는 설득되지 않습니다.
  • @spillybones — 이 도구가 추구하는 방향은 제게 바람직하지 않습니다. Git 같은 명령이 하위 명령을 한 바이너리로 묶는 이유는 git add라고 입력하는 편의만이 아닙니다. 하위 명령끼리 인자 처리와 Git 파일 구조 조작 로직을 많이 공유합니다. 이를 별도 바이너리로 만들면 코드가 중복됩니다. 제가 세어 보니 Git에는 하위 명령이 178개, 바이너리 크기는 4.7MB입니다. 대부분 코드를 공유한다고 가정하면 중복 바이너리가 800MB를 넘고 빌드도 번거로워집니다.
    • @5d22b — 공유 로직을 공유 라이브러리에 넣으면 안 되나요?
  • @arcade — 문학적 프로그래밍(literate programming) 방식이 정말 멋집니다.
  • @z3bra — 명령을 네임스페이스로 묶는 아이디어와 Brut이 나온 배경은 이해합니다. 하지만 새 “언어”와 프로그램 작성 방식을 배워야 한다는 점은 마음에 들지 않습니다. Plan 9처럼 디렉터리 아래 프로그램을 두고 git/clone $REPO, ip/add $IP/$MASK처럼 하위 명령을 부를 수 있으면 좋겠습니다. $PATH 어디에서든 현재 디렉터리 기준 상대 경로로 명령을 호출하도록 하면 모두 만족하지 않을까요? 실행 파일 호출 방식을 POSIX가 정한다고 생각하지 않습니다.

원문: Brut / 번역·요약: Trawling