셸 스크립팅 · 변수와 조건 · 이론
변수 확장은 문자열 치환이 아니다
한 줄 요약
셸은 변수를 값으로 바꾼 뒤에 단어를 나눈다. 그래서 큰따옴표 하나를 빼먹으면 인자 하나가 여러 개로 쪼개지고, 그 사고는 공백이 든 파일 이름을 만나는 날 처음 드러난다.
왜 이게 필요했나
배포 스크립트가 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 은 입력 파일 없음이다. 값 자체보다 중요한 것은 한 프로젝트 안에서 일관되게 쓰고 문서에 적어 두는 것이다.
다음 실습에서 할 것
인사말 스크립트로 시작해 인자 기본값과 환경변수 우선순위를 다루고, 파일 종류를 판별하는 분기, 점수 등급 경계값 처리, 마지막에는 환경변수를 검증해 종료 코드로 결과를 알리는 배포 스크립트를 만든다.