dev.to

Bash Isn't a Programming Language. It's a Text Substitution Engine.

Bash는 프로그래밍 언어가 아니라 텍스트 치환 엔진입니다

Bash를 일반적인 프로그래밍 언어처럼 해석하면 공백, 따옴표, 파이프에서 생기는 오류를 이해하기 어렵습니다. 글은 Bash의 토큰 분리와 확장, 서브셸, 파일 디스크립터 동작을 설명하고 안전한 스크립트 작성 습관을 소개합니다.

AI 요약

Bash의 동작을 이해하려면 Python이나 JavaScript처럼 문법을 읽는 언어가 아니라, 텍스트를 변환해 Unix 프로세스를 연결하는 셸로 바라봐야 한다고 설명합니다. 이 관점에서 보면 공백 하나 때문에 대입이 실패하거나 파이프 안 루프의 변수가 사라지는 현상이 Bash의 처리 방식에서 비롯됨을 알 수 있습니다.

공백은 명령과 인수를 나눕니다

x = 10을 입력하면 Bash는 이를 x, =, 10이라는 세 단어로 나눕니다. 첫 단어 x를 실행 파일 이름으로 취급하고 나머지를 인수로 넘기므로, 해당 명령을 찾지 못하면 command not found가 발생합니다. 반대로 x=10은 공백 없는 NAME=VALUE 형태라 변수 대입으로 처리합니다.

조건식의 대괄호도 문법 기호가 아니라 명령입니다. 따라서 if [$x == 10]은 올바른 인수 구성을 만들지 못합니다. 글은 if [ "$x" == 10 ]; then처럼 대괄호와 값 사이에 공백을 두고 변수를 따옴표로 감싸라고 설명합니다. 대괄호 두 개를 쓰는 Bash의 [[ ... ]]도 대안으로 제시합니다.

확장 뒤에 프로그램이 실행됩니다

Bash는 입력을 토큰으로 나누고 중괄호 확장, 틸드 확장, 변수·명령·산술 확장 등을 거칩니다. 그 뒤 인용되지 않은 값은 공백을 기준으로 다시 분리되고, *.png 같은 패턴은 파일 이름으로 확장됩니다. 리디렉션을 처리한 다음 셸 내장 명령은 셸 안에서 실행하고, 외부 프로그램은 fork()와 execve()를 거쳐 실행합니다. 따라서 프로그램이 받는 것은 원래 입력한 문자열이 아니라 Bash가 확장을 마친 결과입니다.

예를 들어 $PATTERN에 공백이 있는데 grep $PATTERN $FILE처럼 따옴표 없이 쓰면, 확장된 값이 여러 인수로 나뉠 수 있습니다. 글은 "$VAR"로 감싸면 단어 분리와 파일 이름 확장을 막아 문자열을 한 인수로 전달한다고 설명합니다.

파이프는 서브셸을 만들 수 있습니다

cat sales.txt | while read line; do ...; done에서 루프가 파이프의 일부로 실행되면 Bash는 서브셸에서 루프를 돌릴 수 있습니다. 자식 프로세스는 부모 메모리의 복사본을 사용하므로 루프 안에서 total을 50까지 늘려도 부모 셸의 값은 그대로 0입니다. 글은 입력 리디렉션과 프로세스 치환을 사용해 루프를 현재 셸에서 실행하거나, Bash 4.2 이상의 lastpipe 옵션을 쓰는 방법을 소개합니다. 대화형 셸에서는 작업 제어를 꺼야 lastpipe가 동작합니다.

리디렉션은 왼쪽부터 적용됩니다

프로세스는 표준 입력 FD 0, 표준 출력 FD 1, 표준 오류 FD 2를 사용합니다. > output.log 2>&1은 먼저 FD 1을 파일로 바꾸고, 이어 FD 2가 FD 1의 현재 대상을 가리키게 합니다. 순서를 바꾼 2>&1 > output.log에서는 FD 2가 먼저 터미널을 가리키게 되므로 오류 출력이 파일로 가지 않습니다. Bash 4 이상에서는 &> output.log로 표준 출력과 표준 오류를 함께 보낼 수도 있습니다.

오류 처리와 정리 작업

글은 set -Eeuo pipefail을 스크립트 앞부분에 두면 명령 실패, 정의되지 않은 변수, 파이프라인 내부 명령의 실패를 감지하는 데 도움이 된다고 권합니다. 다만 set -e는 모든 실패에서 무조건 즉시 종료하는 옵션은 아니며 조건문이나 일부 명령 문맥에서는 다르게 동작합니다. 글의 댓글에서도 grep처럼 실패 코드를 정상적인 결과 신호로 쓰는 함수에서는 set -e가 예상치 못하게 작동할 수 있다는 경험이 나옵니다.

임시 디렉터리 같은 자원을 정리할 때는 trap에 종료 처리 함수를 연결하는 예도 듭니다. 종료 코드와 임시 디렉터리 경로를 보존해 정리하도록 구성합니다.

주의할 예시

본문은 변수 이름을 잘못 쓴 rm -rf "$TEM_DIR/*"가 / 아래 파일을 지울 수 있다고 설명하지만, 이 코드는 큰따옴표 안의 *가 파일 이름 확장을 일으키지 않으므로 설명과 다르게 동작합니다. 삭제 위험을 다루려면 실제 인용 방식과 변수 확장 결과를 정확히 확인해야 합니다. 본문이 제안하는 ${DIR:?Variable DIR is required} 형태는 변수가 비어 있거나 설정되지 않았을 때 명령 실행을 막는 데 쓸 수 있습니다.

dev.to 반응

  • @yuqiosipov — Bash를 잘 정리한 글입니다. 공유해 주셔서 감사합니다.
    • @manfredmorgan30 — 도움이 됩니다.
    • @smtahosin — 도움이 됐다니 기쁩니다!
    • @smtahosin — 감사합니다! 유용하게 읽으셨다니 정말 기쁩니다.
  • @donnnnn14 — 좋은 관찰입니다!
  • @sgaggjhkjh — ‘텍스트 치환 엔진’이라는 관점 덕분에, 고생으로만 익혔던 내용을 설명할 말을 찾았습니다. 공백이 들어간 =, if[의 함정, 사라진 루프 변수처럼 제가 겪은 미스터리한 Bash 오류가 모두 처리 단계와 연결된다는 걸 알게 됐습니다. 꽤 마음이 놓입니다. 서브셸의 기억 상실 사례는 실제 운영 환경에서 겪었습니다. 카운터가 아니라 임시 파일에 내용을 덧붙이는 루프였는데, 눈에 띄는 오류 없이 아무 일도 하지 않았습니다. 알아차리는 데 몇 시간이 걸렸습니다. 프로세스 치환과 lastpipe의 차이도 유용합니다. 저는 늘 프로세스 치환을 썼지만, 대화형 셸에서 lastpipe를 쓰려면 왜 set +m이 필요한지 몰랐습니다. 한 가지 덧붙이자면 엄격 모드 설정은 좋지만, grep 기반 검사처럼 함수가 정상적인 신호로 0이 아닌 값을 반환할 때 set -e가 문제를 일으켰습니다. 저는 이제 호출하는 쪽에서 명시적으로 검사합니다. 작성자도 이런 충돌을 겪었나요, 아니면 그 경우에도 이 설정을 그대로 권하나요?
  • @jsvnsk-tech — 정확합니다! 깊이 파고들면 Bash는 정말 최고입니다. 글을 공유해 주셔서 감사합니다!
  • @samstag — Bash는 애초에 프로그래밍 언어가 되려고 한 적이 없습니다. 스크립트 언어입니다.

원문: dev.to / 번역·요약: Trawling