クォートを外すと何が起きるのか
한국어 원문으로 표시합니다.
한 줄 요약
셸은 변수를 확장한 뒤 단어로 쪼갭니다. 따옴표를 안 붙이면 공백이 든 값 하나가 여러 개의 인자로 변하고, 그 순간 스크립트가 전혀 다른 일을 합니다.
왜 이게 필요했나
정리 스크립트가 있습니다.
OLD=$(find /data -name '*.tmp' -mtime +7)
rm $OLD
/data/my report.tmp 라는 파일이 하나 생기는 순간, rm 은
/data/my 와 report.tmp 두 개를 지우려 시도합니다. 운이 나쁘면
/data/my 라는 디렉터리가 실제로 있고, 그게 사라집니다.
여기에는 문제가 두 개 겹쳐 있습니다.
- 따옴표 없음 — 단어 분리가 일어납니다.
- 줄 단위 파싱 — 파일명에 줄바꿈이 들어갈 수도 있습니다(가능합니다).
어떻게 동작하나
규칙 1 — 변수는 항상 따옴표로
f="my report.txt"
rm $f # rm my report.txt → 인자 2개
rm "$f" # rm "my report.txt" → 인자 1개 ✅
예외는 거의 없습니다. "이건 공백이 없을 게 확실한데" 라는 판단이 틀리는 날이 스크립트가 망가지는 날입니다.
배열도 마찬가지입니다.
args=(-l -a "my dir")
ls "${args[@]}" # 각 원소가 하나의 인자로 ✅
ls ${args[@]} # 다시 쪼개진다 ✗
"$@" 와 "$*" 의 차이도 같은 원리입니다. "$@" 는 인자를 각각
보존하고, "$*" 는 하나의 문자열로 합칩니다. 스크립트가 받은 인자를
그대로 넘길 때는 항상 "$@" 입니다.
규칙 2 — 파일 목록은 NUL 로 넘긴다
파일명에 들어갈 수 없는 문자는 NUL 하나뿐입니다. 줄바꿈도 공백도 들어갈 수 있습니다. 그래서 안전한 파이프는 NUL 구분입니다.
# 안전
find /data -name '*.tmp' -mtime +7 -print0 | xargs -0 rm --
# 더 안전 (xargs 도 필요 없다)
find /data -name '*.tmp' -mtime +7 -delete
# 루프가 필요하면
while IFS= read -r -d '' f; do
process "$f"
done < <(find /data -name '*.tmp' -print0)
IFS= read -r 조합도 관용구입니다. IFS= 는 앞뒤 공백이 잘리는 것을
막고, -r 은 백슬래시를 이스케이프로 해석하지 않게 합니다.
규칙 3 — -- 로 옵션과 파일을 가른다
-rf 라는 이름의 파일이 있다면(만들 수 있습니다), rm $file 은 그것을
옵션으로 해석합니다. -- 는 "여기부터는 전부 인자" 라는 표시입니다.
rm -- "$f"
grep -- "$pattern" "$file"
규칙 4 — 옵션 파싱은 getopts 로
직접 $1 을 case 로 돌리는 코드는 곧 무너집니다. 짧은 옵션이라면
getopts 가 정석입니다.
verbose=0; out=""
while getopts ":vo:" opt; do
case $opt in
v) verbose=1 ;;
o) out=$OPTARG ;;
:) echo "-$OPTARG 에 값이 필요합니다" >&2; exit 2 ;;
\?) echo "알 수 없는 옵션: -$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
# 남은 것이 위치 인자
앞의 콜론(":vo:")은 조용한 에러 모드를 켭니다. 이걸 넣어야 : 와 \?
분기가 동작해 우리가 직접 메시지를 만들 수 있습니다.
규칙 5 — 입력을 신뢰하지 않는다
사용자 입력을 명령으로 만들면 안 됩니다.
# 절대 금지
eval "grep $user_input file.txt"
# 값으로만 쓴다
grep -- "$user_input" file.txt
eval 은 문자열을 셸 코드로 실행합니다. 입력에 ; rm -rf / 가 들어오면
그대로 실행됩니다. 마찬가지로 파일 경로도 검증합니다 — ../../etc/passwd
같은 값이 오면 의도한 디렉터리를 벗어납니다.
점검 도구
shellcheck 는 위 실수를 대부분 잡아 줍니다. 따옴표 누락(SC2086),
read 의 -r 누락(SC2162), 쓸모없는 cat(SC2002)까지 지적합니다.
CI 에 넣어 두면 리뷰에서 이런 얘기를 반복하지 않아도 됩니다.
현장에서 만나는 모습
- 배치가 몇 년 잘 돌다가 어느 날 파일을 지움 → 공백 든 파일이 처음 생긴 날.
- 경로에 한글이 들어가자 실패 → 인코딩이 아니라 대개 따옴표 문제.
- 옵션 순서를 바꾸면 동작이 달라짐 → 손으로 만든 파서의 전형적 증상.
이어지는 실습에서 할 것
적대적인 이름의 파일을 직접 만들어 놓고 규칙을 하나씩 시험합니다.
채점기가 여러분이 쓴 스크립트를 직접 돌립니다 — "$@" 대신 $* 를
쓰면 인자 3개가 5개로 도착하고, getopts 대신 손으로 판 파서는 -vo 에서
무너지며, eval 을 쓰면 채점기가 넣은 주입 문자열이 실제로 실행됩니다.
적어 놓기만 한 답으로는 통과할 수 없습니다.