Scripts That Survive Hostile Filenames
한국어 원문으로 표시합니다.
목표
공백·줄바꿈·대시로 시작하는 파일 이름과, 옵션과 주입 문자열까지 견디는 스크립트를 씁니다. 각 규칙을 깨진 쪽과 고친 쪽을 나란히 두고 확인합니다.
환경
/root/args 아래에서 작업합니다. 재료는 직접 만듭니다.
mkdir -p /root/args/data && cd /root/args/data
: > "my report.tmp"
: > "$(printf 'two
lines').txt"
: > "-rf"
: > "normal.txt"
ls -b
세 번째가 핵심입니다. -rf 라는 이름의 파일을 만들 수 있습니다. 그리고
두 번째처럼 이름에 줄바꿈도 들어갑니다 — 파일명에 못 들어가는 문자는
NUL 하나뿐입니다.
만들 것
/root/args/split.txt 따옴표 유무에 따른 인자 수
/root/args/nul.txt 줄 단위와 NUL 단위로 센 개수
/root/args/clean.sh 파일 하나를 이름이 무엇이든 안전하게 지운다
/root/args/wrap.sh 받은 인자를 쪼개지 않고 그대로 넘긴다
/root/args/opts.sh getopts 로 옵션을 판다
/root/args/search.sh 입력을 값으로만 쓴다
/root/args/report.md 다섯 규칙 정리
채점 방식
3~7단계는 채점기가 여러분의 스크립트를 적대적인 입력으로 직접 호출합니다.
wrap.sh "a b" "c" "d e" 를 넘겨 3개로 도착하는지
opts.sh -v -o 값 / 붙여 쓴 -vo 값 / 값 빠진 -o / 모르는 -Z
search.sh "alpha; touch /tmp/…" 를 넣고 그 파일이 생기는지
clean.sh "-rf" 라는 이름을 인자로 직접 넘겨 지워지는지
단계
- 적대적인 이름 네 종류를 만듭니다.
- 같은 값에 따옴표를 붙였을 때와 뺐을 때의 인자 수를 세어 적습니다.
- 같은 디렉터리를 줄 단위로 셀 때와
-print0으로 셀 때의 개수를 적습니다. 두 숫자가 다릅니다. clean.sh를 씁니다. 파일 이름 하나를 인자로 받아 지웁니다. 없는 파일이면 0 이 아닌 코드로 끝나야 합니다.wrap.sh를 씁니다. 받은 인자마다ARG:<값>을 한 줄씩 냅니다.opts.sh를 씁니다.-v,-o <값>을 받고 나머지 인자 수를 냅니다. 출력은verbose=0 out= rest=0형태입니다.search.sh를 씁니다. 패턴과 파일을 받아 검색합니다.eval은 쓰지 않습니다.- 다섯 규칙과 그 근거를 정리합니다.
참고
4단계에서 find 로 이름을 뽑으면 앞에 ./ 가 붙어 문제가 가려집니다.
그래서 채점기는 이름을 인자로 직접 넘깁니다. 현장에서도 그 형태가 위험합니다 —
for f in * 나 다른 프로그램이 준 이름을 그대로 넘길 때입니다.
적대적인 이름 만들기
적대적인 이름 네 종류를 만듭니다.
공백·줄바꿈·대시로 시작하는 이름, 그리고 평범한 이름 하나. ls -b 로 보면 특수문자가 이스케이프되어 보입니다.
따옴표 하나로 인자 수가 바뀐다
같은 값에 따옴표를 붙였을 때와 뺐을 때의 인자 수를 세어 적습니다.
set -- $f 와 set -- "$f" 뒤에 $# 을 보면 숫자로 나옵니다. 왜 그렇게 되는지도 한 줄 적으세요 — 셸은 확장한 뒤에 단어로 쪼갭니다.
줄로 세면 틀리고 NUL 로 세면 맞다
같은 디렉터리를 줄 단위로 셀 때와 -print0 으로 셀 때의 개수를 적습니다.
두 숫자가 다릅니다.
이름에 줄바꿈이 든 파일은 줄 단위로 읽으면 두 개로 세어집니다. find -print0 은 NUL 로 나눕니다 — 파일명에 못 들어가는 유일한 문자라서 안전합니다.
이름이 대시로 시작할 때
clean.sh 를 씁니다. 파일 이름 하나를 인자로 받아 지웁니다. 없는 파일이면
0 이 아닌 코드로 끝나야 합니다.
rm -f "$f" 는 이름이 -rf 면 그것을 옵션으로 읽습니다. -- 가 '여기부터는 인자' 라는 표시입니다. 없는 파일에는 0 이 아닌 코드로 끝나야 합니다 — rm -f 는 없어도 성공하기 때문입니다.
받은 인자를 그대로 넘기기
wrap.sh 를 씁니다. 받은 인자마다 ARG:<값> 을 한 줄씩 냅니다.
"$@" 는 인자를 각각 보존하고 $* 는 하나로 합칩니다. 따옴표 없는 $@ 도 다시 쪼개집니다. 채점기가 "a b" 와 "d e" 를 넘겨 공백이 살아 있는지 봅니다.
옵션 파서는 직접 만들지 않는다
opts.sh 를 씁니다. -v, -o <값> 을 받고 나머지 인자 수를 냅니다.
출력은 verbose=0 out= rest=0 형태입니다.
손으로 판 파서는 -vo 값 처럼 붙여 쓴 형태에서 무너집니다. getopts ":vo:" 의 앞 콜론은 조용한 에러 모드를 켜서 : 와 \? 분기를 직접 처리하게 해 줍니다. 끝에 shift $((OPTIND - 1)) 을 빠뜨리면 나머지 인자 수가 틀립니다.
입력은 값이지 코드가 아니다
search.sh 를 씁니다. 패턴과 파일을 받아 검색합니다. eval 은 쓰지 않습니다.
eval 은 문자열을 셸 코드로 실행합니다. 채점기가 alpha; touch <파일> 을 패턴으로 넘겨 그 파일이 생기는지 봅니다. 막기만 하고 정상 검색이 죽으면 그것도 실패입니다.
다섯 규칙 정리
다섯 규칙과 그 근거를 정리합니다.
규칙마다 '무엇을 막는가' 를 한 줄로 적으세요. 특히 NUL 을 쓰는 이유 — 파일명에 못 들어가는 문자가 그것 하나뿐이라는 사실 — 이 빠지면 -print0 이 관습으로만 남습니다.