変数展開は文字列置換ではない
한국어 원문으로 표시합니다.
한 줄 요약
셸은 변수를 값으로 바꾼 뒤에 단어를 나눈다. 그래서 큰따옴표 하나를 빼먹으면 인자 하나가 여러 개로 쪼개지고, 그 사고는 공백이 든 파일 이름을 만나는 날 처음 드러난다.
왜 이게 필요했나
배포 스크립트가 6개월 동안 잘 돌았다. 어느 날 누가 My Report.pdf 라는 파일을 올렸고, 스크립트는 My 와 Report.pdf 두 개를 지우려다 실패했다. 코드는 한 글자도 바뀌지 않았다.
이유는 셸의 처리 순서에 있다. rm $FILE 을 만나면 셸은 먼저 $FILE 을 값으로 치환하고, 그 결과를 다시 공백 기준으로 단어 분리한다. rm "$FILE" 처럼 큰따옴표로 감싸면 이 분리가 일어나지 않는다. 규칙은 하나다 - 변수를 쓸 때는 언제나 큰따옴표. 예외를 고민할 시간에 그냥 붙이는 게 빠르다.
어떻게 동작하나
파라미터 확장은 기본값 처리와 필수값 검증을 한 줄로 끝내 준다. 세 가지를 구분해야 한다.
| 문법 | 하는 일 | 변수에 값을 남기나 |
|---|---|---|
${VAR:-기본값} |
비어 있으면 기본값으로 대신 쓴다 | 남기지 않는다 |
${VAR:=기본값} |
비어 있으면 기본값을 할당한다 | 남긴다 |
${VAR:?메시지} |
비어 있으면 메시지를 내고 종료한다 | - |
함수 인자 필수화에도 그대로 쓴다. local name="${1:?사용법: deploy.sh <앱이름>}" 한 줄이면 인자 검증과 사용법 안내가 동시에 끝난다.
문자열 조작도 외부 명령 없이 된다. echo | sed 로 프로세스 두 개를 띄우는 대신 확장 문법을 쓰면 훨씬 빠르다.
file="/var/log/nginx/access.log"
${file##*/} # access.log (앞에서 최장 매칭 제거 = 경로 떼기)
${file%.*} # /var/log/nginx/access (뒤에서 최단 매칭 제거 = 확장자 떼기)
${file%.log}.bak # /var/log/nginx/access.bak
${VER^^} # 대문자로
${var//old/new} # 전부 치환
조건 검사는 세 가지 문법이 있고 용도가 다르다.
[ ... ]- POSIX 표준.sh로 도는 스크립트에서는 이것만 쓸 수 있다.[[ ... ]]- bash 확장. 단어 분리와 글로빙이 일어나지 않아 더 안전하고,=~로 정규식도 쓴다.(( ... ))- 산술 전용.(( retries > 3 ))처럼 숫자 비교가 자연스럽다.
파일 검사는 -f(일반 파일) -d(디렉터리) -s(크기 0 초과) -r(읽기 가능) -x(실행 가능) -e(존재)를 구분해서 쓴다. -e 는 디렉터리에도 참이므로 파일만 원하면 -f 를 써야 한다.
현장에서 만나는 모습
환경변수는 설정의 표준 통로다. 12-factor 스타일 배포에서는 설정을 파일이 아니라 환경변수로 넘긴다. 그래서 스크립트는 "환경변수가 있으면 그것을 쓰고, 없으면 합리적인 기본값으로 돈다"는 형태가 된다. 그 한 줄이 : "${LOG_LEVEL:=info}" 이다.
오류 메시지는 표준에러로. 성공 결과는 stdout, 오류와 사용법 안내는 stderr 로 보낸다. 이 규약을 지켜야 result=$(script.sh) 처럼 결과만 캡처하는 사용법이 성립한다. 사용법 안내가 stdout 으로 나오면 그게 곧 결과값이 되어 버린다.
종료 코드에도 관례가 있다. 0 은 성공, 1 은 일반 실패, 2 는 사용법 오류, 그리고 sysexits 관례를 따르면 64 는 사용법 오류, 66 은 입력 파일 없음이다. 값 자체보다 중요한 것은 한 프로젝트 안에서 일관되게 쓰고 문서에 적어 두는 것이다.
따옴표가 없어도 되는 자리, 있어도 소용없는 자리
"언제나 큰따옴표" 가 옳은 규칙이지만, 왜 그런지 알면 예외적인 자리에서 헤매지 않는다.
따옴표가 필요 없는 자리. [[ ... ]] 안에서는 단어 분리와 글로빙이 일어나지
않으므로 [[ -n $VAR ]] 가 안전하다. 변수에 값을 넣는 VAR=$OTHER 도
할당의 오른쪽에서는 분리가 일어나지 않는다. 이 둘을 아는 것의 값어치는
따옴표를 빼도 된다는 데 있지 않고, 다른 자리에서는 왜 필요한지가 선명해지는
데 있다.
따옴표가 있어도 소용없는 자리. 큰따옴표는 단어 분리를 막을 뿐이고,
그 안에서 변수 확장과 명령 치환은 그대로 일어난다. 그래서 신뢰할 수 없는
문자열을 eval 이나 bash -c 에 넘기면 따옴표를 아무리 붙여도 그 안의
$(...) 가 실행된다. 이런 자리는 따옴표가 아니라 애초에 그 문자열을
명령으로 해석하지 않는 형태로 바꿔야 한다.
작은따옴표는 아무것도 확장하지 않는다. 그래서 정규식이나 awk 프로그램처럼
셸이 건드리면 안 되는 것은 작은따옴표로 감싼다. 반대로 그 안에서 변수를 쓰려면
작은따옴표를 잠깐 닫았다 여는 형태가 되는데, 그렇게 복잡해지면 대개 그 값을
환경 변수나 인자로 넘기는 편이 낫다.
마지막으로 배열을 짚어 둔다. 인자 목록을 변수 하나에 담아 두고 나중에 펼치는
경우가 흔한데, 문자열로 담으면 공백이 든 인자에서 반드시 깨진다. bash 배열에
담고 "${ARGS[@]}" 로 펼치면 앞에서 본 "$@" 와 같은 성질을 얻는다. 그리고
빈 배열을 펼칠 때도 안전한 형태라, 조건에 따라 인자가 없을 수도 있는 명령을
조립할 때 특히 값이 크다.
다음 실습에서 할 것
인사말 스크립트로 시작해 인자 기본값과 환경변수 우선순위를 다루고, 파일 종류를 판별하는 분기, 점수 등급 경계값 처리, 마지막에는 환경변수를 검증해 종료 코드로 결과를 알리는 배포 스크립트를 만든다.