TT Lab
はじめる
学ぶ 学習パス コース

シェルスクリプティング

クォートを外すと何が起きるのか

TT Labで続きを見る

一言でいうと

シェルは変数を展開した後で、単語に分割します。クォートを付けないと、空白を含む値1つが複数の引数に変わり、その瞬間、スクリプトがまったく別のことを行います。

なぜ必要なのか

整理用のスクリプトがあります。

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

/data/my report.tmpというファイルが1つできた瞬間、rmは/data/myとreport.tmpの2つを削除しようとします。運が悪ければ、/data/myというディレクトリが実際に存在していて、それが消えてしまいます。

ここには問題が2つ重なっています。

  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[@]}      # 다시 쪼개진다 ✗

"$@"と"$*"の違いも、同じ原理です。"$@"は引数をそれぞれ保存し、"$*"は1つの文字列にまとめます。スクリプトが受け取った引数をそのまま渡すときは、常に"$@"を使います。

ルール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に入れておけば、レビューでこのような話を繰り返さなくて済みます。

現場での姿

続くラボですること

敵対的な名前のファイルを自分で作っておいて、ルールを1つずつ試します。採点ツールがみなさんの書いたスクリプトを直接実行します。"$@"の代わりに$*を使うと、引数3つが5つになって届き、getoptsの代わりに手で作ったパーサーは-voで破綻し、evalを使うと、採点ツールが入れたインジェクション文字列が実際に実行されます。書いておくだけの答えでは、合格できません。