/home/caml-shaving

배쉬 스크립트에서 지뢰 피하기

뭐 이딴게 다있어

2019-10-25

태그: dev

모름지기 프로그래머는 귀찮은 반복적인 일을 자동화할 줄 알아야 한다. 대부분의 자동화는 현존 최강의 스크립트 언어 파이썬으로 해결되지만, 리눅스 커맨드를 요리조리 지지고 볶고 해야하는 경우는 쉘 스크립트가 더 편한 경우가 종종 있어서 자주 쓰는 편이다.

하지만 쉘 스크립트, 특히 배쉬 스크립트는 … 언어 모델이 지뢰밭이라서 까딱 잘못 하면 밟고 터지기 십상이다. 오늘은 내가 이때까지 밟은 지뢰들을 정리한다.

지뢰밭 목록

대입문에서 공백이 의미를 해친다

말 그대로 어떤 변수에 값을 대입할 때, 무심코 다른 언어에서 하던 것처럼 lhs = rhs 로 쓰는 순간 에러코드 127을 볼 수 있다. 이 뜻은 “현재 PATH 에서 님이 실행한 커맨드 못찾겠음” 인데, 즉 저 대입문을 대입문이 아니라 커맨드의 실행으로 인식한 것이다. 그러고보니 커맨드에서 옵션 줄 때 종종 =를 쓰지 않던가?

암튼 원하는 걸 얻으려면 딱 붙여 써야 한다: lhs=rhs

조건문에서는 공백이 의미를 갖는다

일단 쉘에서 조건문은 다음처럼 쓴다.

if [ COND ]; then
 ...
fi

if로 열고 fi로 닫는 참신한 발상은 대체 누구 생각인지 궁금해지지만 넘어가자. 뱀발로 그러면 당연히 case 문은 esac으로 닫겠지? 그렇다. 그럼 for문은 rof으로 닫지 않을까? 아니다. 이건 함정이다. 반복문은 dodone으로 닫는다. 놀랍지 않은가?

삼천포로 빠졌다. 다시 돌아와서, 저기서 [ ... ]; 부분이 조건문의 조건을 작성하는 부분인데, 여기서 중요한 것은 중괄호 사이에 반드시 공백이 있어야 한다는 것이다. 예를 들어서 VAR 변수가 정수 1과 같은지를 체크하려면:

if [ $VAR -eq 1]; then
  ...
fi

이렇게 쓰면 파싱에러가 뜬다. 정확하게

if [ $VAR -eq 1 ]; then
  ...
fi

이렇게 써줘야한다. 아주 독불장군이다.

파이프라이닝에는 숨겨진 지뢰가 있다.

이게 무슨 뜻인지 결론부터 말하면 파이프라이닝은 서브 쉘에서 실행된다. 이게 대체 뭔 말이냐? 다음 상황을 보자.

빌드 대상을 확인하는 함수와 실제 빌드를 수행하는 함수를 쉘로 대충 다음과 같이 작성했다고 하자. 이때 validate 함수는 빌드 대상을 BUILD_TARGETS 배열 변수에 쌓고 full_build 함수에서 이걸 읽어서 빌드를 수행하려고 한다.

function validate
{
  ...
  BUILD_TARGETS=( )
  ...
  BUILD_TARGETS=( "${BUILD_TARGETS[@]}" "${BUILD_TARGET}" )
  ...
}

function full_build
{
  real_build_command --targets "${BUILD_TARGETS[@]}"
}

타입도 없고 공백이 시맨틱을 갖는 쉘에서 배열을 쓸려는 시도 자체가 잘못된 것 같긴 하지만(…) 넘어가자. 암튼 이 함수들을 이렇게 쓰면 잘 된다:

...
validate
full_build

근데 빌드 하면서 stdout에 찍히는 로그를 타임스탬프랑 함께 파일에 찍고 싶어서 다음처럼 바꿨다고 하자.

validate
full_build | ts | tee full-build.log

그럼 바로 에러가 뜬다!

그 이유는 바로 쉘에서 파이프라이닝되는 명령들은 서브 쉘에서 실행되어 독립적인 환경을 갖기 때문이다. 그래서 full_build가 그냥 호출되면 validate에서 설정한 빌드 대상 변수를 잘 읽어오지만, 파이프라이닝과 함께 실행되는 경우 BUILD_TARGETS 변수가 정의되지 않았고 , 그래서 실패가 뜬다. 세상에 뭐 이딴 경우가. 스펙을 모르면 100% 밟고 터질 수 밖에 없는 초특급 지뢰다.

그래서 예전 끈닷넷님이 공유해주셨던 배쉬의 함정을 다시 읽어보고 있는데 여기 8번에 딱 이 상황이 나와있더라.

Changes to count won’t propagate outside the while loop because each command in a pipeline is executed in a separate SubShell. This surprises almost every Bash beginner at some point.

호환마마보다도 무서운 언어다. 이 외에도 위의 “배쉬 함정” 글에는 정말 참신하고도 기발한 지뢰들이 많으니 꼭 읽어보자. 그리고 shellcheck를 생활화하도록 하자. (물론 이것도 완벽한 솔루션은 아니다. 특히 정규식 관련해서는 의미가 달라지는 제안을 하기도 하던데 이건 다음 기회에.) 지뢰밭을 피하려면 지뢰탐지기가 필요하듯 우리에겐 쉘을 잘 쓰기 위한 장비가 필요하다. 물론 제일 좋은 것은 쉘을 쓰지 않아도 되는 것이겠지만 리눅스가 점령한 세상에서 그런게 있을리가 없잖아? 결국 각개로 살아남는 수 밖에 없으니 조심 또 조심하자.