LabHub
배우기 러닝패스 코스

Shell Scripting

Variable Expansion Is Not String Substitution

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

셸은 변수를 값으로 바꾼 뒤에 단어를 나눈다. 그래서 큰따옴표 하나를 빼먹으면 인자 하나가 여러 개로 쪼개지고, 그 사고는 공백이 든 파일 이름을 만나는 날 처음 드러난다.

Concept map: 뒤에 · 그 결과를 다시 공백 기준으로 단어 분리 · 변수를 쓸 때는 언제나 큰따옴표. · 파라미터 확장

왜 이게 필요했나

배포 스크립트가 6개월 동안 잘 돌았다. 어느 날 누가 My Report.pdf 라는 파일을 올렸고, 스크립트는 MyReport.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}  # 전부 치환

조건 검사는 세 가지 문법이 있고 용도가 다르다.

파일 검사는 -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[@]}" 로 펼치면 앞에서 본 "$@" 와 같은 성질을 얻는다. 그리고 빈 배열을 펼칠 때도 안전한 형태라, 조건에 따라 인자가 없을 수도 있는 명령을 조립할 때 특히 값이 크다.

다음 실습에서 할 것

인사말 스크립트로 시작해 인자 기본값과 환경변수 우선순위를 다루고, 파일 종류를 판별하는 분기, 점수 등급 경계값 처리, 마지막에는 환경변수를 검증해 종료 코드로 결과를 알리는 배포 스크립트를 만든다.