位置パラメーターの一括展開 $* $@ "$*" "$@" の違いを知れ!! - Shell Script Advent Calendar 2016 - ダメ出し Blog

2016-12-15(Thu) [sh][shell] [更新履歴]

Shell Script Advent Calendar 2016 の 15日目の記事です。

位置パラメーター (Positional Parameters) の紹介と、 その値をすべて展開する $*, $@, "$*", "$@" の違いについて解説します。

$*, $@, "$*", "$@" の違いを認識し使い分けできるかどうかは、 シェルをちゃんと理解しているかどうかの指標の一つと言えるのではないかと思います。 残念ながら、適切な位置パラメーター展開を用いていないシェルスクリプトが珍しくありません。 あなたのシェルスクリプトは大丈夫ですか? あなたの好きな◯◯のソースコードや配布物に付属のシェルスクリプトも要チェックですよ! (コントリビューションのチャンスかも!)

パラメーターってなに?

シェルの「パラメーター」とは、シェルスクリプトで設定したり参照 (展開) できる各種の変数のことです。シェル変数や環境変数のことを差します。 「パラメーター」=「変数」と理解しておけばよいです。

位置パラメーターってなに?

シェルスクリプトを実行するときのコマンドラインに引数を渡すと、 シェルはそれらの値を「位置パラメーター (Positional Parameters)」に設定します。

位置パラメーターは、$1$2, … で個別の値を、 $*, $@ ですべての値を展開できます。 位置パラメーターの数は $# で展開できます。

$ cat test-dump.sh
#!/bin/sh
echo "$#"
echo "[$1] [$2] [$3]"
echo "[$*]"
echo "[$@]"
$ sh test-dump.sh foo bar qux
3
[foo] [bar] [qux]
[foo bar qux]
[foo bar qux]

シェルのマニュアルに依ってはシェルスクリプトの名前を示す変数 $0 も位置パラメーターの一種のように記載しているものがありますが、 この文書では含めないものとします。

位置パラメーターに値を設定する

位置パラメーターの値はシェルスクリプト (コマンド) の引数を受け取るだけでなく、 スクリプト中で任意の値を設定することができます。ただし、 代入構文 変数名=値 は使用できません。 また、$1 など個別の値だけ設定することもできません。

位置パラメーターを設定するには、組込みコマンド set を利用して次のように記述します。

$ set -- 1st 2nd 3rd 4th
$ echo $#
4
$ echo "[$1] [$2] [$3]"
[1st] [2nd] [3rd]
$ echo "[$*]"
[1st 2nd 3rd 4th]
$ echo "[$@]"
[1st 2nd 3rd 4th]

set コマンドは位置パラメーター値だけでなく各種シェルオプションも受け付けるため、 この例で set の最初の引数に指定しているオプション終端オプション -- が重要です。 次の例のように、set はハイフン - で始まる引数をシェルオプションと解釈します。 set -a -b 実行後、現在のシェルオプション状態を示す $- の値に ab が追加され、位置パラメーターには変化がないことに注目。

$ set aa bb
$ echo "[$*] (shell options=$-)"
[aa bb] (shell options=himBHPs)
$ set -a -b
$ echo "[$*] (shell options=$-)"
[aa bb] (shell options=abhimBHPs)
$ set -- -a -b
$ echo "[$*] (shell options=$-)"
[-a -b] (shell options=abhimBHPs)

最初の位置パラメーターの値が - で始まる文字列でなければ -- は不要ですが、 値に応じて書き分ける手間や事故を防ぐため、常に -- を指定することを推奨します。

位置パラメーターを破棄する

組込みコマンド shift を実行すると、先頭の位置パラメーター値が破棄され、 以降の値を先頭にシフトします。

$ set -- 1st 2nd 3rd 4th
$ echo "$# [$*]"
4 [1st 2nd 3rd 4th]
$ shift
$ echo "$# [$*]"
3 [2nd 3rd 4th]

shift の引数に数値を与えると、先頭からその数だけシフトします。

$ set -- 1st 2nd 3rd 4th
$ echo "$# [$*]"
4 [1st 2nd 3rd 4th]
$ shift 2
$ echo "$# [$*]"
2 [3rd 4th]

位置パラメーターをすべて破棄したいなら、shift と位置パラメーター数 $# を組み合わせましょう。

$ set -- 1st 2nd 3rd 4th
$ echo $#
4
$ shift $#
$ echo $#
0

もしくは set を使用しましょう。

$ set -- 1st 2nd 3rd 4th
$ echo $#
4
$ set --
$ echo $#
0

ただし、Solaris 10 の /bin/sh (POSIX sh 非互換) においては set -- は何の効果もなく、 位置パラメーターは変化しません。 POSIX sh かそれ以上の互換シェルなら問題ないと思われますが、 先に紹介した shift $# を用いたほうが無難かもしれません。

位置パラメーター値を個別に展開する

先に紹介したように $1, $2, … を用います。

通常の変数展開と同じく、ダブルクォート " で括られていれば値が展開されるだけ、 括られていなければ値の展開後にワード分割やパス名展開などが適用されます。

位置パラメーター値をすべて一括展開する

"$*" の場合 (ダブルクォート内の $*)

"$*" は、 位置パラメーターのすべての値の間にスペース ` ` が差し込まれた文字列に展開されます。

$ set -- 1st 2nd 3rd 4th
$ echo "$*"
1st 2nd 3rd 4th

値の間に差し込まれる文字は $IFS の先頭の文字 (デフォルトはスペース ` `) が採用されます。

$ set -- 1st 2nd 3rd 4th
$ (IFS='|'; echo "$*")
1st|2nd|3rd|4th

ときどき $IFS の変更をあるコマンドラインにだけ適用したいことがありますが、 次のように $IFS の設定と "$*" を同一コマンドラインに記述しても、 $IFS 設定の効果は得られません。何故なら、このコマンドラインは $IFS の変更より先に "$*" の展開が実行されるためです。

$ set -- 1st 2nd 3rd 4th
$ IFS='|' echo "$*"
1st 2nd 3rd 4th

"$*" はダブルクォートに括られているため、展開後にワード分割はされません。 同様にパス名展開やブレース展開なども適用されません。

展開の結果は元にように個別の値にはならず、ひと塊の一つの値になります。 次のように "$*" の展開結果を位置パラメーターに設定しなおしてみると確認できます。

$ set -- 1st 2nd 3rd 4th
$ echo "$# [$1]"
4 [1st]
$ set -- "$*"
$ echo "$# [$1]"
1 [1st 2nd 3rd 4th]

$*, $@ の場合 (ダブルクォートに括られていない $*, $@)

位置パラメーターの各値がそれぞれ個別に展開された後、個別にワード分割やパス名展開などが適用されます。

$*, $@ どちらでも結果は変わりません。以下は $* だけ紹介します。

次の例の echo $* は、 位置パラメーター展開の後、ワード分割により $IFS に含まれる文字 (デフォルトはスペース、タブ、改行) で分割され、 echo コマンドの引数には 6個の引数 foo, bar, abc, xyz, XXX, XXX が渡され、 結果、それが出力されています。 (ワード分割で空白文字が失なわれ、パラメーターの数が元の 4個から 6個に増えていることに注目)

$ set -- 'foo bar' 'abc ' 'xyz ' 'XXX   XXX'
$ echo $*
foo bar abc xyz XXX XXX

パラメーターの数が 6個になっていることを確認してみましょう。

$ set -- 'foo bar' 'abc ' 'xyz ' 'XXX   XXX'
$ echo $#
4
$ set -- $*
$ echo $#
6

次の例の echo $* は、 位置パラメーター展開の後、パス名展開の対象の文字 * を含むものがパス名展開され、 echo コマンドの引数には 2個の引数 /bin/csh, /bin/tcsh が渡され、 結果、それが出力されています。

$ set -- '/bin/*csh'
$ echo $*
/bin/csh /bin/tcsh

"$@" の場合 (ダブルクォートに括られている $@)

位置パラメーターの各値が展開されます。それだけです。 パラメーターの数は変化せず、ワード分割や各種展開もされません。

$ set -- ' foo  bar ' hoge '/bin/*csh'
$ echo "$@"
 foo  bar  hoge /bin/*csh

上記の実行例を一見すると、結果は "$*" の場合と変わりないように見えますが、 次の例が示すように "$@" は元の値と数を維持していることがわかります。

$ set -- ' foo  bar ' hoge '/bin/*csh'
$ echo $#
3
$ set -- "$@"
$ echo $#
3
$ echo "[$1] [$2] [$3]"
[ foo  bar ] [hoge] [/bin/*csh]

"$*" で同様のことを実行すると次のようになります。

$ set -- ' foo  bar ' hoge '/bin/*csh'
$ echo $#
3
$ set -- "$*"
$ echo $#
1
$ echo "[$1] [$2] [$3]"
[ foo  bar  hoge /bin/*csh] [] []

$*, $@, "$*", "$@" の使い分け

$*, $@ の使い所

もしあなたがシェルが行なうワード分割処理や各種展開処理を理解していないなら、 使わないでください。危険です。

理解していても注意して使いましょう。 位置パラメーターの値に空白文字 ($IFS に含まれる文字)、 パス名展開対象の文字 (*, ? など)、 そのほかの展開対象の文字 (ブレース展開など) が含まれてないことが確実であれば、安全です。

"$*" の使い所

位置パラメーターの数に依らず、そのままの値をまとめて扱いたい場合に使用しましょう。 例えば、ログとして位置パラメーターをすべてダンプしたい場合です。

log_error() {
  printf "%s: ERROR: %s\n" "$0" "$*" 1>&2
}

"$@" の使い所

位置パラメーターの数と値を維持したまま扱いたい場合に使用しましょう。 例えば、ほかのコマンドの引数に与える場合です。 よくあるのはラッパースクリプトでの使用です。

#!/bin/sh
exec gcc -Werror "$@"

よく次のような例を見かけますが、いずれも誤動作の要因になります。気をつけましょう。

駄目な例1:

#!/bin/sh
## これは駄目な例です!!
## スクリプトの引数をワード分割、各種展開した結果がコマンドに渡ってしまう!!
exec gcc -Werror $*

駄目な例2:

#!/bin/sh
## これは駄目な例です!!
## スクリプトの引数をワード分割、各種展開した結果がコマンドに渡ってしまう!!
exec gcc -Werror $@

zsh だと $*, $@"$@" と同じ結果になるように見えるんだけど?

その通り。 zsh のデフォルト状態では $*, $@, は "$@" と等価です。

何故かは三年前の記事に書きました。zsh オプション GLOB_SUBST, SH_WORD_SPLIT の解説をご覧ください。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。