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

シェルスクリプティング

変数展開は文字列置換ではない

TT Labで続きを見る

一言でいうと

シェルは、変数を値に置き換えた後に単語を分割します。そのため、ダブルクォートを1つ付け忘れると、引数1つが複数に分割されてしまい、その事故は、空白を含むファイル名に出会った日に初めて表に出ます。

なぜ必要なのか

デプロイスクリプトが6か月間、問題なく動いていました。ある日、誰かがMy Report.pdfというファイルをアップロードしたところ、スクリプトはMyとReport.pdfの2つを削除しようとして失敗しました。コードは1文字も変わっていません。

理由は、シェルの処理順序にあります。rm $FILEに出会うと、シェルはまず$FILEを値に置換し、その結果をもう一度空白を基準に単語分割します。rm "$FILE"のようにダブルクォートで囲むと、この分割は起こりません。ルールは1つです。変数を使うときは、常にダブルクォートで囲みます。例外を悩む時間があるなら、付けてしまうほうが早いです。

どう動くのか

パラメーター展開は、デフォルト値の処理と必須値の検証を1行で済ませてくれます。3つを区別する必要があります。

構文 動作 変数に値を残すか
${VAR:-기본값} 空の場合、デフォルト値を代わりに使います 残しません
${VAR:=기본값} 空の場合、デフォルト値を代入します 残します
${VAR:?메시지} 空の場合、メッセージを出して終了します -

(表のコード内のプレースホルダーは、デフォルト値とメッセージです)

関数の引数を必須にするときにも、そのまま使えます。local name="${1:?사용법: deploy.sh <앱이름>}"という1行で、引数の検証と使い方の案内が同時に済みます(プレースホルダーは、使い方の案内文とアプリ名です)。

文字列の操作も、外部コマンドなしでできます。echo | sedでプロセスを2つ起動する代わりに、展開構文を使うとはるかに速くなります。

file="/var/log/nginx/access.log"
${file##*/}      # access.log      (앞에서 최장 매칭 제거 = 경로 떼기)
${file%.*}       # /var/log/nginx/access   (뒤에서 최단 매칭 제거 = 확장자 떼기)
${file%.log}.bak # /var/log/nginx/access.bak
${VER^^}         # 대문자로
${var//old/new}  # 전부 치환

条件検査には3つの構文があり、用途が異なります。

ファイル検査は、-f(通常ファイル)、-d(ディレクトリ)、-s(サイズが0より大きい)、-r(読み取り可能)、-x(実行可能)、-e(存在)を区別して使います。-eはディレクトリにも真になるので、ファイルだけが欲しいなら-fを使う必要があります。

現場での姿

環境変数は、設定の標準的な経路です。12-factorスタイルのデプロイでは、設定をファイルではなく環境変数で渡します。そのためスクリプトは、「環境変数があればそれを使い、なければ妥当なデフォルト値で動く」という形になります。その1行が: "${LOG_LEVEL:=info}"です。

エラーメッセージは標準エラー出力へ。成功の結果はstdout、エラーと使い方の案内はstderrに送ります。この規約を守ってこそ、result=$(script.sh)のように結果だけをキャプチャする使い方が成り立ちます。使い方の案内がstdoutに出ると、それがそのまま結果の値になってしまいます。

終了コードにも慣例があります。0は成功、1は一般的な失敗、2は使い方のエラーで、sysexitsの慣例に従うなら、64は使い方のエラー、66は入力ファイルなしです。値そのものより重要なのは、1つのプロジェクトの中で一貫して使い、文書に書いておくことです。

クォートがなくてよい場面と、あっても無意味な場面

「常にダブルクォート」が正しいルールですが、なぜそうなのかがわかっていれば、例外的な場面で迷いません。

クォートが必要ない場面。[[ ... ]]の中では単語分割とグロブ展開が起こらないので、[[ -n $VAR ]]が安全です。変数に値を入れるVAR=$OTHERも、代入の右辺では分割が起こりません。この2つを知っておく価値は、クォートを外してもよいという点にはなく、他の場面でなぜ必要なのかが鮮明になる点にあります。

クォートがあっても無意味な場面。ダブルクォートは単語分割を防ぐだけで、その中で変数展開とコマンド置換はそのまま起こります。そのため、信頼できない文字列をevalやbash -cに渡すと、クォートをいくら付けても、その中の$(...)が実行されます。このような場面は、クォートではなく、そもそもその文字列をコマンドとして解釈しない形に変える必要があります。

シングルクォートは何も展開しません。そのため、正規表現やawkプログラムのように、シェルが触ってはいけないものはシングルクォートで囲みます。逆に、その中で変数を使うには、シングルクォートをいったん閉じて開き直す形になりますが、そこまで複雑になったら、たいていはその値を環境変数や引数として渡すほうがよいです。

最後に、配列に触れておきます。引数のリストを変数1つに入れておいて、後で展開する場合がよくありますが、文字列として入れると、空白を含む引数で必ず壊れます。bashの配列に入れて"${ARGS[@]}"で展開すれば、先ほど見た"$@"と同じ性質が得られます。しかも空の配列を展開するときにも安全な形なので、条件によって引数がないこともあるコマンドを組み立てるときに、特に価値があります。

次のラボですること

あいさつのスクリプトから始めて、引数のデフォルト値と環境変数の優先順位を扱い、ファイルの種類を判別する分岐、点数の等級の境界値の処理を行い、最後には環境変数を検証して終了コードで結果を知らせるデプロイスクリプトを作ります。