LabHub
배우기 러닝패스 코스

Shell Scripting

Scripts That Survive Hostile Filenames

LabHub 에서 이어서 보기

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

목표

공백·줄바꿈·대시로 시작하는 파일 이름과, 옵션과 주입 문자열까지 견디는 스크립트를 씁니다. 각 규칙을 깨진 쪽과 고친 쪽을 나란히 두고 확인합니다.

환경

/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" 라는 이름을 인자로 직접 넘겨 지워지는지

단계

  1. 적대적인 이름 네 종류를 만듭니다.
  2. 같은 값에 따옴표를 붙였을 때와 뺐을 때의 인자 수를 세어 적습니다.
  3. 같은 디렉터리를 줄 단위로 셀 때와 -print0 으로 셀 때의 개수를 적습니다. 두 숫자가 다릅니다.
  4. clean.sh 를 씁니다. 파일 이름 하나를 인자로 받아 지웁니다. 없는 파일이면 0 이 아닌 코드로 끝나야 합니다.
  5. wrap.sh 를 씁니다. 받은 인자마다 ARG:<값> 을 한 줄씩 냅니다.
  6. opts.sh 를 씁니다. -v, -o <값> 을 받고 나머지 인자 수를 냅니다. 출력은 verbose=0 out= rest=0 형태입니다.
  7. search.sh 를 씁니다. 패턴과 파일을 받아 검색합니다. eval 은 쓰지 않습니다.
  8. 다섯 규칙과 그 근거를 정리합니다.

참고

4단계에서 find 로 이름을 뽑으면 앞에 ./ 가 붙어 문제가 가려집니다. 그래서 채점기는 이름을 인자로 직접 넘깁니다. 현장에서도 그 형태가 위험합니다 — for f in * 나 다른 프로그램이 준 이름을 그대로 넘길 때입니다.

적대적인 이름 만들기

적대적인 이름 네 종류를 만듭니다.

공백·줄바꿈·대시로 시작하는 이름, 그리고 평범한 이름 하나. ls -b 로 보면 특수문자가 이스케이프되어 보입니다.

따옴표 하나로 인자 수가 바뀐다

같은 값에 따옴표를 붙였을 때와 뺐을 때의 인자 수를 세어 적습니다.

set -- $fset -- "$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 이 관습으로만 남습니다.