Bash 스크립트를 더 엄격하고 예측 가능하게 만들기 위한 ‘엄격 모드’ 설정과 변형들, 그리고 각각의 장단점을 정리한다.
URL: https://olivergondza.github.io/2019/10/01/bash-strict-mode.html
Title: Bash strict mode and why you should care
Aaron Maxwell이 “비공식 Bash 엄격 모드(Unofficial Bash Strict Mode)”라고 부르는 것을 소개한 훌륭한 글을 처음 접한 지도 꽤 시간이 흘렀고, 그 이후로 저는 일상적으로 그 장점을 활용해 왔습니다. 이제는 제 bash 스크립트 템플릿의 일부가 되어, (글쎄요, 거의) 엄격 모드 헤더 없이 새 파일을 만드는 일이 없을 정도입니다.
대화형 셸(interactive shell)로도 쓰이면서 동시에 꽤 복잡한 시스템 프로그래밍 언어의 목적도 수행해야 하는 언어를 설계하는 일이 결코 쉽지 않다는 것은 분명합니다. 하지만 시간이 지나면서, bash가 더 엄격하고, 더 예측 가능하며, 전반적으로 “프로그래밍 언어”에 좀 더 가까웠으면 하는 상황을 계속 마주치게 됩니다.
다행히도, 제가 (반쯤 농담이지만) 세상에서 가장 인기 있는 난해(에소테릭) 프로그래밍 언어라고 부르는 도구로 작업하면서 정신 건강을 지키기 위해, 그쪽으로 조금 더 다가갈 수 있는 방법들이 있습니다. 그리고 제가 아는 한, bash에서 엄격 모드라는 개념을 대중화한 사람은 Aaron이 처음이었습니다.
요컨대 엄격 모드는 파일 맨 앞에 넣어 bash의 동작을 더 안전한 쪽으로 바꾸는 작은 스니펫(snippet)일 뿐입니다. Aaron이 제안한 엄격 모드 선언은 다음과 같습니다:
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
이게 무엇을 얻는지(관련된 자세한 내용은 원문에 아주 많이 있으니 참고하세요) 간단히 살펴보겠습니다:
set -u: 정의되지 않은 변수를 치환하려고 하면 오류를 냅니다. 정의되지 않은 변수를 치환하면서도 올바른 코드를 작성할 수 있는 경우가 꽤 있긴 하지만, 오타나 리팩터링 실수는 이런 방식으로 드러나는 일이 매우 흔합니다.set -e: 실행한 어떤 명령이든 0이 아닌 종료 코드를 반환하면 종료합니다. 여기서 명백한 예외는 루프 조건, 조건문 내부에서 실행되는 명령, 또는 논리 연산자(||, &&)의 인자로 실행되는 명령입니다.set -o pipefail은 연결된 명령 중 하나라도 실패하면 즉시 명령 파이프라인(pipeline)을 중단합니다. 이 두 설정의 조합은 제가 bash 스크립팅에서 가장 자주 마주친 문제 중 하나를 해결하려고 합니다. 즉, 어떤 중요한 작업이 실패했는데도 스크립트가 계속 실행되어 스크립트의 불변 조건(invariant)이 깨진 채로 나중에 훨씬 뒤에서 실패하거나, 심지어 끝까지 가서 성공 종료 코드로 종료해 버리는 문제 말이죠.즉, bash로 작업할 때 처리해야 할 오류와 제약을 추가로 “한 접시 더” 요청하는 방법입니다. 믿으셔도 됩니다. 이건 좋은 일입니다. 원문에서는 엄격 모드와 맞지 않는 흔한 코드 패턴을 어떻게 고쳐 적응할지에 대해서도 꽤 길게 다루는데, 코드가 덜 관용적(비정형적)으로 변하는 경우는 드물고, 대부분은 훨씬 더 탄탄해집니다.
마지막 줄은 “내부 필드 구분자(Internal Field Separator)”를 조정하여, 공백이 들어 있는 문자열의 단어 분리(word splitting) 동작이 더 직관적으로 되게 합니다. 즉, 버그 있는 코드에서 공백이 종종 원치 않는 구분자로 동작하는 문제를 줄이려는 것입니다.
물론 Aaron만 더 매끄러운 bash 경험을 추구한 것은 아니어서, 더 많은 엄격 모드 변형들이 등장했습니다. 제가 정말 좋아하는 것은 Michael Daffin이 공개한 버전입니다:
#!/bin/bash
set -uo pipefail
trap 's=$?; echo "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
IFS=$'\n\t'
이전 버전의 변형처럼 보이는데(다만 set -e는 빠져 있습니다), 여기서 trap은 무엇일까요? bash의 트랩(trap)은 스크립트가 지정한 에러 코드로 종료할 때 실행할 (문자열) 코드 스니펫을 등록합니다. 또는 ERR의 경우처럼, 어떤 명령이 0이 아닌 종료 코드를 반환할 때(예외는 set -e에서와 동일하게 적용됩니다) 실행됩니다.
저는 이것이 set -e와 set -o pipefail과 함께 쓸 때 진짜 보석이라고 생각합니다. set -e와 pipefail만으로도 스크립트는 중단되지만, 어디서 실패했는지는 알려주지 않기 때문입니다. 이 트랩을 쓰면, (종료 코드를 확인하지 않으면 놓칠 수 있는) 실패를 명확히 보고하면서, 실패한 줄 번호와 실패한 명령까지 출력해 줍니다! 즉, 빠르게 실패하고(fail fast), 요란하게 실패합니다.
음, 완전히 그렇지는 않습니다. 이런 것들이 개발자의 정신 건강에 기여하는 건 맞지만, 비용이 있습니다. Aaron의 원문에서 다루는 주의점 외에도, 다음을 조심하세요:
더 고급 엄격 모드를 조합하는 것은 bash를 더 온전한 언어로 만드는 것만큼이나, 팀이 도구에 대해 쌓아온 집단적 경험으로부터도 멀어지게 합니다. 파일 맨 앞의 난해한 소음 같은 설정에 놀랄 팀원을 떠올려 보세요. 그들이 안다고 생각했던 코드의 의미론(semantics)이 바뀐 걸 알아차렸을 때의 반응은 말할 것도 없고요. 이는 문서와 재사용 가능한 코드 스니펫에도 영향을 미치는데, 이들이 기본 동작을 조용히 전제하고 있기 때문에 관련성이 떨어집니다. Stack Overflow에서 복사한 코드가 의도대로 동작하지 않는다는 걸 들어본 적이 누가 없겠어요?😱
IFS 문제. 제 관점에서, 다른 트윅들은 “피할 수 있고 피해야 마땅한 상황”(예: 정의되지 않은 변수 사용)을 용납하지 않음으로써 안전성을 높입니다. 하지만 IFS에서 공백 문자를 빼는 것은, 변수 치환 작업에서 자주 하는 실수를 보정하기 위해 의미론을 바꾸는 것에 가깝습니다. 더 좋은 코드를 쓰게 강제하는 것이 아니라, 나쁜 코드를 더 관대하게 받아들이고 그 결과를 올바르게(-비슷하게) 만들려고 하는 셈이죠.
bash trap 사용의 끔찍한 함정 하나(대부분 사람들은 아프게 겪으며 배웁니다)는, 여러 트랩이 스택처럼 쌓여 순서대로 실행되는 것이 아니라, 해당 시그널별로 이전 트랩을 대체한다는 점입니다. 다시 말해 ERR에 바인딩된 다른 트랩을 선언하는 순간, 엄격 모드 선언이 제공하던 진단 출력이 대체되어 사라집니다. 다행히도 ERR 트랩을 진단 목적 외의 이유로 도입할 동기는 별로 없어서, 실제 코드에서 충돌할 가능성은 크지 않다고 봅니다.
그래서 저는 결국 제 자체 엄격 모드를 가꾸게 되었고, 지금까지도 대략 이런 모습입니다:
#!/usr/bin/env bash
set -euo pipefail
trap 's=$?; echo >&2 "$0: Error on line "$LINENO": $BASH_COMMAND"; exit $s' ERR
bash 인터프리터를 찾기 위해 항상 /usr/bin/env를 사용합니다.set -e는 빠지지 않습니다.IFS는 건드리지 않습니다.참고로 set -euo pipefail(그리고 그 부분집합들)은 꽤나 유명해져서, 많은 사람들이 군데군데 변형을 사용하고 있습니다. 일관된 엄격 모드 정의를 사용하면, 모든 스크립트가 예측 가능하게 동작한다는 장점이 있습니다. bash 기본 동작에서 벗어나기로 선택했으면, 최소한 그 방식만큼은 일관되게 합시다.
이제 여러분의 삶에서도 bash 엄격 모드를 고민해 볼 때가 되지 않았나요?
Tags: bash,productivity