LabHub

셸 스크립팅 · 입력과 인자를 안전하게 다루기 · 이론

따옴표를 빠뜨리면 무슨 일이 생기는가

LabHub 에서 이어서 보기

한 줄 요약

셸은 변수를 확장한 뒤 단어로 쪼갭니다. 따옴표를 안 붙이면 공백이 든
값 하나가 여러 개의 인자로 변하고, 그 순간 스크립트가 전혀 다른 일을 합니다.

왜 이게 필요했나

정리 스크립트가 있습니다.

OLD=$(find /data -name '*.tmp' -mtime +7)rm $OLD

/data/my report.tmp 라는 파일이 하나 생기는 순간, rm
/data/myreport.tmp 두 개를 지우려 시도합니다. 운이 나쁘면
/data/my 라는 디렉터리가 실제로 있고, 그게 사라집니다.

여기에는 문제가 두 개 겹쳐 있습니다.

1. 따옴표 없음 — 단어 분리가 일어납니다.
2. 줄 단위 파싱 — 파일명에 줄바꿈이 들어갈 수도 있습니다(가능합니다).

어떻게 동작하나

규칙 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 로

직접 $1case 로 돌리는 코드는 곧 무너집니다. 짧은 옵션이라면
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 ;;  esacdoneshift $((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 을 쓰면 채점기가 넣은 주입 문자열이 실제로 실행됩니다.
적어 놓기만 한 답으로는 통과할 수 없습니다.