昔 /bin/sh (NetBSD) 用に書いた CGI プログラムに GET リクエストのパラメータで引数を与えらるようにしようと思った。
Perl とかで書けば書くのは簡単だけど、実行環境が Perl 必須になってしまう。まあ、記述がややこしくなってメンテナンスの手間が増えてしまうようだと本末転倒なのでその時は割りきって Perl で書くことにしてしまおう。
外部コマンドの呼び出しがやたらと増えてしまう場合も同様だ。それなら Perl の方がましだろう。標準コマンドかそうでないかの違いはあるが。
(もちろん、これは閉じた環境で、外部からアクセス可能なところに置くものではない。なので、脆弱性の話は除外して考えていることをお断りしておく。)
シェルスクリプトを書くのも久しぶりだ。だいぶ忘れている。
まずは仕様の整理。
- パラメータ文字列は環境変数 QUERY_STRING で渡される。
- パラメータ文字列は ‘&’ で区切られており、複数のパラメータが指定可能。
- 各パラメータは名前と値が ‘=’ で連結されている。
- 今回作成する CGI はパラメータ名 ‘target’ のみを受け付け、それ以外は無視する。
- パラメータ ‘target’ は複数指定でき、値は CGI 内部から実行する別のコマンドの引数として渡す。
- パラメータの値はデコードしない。空の場合は無視する。
ということで、最初に ‘&’ を区切り文字として分解する部分。
区切り文字といえば、シェル変数 IFS がどーたらという話があった記憶があるが、どう使うのだったか覚えていない。
標準入力を分割するのに使えるのだった記憶はあるのだが…。
ググると、for 文と組み合わせる例が見つかった。それを参考に記述。
IFS='&'
for i in $QUERY_STRING; do
(変数 $i には & で切り分けられた内容が順に入る)
done
次は ‘=’ で名前と値に分割する部分。これはパラメータ展開でプレフィックスやサフィックスを削除する ‘%’ や ‘##’ が使える。
key=${i%%=*}
value=${i#*=}
‘%%’ は後方から最長一致でマッチした部分を取り除く。つまり、’%%=*’ で最初に出現した ‘=’ 以降の文字を取り除くことになる。’#=’ は前方から最短一致でマッチした部分を取り除く。つまり、’#*=’ で最初に出現した ‘=’ までの文字を取り除くことになる。
結果は位置パラメータ ’$@’ に格納して、外部コマンド呼び出し時に展開するのが良いだろう。位置パラメータ ‘$@’ の初期化は “shift $#” のようにするらしい(’$#’ は位置パラメータの個数)。
shift $#
for i in ...; do
...
value=${i#*=}
set "$@" "$value"
done
これで for ループが終了したあとには、位置パラメータに値がセットされている。
まとめると、下のように。
OLDIFS=$IFS
IFS='&'
shift $#
for i in $QUERY_STRING; do
key=${i%%=*}
value=${i#*=}
case "$key" in
target)
case "$value" in
-* | *=* | '')
;;
*)
set "$@" "$value"
;;
;;
esac
done
IFS=$OLDIFS
sh exec-make.sh "$@"
必要な処理が終わったあとは IFS は元の値に戻しておく。間違えるとややこしいので。
重ねて書くが、このスクリプトは内部の閉じた環境からのみアクセスする前提で書いているので、脆弱性への対策は行なっていない。シェルスクリプトの記述の事例として読んでもらうのは良いが、CGI の事例としては読まないことをおすすめする。入力を解析する必要のある CGI なら、シェルスクリプトではなく、ちゃんとしたライブラリとかの支援がある言語環境で書いたほうが良いに決まっている。
今回のスクリプトでは使わなかったが、if 文で変数の内容を比較する際に、変数と比較対象の頭にそれぞれ文字 ‘x’ をつけて比較する、という記述を見かける。
if [ x"$hoge" = xyes ]; then ...
なんでだったか忘れたので、これもググった。
$hoge が ‘-f’ とかだった場合に test(1) のオプションとみなされることを防ぐためらしい。
手元の環境 (NetBSD 5.1.2) で試してみたら、”test -f = -f” の “-f” はオプションとは解釈されないようだ(test(1) も シェル組み込みも)。引数の解釈の順序が先頭から順じゃないのかな?