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
の解説をご覧ください。
- zsh でシェルスクリプトを書くときの留意点 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog
ところで、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 もどうぞ。
- Ansible Advent Calendar 2023
- シェル芸 Advent Calendar 2023
- 闇の魔術に対する防衛術 Advent Calendar 2023
- Ansible Advent Calendar 2023
- Ansible Advent Calendar 2020
- DNS温泉 Advent Calendar 2019
- OSSTech Advent Calendar 2019
- Ansible Advent Calendar 2018
- OSSTech Advent Calendar 2018
- Debian/Ubuntu Advent Calendar 2017
- Linux Advent Calendar 2017
- Shell Script Advent Calendar 2017
- Shell Script Advent Calendar 2016
- OpenLDAP と仲間たち Advent Calendar 2015
- Postfix Advent Calendar 2014
- 拡張 POSIX シェルスクリプト Advent Calendar 2013