LoginSignup
22
28

More than 1 year has passed since last update.

シェルスクリプトの $* と $@ の違いと雑学色々

Last updated at Posted at 2022-12-31

TL;DR

$*$@ の違いは複数の引数を一つに結合するかしないかの違いです。通常は複数の引数を一つに結合したりしないので "$@" を使いましょう。またダブルクォートなしの $*$@ はバグに近いレベルの間違った使い方なので覚えなくてよいです。

  • "$*" = "$1 $2 $3 ..." と同じ意味
    • すべての位置パラメータを結合して、一つの引数として結合にする時などに使う
    • 他の言語で例えるなら、配列を結合 (join) して一つの文字列を作る処理に相当する
  • "$@" = "$1" "$2" "$3" ... と同じ意味
    • 以下の場合などに使う(通常必要なのはこっち
    • すべての位置パラメータを別のコマンドにそのまま渡す時
    • for ループで繰り返す時

使い分けは簡単

$*$@ の違いをちゃんと説明すると長くなりますが、おそらくあなたにとって必要なものは "$@" です。

書き方 いつ使うか? 書き方 いつ使うか?
$* 使いません $@ 使いません
${*} 使いません ${@} 使いません
"$*" 結合する時 "$@" 使います
"${*}" ほぼ使いません "${@}" ほぼ使いません

まず位置パラメータを含め変数を参照する時にダブルクォートしないのは無しです。理由は予期せぬ変数展開やパス名展開が行われるからです。詳細は「シェルスクリプトの変数はダブルクォートしなければいけない!という話」を参照してください。この理由により上半分は「使いません」で終わりです。

ダブルクォートはほぼ必須ですが { } は必要な時だけ書けば十分です。常に ${var} のように { } を書く人がいるようですが、そういう人に限って面倒なのかダブルクォートをしてないことをよく見かけます。逆です。省略可能なのは { } であり、ダブルクォートは(本当に不要な場合を除き)省略できません。常に { } を使ってもかまわないと思いますがダブルクォートも書きましょう。

  • ${var} ・・・ ダブルクォートが抜けている!
  • "$var" ・・・ このように書け!
  • "${var}" ・・・ 問題ないが、必要ないし面倒なので私はこのように書かない
  • "${10}" ・・・ 例外 10 以上の位置パラメータはこのように書かないといけない
  • "foo $var baz" ・・・ 隣の文字とくっついてないなら { } は不要
  • "foo${var}baz" ・・・ { } は隣の文字とくっついて意味が変わったり見づらくなるときだけ書けば良い

なお "${*}""${@}" に関しては隣の文字とくっつけて書くような場面はあまりないので「ほぼ使いません」。ということで残る書き方は "$*""$@" だけということになります。

"$@" は引数全てを意味する特殊なパラメータ(変数)で $1, $2, $3, ... のすべてを意味する、いわば「配列のようなもの」です。例えば以下のように別のコマンドに引数配列全てを渡すというような使い方ができます。シェルスクリプトにとって "..." とは文字列を作るものではなく、特殊な意味を持つ文字をエスケープするためのもので、位置パラメータがない時は "$@" は空文字ではなく空の位置パラメータを意味します。

func1() {
  echo "$@" # echo 'a' 'b' 'c' と等しい
}
func1 a b c

func2() {
  echo "$@" # echo と等しい(echo '' ではない)
}
func2

それに対して "$*" というのは文字列の結合で、他の言語であれば、配列を引数に取り区切り文字で結合した文字列を返す str_join() 関数とでも言うべきものです。位置パラメータを IFS 変数の最初の一文字で結合させるために使います。

# IFS のデフォルト値は、スペース タブ 改行 の 3 文字
# 補足 zsh では \0 を加えた 4 文字
IFS=","
func() {
  echo "$*" # => echo 'a,b,c' と等しい
}
func a b c

この二つはどちらも位置パラメータを扱うための書き方ですが、用途が全く別なので混乱するようなものではありません。位置パラメータを一つの文字列に結合したい場面はそれほどあるわけではないので、通常必要なものは "$@" というわけです。

雑学(本編?)

関連しそうな話題を色々と

なぜ $*$@ と書いたらダメなのか?

引数の意味が変わってしまうからです。

create_file() {
    touch "$@" # 「foo bar.txt」というファイルが作成されます
    touch $@   # 「foo」と「bar.txt」というファイルが作成されます
}
create_file "foo bar.txt"

delete_file() {
    rm "$@" # 「foo*.txt」というファイルが削除されます
    rm $@   # foo*.txt に一致する「foo1.txt」や「foo2.txt」が(あれば)削除されます
}
delete_file "foo*.txt"

$* を使った場合も同様です。こんな挙動は望んでいませんし、どう考えても危険ですよね?

{ } は本当はどういう時に必要なのか?

{ } が本当に必要なのは、変数名の後に英数文字とアンダースコアが続く場合です。例えば ${var}baz$varbaz と書いてしまえば別の変数名になるというのは明らかでしょう。それ以外の文字であれば { } を使わなくても良いのですが、zsh では [ が後に続く場合でも { } が必要です。

var="123"
echo "${var}baz" # 必須
echo "$var baz"  # 必須ではない
echo "$var/baz"  # 必須ではない

echo "$var[baz"   # zsh 以外ではこの書き方でも良いが
echo "${var}[baz" # zsh ではこのように書く必要がある
# zsh では $var[1] のように配列への参照として解釈される

このように { } が必要な場面はシェルによって異なる可能性があります。とはいえ互換性のことを考えると [ 以外に増えることはないでしょう。しかしながらこのような違いをいちいち考えるのは面倒ですし、そもそも何のために { } を使うのかというと見やすくするためです。なので私は「隣の文字とくっついているときは見づらい」という理由で { } を使っています。逆に常に { } を使うのは面倒な上に逆に見づらいと思っています。まともなテキストエディタを使っていれば、ダブルクォートの中の変数には色がつくので困りません。

set で位置パラメータに設定できる

set コマンドは set -e などのようにシェルのオプションを設定するコマンドですが、位置パラメータへ値を設定する機能も持っています。なぜ本質的に全く関係のない機能を一つのコマンドに持たせたのかよくわかりませんね。一つのことだけをせよという UNIX 哲学に反しています。

set コマンドによる位置パラメータの設定は、すべてを一括で設定することしかできず、途中の $2 だけを変更するというようなことは(簡単には)できません。

set -- a b c # $1 = a, $2 = b, $3 = c

# 何も変更せずにそのまま再設定したい場合
set -- "$@"

# $2 だけ変更したい場合
set -- "$1" B "$3" # $1 = a, $2 = B, $3 = c

上記の例では -- はなくても構いません。もし -a のような文字列を位置パラメータに入れたい場合は set -a ではシェルのオプションとみなされてしまうので set -- -a と書かなければなりません。

位置パラメータを空にするには set -- を実行します。set だけだと現在のシェル変数(シェルによってはシェル関数も含む)の一覧を出力する(セットなのに出力とはこれ如何に)という、これまた全く別の機能(unset は変数や関数と関連しているが set は変数や関数とはほとんど無関係。setunset はその名前に反して対称の機能ではない)を持っているので、位置パラメータを空にするときは -- は必須です。

# 位置パラメータを空にする方法
# set だと違う意味になるので -- は必須
set -- # もしくは shift $#

余談ですが set -- は POSIX で標準化されている方法でが、古い(POSIX 準拠ではない)Bourne シェルでは使えません。例えば Solaris 10 の /bin/sh は Bourne シェルなのでこの方法は使えませんが、POSIX に準拠している方の /usr/xpg4/bin/sh であれば動作します。代わりに shift $# を使えば Bourne sh でも POSIX sh でもどちらでも位置パラメータを空にできますが、今更 Bourne シェル対応で書く必要はないでしょう。まあ shift $# でも、そこまで分かりづらいということもないのでどちらでもかまわないと思いますが。

$*$@ は同じ意味ではない

ダブルクォートをしない $*$@ が同じ意味だと書いてあるのをよく見かけますが、これは正確ではありません。論より証拠ということで異なる結果になる例を紹介します。

#!/bin/bash

set -- a b c
IFS=,

s=$* # この時点で変数 s には「a,b,c」が代入されている
echo "$s" # => a,b,c

# 参考 ダブルクォートしない場合は IFS で
# 再度分割されるため以下のように出力される
# echo $s # => a b c

s=$@ # この時点で変数 s には「a b c」が代入されている
echo "$s" # => a b c

何故このような違いが発生するかというと $* を変数に代入する時に、IFS の最初の文字(この例ではカンマ)を区切り文字として結合するのに対して(bash では)$@ を変数に入れる時にスペース区切りで結合するからです。この話での重要なポイントは「変数に代入する時」です。

配列ではない変数には単一の値しか代入することができません。位置パラメータは配列のようなものなので、単一の値しか入らない変数に入れる場合、一つの文字列として結合されてから代入されます。$* は結合する時に IFS の最初の文字を区切り記号として結合することが決まっていますが、$@ を結合する場合、区切り記号をどうするかはシェル依存です。シェルごとに異なる動作をするので POSIX では標準化されていない(はず)です。

$ bash -c 'IFS=,; s=$@; echo "$s"' - a b c # ksh, mksh も同様
a b c

$ dash -c 'IFS=,; s=$@; echo "$s"' - a b c # yash, zsh も同様
a,b,c

$*$@ は同じである」という解説はコマンドの引数として使う場合のことしか考慮しておらず、変数への代入や case のような単一の値を要求する場面の話が抜けています。

script.sh
set -- a b c # 位置パラメータに a b c を設定する
IFS=,
case $@ in # 変数の代入と同じように単一の値を要求する = 結合が行われる
  "a b c") echo "space" ;; # bash, ksh, mksh
  "a,b,c") echo "comma" ;; # dash, yash, zsh
esac

実は echo $*echo $@ のような使い方でも内部的には(おそらく)異なる処理が行われています。

  • $* の場合
    • IFS の最初の文字で結合する
    • IFS のいずれかの文字で分割する
  • $@ の場合
    • IFS のいずれかの文字で分割する

$* は結合した文字で分割するという無意味なことをしており、したがって最終的な結果は同じになります。$*$@ は違いがないのではなく、同じ結果になってしまうということなのです。

"[$@]" はどのように解釈される?

以下のような結果になる理由を正しく説明できるでしょうか?

set -- a b c
echo "[$@]" # => [a b c]

実は "[$@]" は次のように解釈されます。

set -- a b c
echo '[a' 'b' 'c]'

つまり "[$@]" の最初の [ は一番最初の引数に結合され、] は一番最後の引数に結合されます。この時、echo '[a' 'b' 'c]' の出力結果が [a b c] のようにスペース区切りになるのは echo の仕様です。echo コマンドに複数の引数を渡した場合の仕様は、複数の引数をスペースを区切り文字として結合します。IFS を区切り文字とするわけではありません。

set -- a b c
IFS=,

echo "$*" # echo 'a,b,c' と同等 => a,b,c
echo "$@" # echo 'a' 'b' 'c' と同等 => a b c

さて引数が一つのときとゼロ個のときはどのように解釈されるでしょうか? 答えは以下のとおりです。

set -- a
echo "[$@]" # => [a]

set -- 
echo "[$@]" # => []

なにかに使えそうでありながら、使いどころがあまりない仕様です。

位置パラメータ全体を一つの変数に保存することはできない

ここまでの話ですでに気づいているかもしれませんが、位置パラメータを一つの(配列ではない)変数に単純に保存することはできません。例えば以下のようなコードは間違いです。なぜなら一つの文字列として結合されてしまっているからです。

backup=$@

set -- $backup # 正しく戻せるとは限らない

set -- "'foo bar'"
echo "$1" # => 'foo bar'
backup=$@
set -- $backup
echo "$1" # => 'foo

上記のような書き方をしても値に特殊な文字が入っていなければ動いてしまうために、この方法で配列を一つの変数に入れることができると勘違いされることがありますが、それは間違いです。

これを正しく行うには(POSIX では標準化されていない)配列を使う必要があります。ただし配列の変数にそのまま代入しても駄目で配列として代入する構文 (...) を使わなければいけません。

# 配列として宣言する(必須ではない)
# 補足 配列として代入すれば自動的に配列になるので宣言する必要はない
declare -a backup

# 配列と宣言された変数に代入したとしても、配列の代入とは解釈されない
# backup=$@

# 配列として代入する構文
backup=("$@")

set -- "${backup[@]}"

同様のことを POSIX 準拠の範囲で行いたい場合はシェル関数を使うと良いでしょう。位置パラメータはシェル関数にローカルなので、位置パラメータを保存するために利用することができます。

set -- a b c
func() {
  # この中でいくら位置パラメータを変えようが呼び出し元には影響がない
  set -- 1 2 3
  echo "$@" # => 1 2 3
}
func
echo "$@" # => a b c

また上記の方法を応用すれば、options="-j 5 -B"; make $options "$file" のような単語分割を使った方法を使わずに、安全に引数を組み立てることができます。詳細は ShellCheck - SC2086 を参照してください。

別の方法としてシェルエスケープを行って変数に保存し、戻す時は eval を使うという方法もあるにはあるのですが説明が面倒なので省略します。素直に bash を使ったほうが良いです。

ちなみに bash などの配列の構文は位置パラメータの構文を踏まえており同じような動作になっています。要するに * は結合 @ は配列です。

a=(1 2 3)

echo "${a[*]}" # echo "1 2 3" と解釈される
echo "${a[@]}" # echo "1" "2" "3" と解釈される

a2=("${a[@]}") # 配列を別の配列に代入する時もダブルクォートすること

POSIX で標準化された機能の範囲で位置パラメータを変数に簡単に保存できないという問題は、POSIX シェルに配列の機能が標準化されていないことが根本的な原因です。UNIX の開発者たちは Bourne シェルの次のシェルとして rc シェルを開発していました。rc シェルでは全ての変数が配列です。UNIX の開発者にとっては、それくらい配列はシェルにとって必要だと考えられていたものなのです(連想配列はシェルには不要だと思いますが)。残念ながら UNIX の次の OS である Plan 9 の(事実上の)開発中止とともに rc シェルが普及することもありませんでした。

本来配列はシェルの機能として必要なものです。変数をダブルクォートせずに参照した時に単語分割される理由は、一つの変数に複数の値を(問題があるにしろ)入れられるようにするためで配列があれば不要な機能です。POSIX で配列が標準化されていない理由は、単に当時まで配列をサポートしたシェルが少なかったからでしかないのですが、配列をサポートしたシェルは普及しているので、もうそろそろ標準化を進めるべきだと思います。POSIX シェルは 1989 年の rc シェルにすら追いついていないのです。

なぜみんな $* と $@ で混乱するのか?

一番の理由は古い本で当たり前のように $* が使われているからではないでしょうか? シェルの本はなかなか新しくならず、そのせいで今ではバッドプラクティスとなったようなものが改定されずに残っています。本が改定されず昔のシェルスクリプトがそれなりにそのまま動くので、シェルスクリプトは 30 年前から変わらず、そしてこれからも変わらないと勘違いされがちですが、そんな事はありません。少しづつ変わっています。もちろん全てが変わってしまったわけではありませんが、30 年前の Bourne シェルと現在の POSIX シェルでは、Python2 と Python3 ぐらいの違いはあります。

変数を使う時にダブルクォートしないのは、今ではバッドプラクティスです。これは ShellCheck を使ってコードの問題をチェックすればすぐにわかります。ダブルクォートしてない変数は(一部を除き)警告が出力されるはずです。ダブルクォートせずに変数を使う場面が全くないというわけではありませんが、$*$@ に限って言えばダブルクォートせずに使うことはまずありません

なぜ昔はすべての引数を意味する場面で $* が使われていたのでしょうか? おそらくファイル名や値に空白を含めようとは考えなかった時代(1980 年代)の名残なのではないかと思います。シェルや Unix コマンドの仕様を見るとデフォルトでは空白はフィールド区切りとして扱われ、そうしたくない場合に \ でエスケープするという考え方がデフォルトです。テキストファイルは改行文字で区切られた行の配列であり、行(文字列)はスペースやタブで区切られた単語(値)の集まりであるという考え方が Unix のテキスト処理の根底の設計にあるように思えます。空白はフィールド区切りであるという発想は、元をたどるとおそらく英語は単語は空白で区切るものというコンピュータ誕生以前からの長い文化から来ているのでしょう。

そしてもう一つ、$* が使われていた理由の一つは "$@" の仕様が 1986 年の SystemV R4 (SVR4) 用の Bourne シェルから変更になっているという点も考えられます。位置パラメータ "$@" の他の言語のプログラマにとってのあまり直感的ではない動作は「位置パラメータが空の時、"$@" も空と解釈される」という点です。例えば位置パラメータが何もなかった時、下記の 1. の動作が現在のシェルの動作です。

  1. echo "$@" ・・・ echo に渡される引数は 0 個
  2. echo "$@" ・・・ echo に渡される引数は 1 個(空文字が渡される)

Bourne シェル (SVR4) よりも前の Bourne シェルでは 2. の動作でした。Bourne シェルはその歴史の中で破壊的変更(互換性がない変更)が行われているのです。2. の動作を行う古いシェルで引数がない時に 0 個の引数を渡す方法の一つが「ダブルクォートでくくらないこと」でした。ただしダブルクォートでくくらない場合、位置パラメータに空白文字が含まれているとそこで分割されてしまいます。しかし昔は値に空白が入ることなどあり得ないと考えられていたとしたら、$* でも $@ でも同じ動作となるため、ダブルクォートでくくらない $*$@ がどちらも同じものとして当たり前のように使われていたのでしょう。

実は 2. の問題の回避策は「ダブルクォートでくくらない」の他にもう一つあります。今でもまれに見るかもしれませんが ${1+"$@"} のような冗長な書き方です。この書き方を使用すれば、引数が 0 個ではない = 一番目の引数がある場合に "$@" に展開されるため、期待通りに動作します。このような回避策があるにも関わらず、昔の本で $* がよく使われていたというのは、やはり昔は空白が含まれる引数の扱いに無頓着だったということなのでしょう。余談ですが ${1+"$@"} という書き方は Bourne シェル (SVR4) 対策の他に 2009 年頃の一昔前のシェルで set -u 状態で "$@" を参照できないというバグの回避策としても使われていました。詳細は「What does ${1+"$@"} mean」を参照してください。古いコードにはまだこのような書き方が残っていたりしますが、もう 10 年以上前のシェルのための回避策なので、今はこのような書き方をする必要はなくなっています。単に冗長なだけなので ${1+"$@"} という書き方は(どうしても必要でない限り)使うのはやめましょう。不要になったものは廃止してよりシンプルにしていくべきです。

さいごに

「シェルスクリプトは 30 年前から変わらない」とは考えないでください。30 年前は Bourne シェルの時代です。POSIX でシェルが標準化された 1992 年前後よりシェルの仕様は変わっています。より良く変わっていますし、より良く変えなければいけません。

「ファイル名に空白を入れるなんてありえない」時代も、Windows 95 以降にロングファイルネーム(それまでの MS-DOS 自体は 8文字 + 3文字の拡張子でファイル名に空白入れられなかった)がサポートされ、技術者ではない一般の人がわかりやすい長い名前のファイル名を使うようになったので「ファイル名に空白文字は普通に含まれる」時代へと変化しました。UNIX は 1990 年代後半に開発が終了したので従来の UNIX の範囲に大きな変化は無くなってしまいましたが、UNIX 以外の範囲とコンピュータを取り巻く環境は大きく変わっており、つまり空白文字に対応するのは今や常識です。シェルで空白文字を扱うことの基本は変数を参照する時にダブルクォートすることです。

現在の常識に照らし合わせればダブルクォートなしの $*$@ はバッドプラクティスであり使う必要がありません。使う必要がないものを無視してしまえば "$*""$@" の違いは「文字列結合処理」か「全ての引数」かの違いとしてはっきり区別できるので悩む必要がありません。シェルスクリプトの世界もよりよく変わっているので、古く時代遅れになったものを切り捨て、新しい常識で説明の仕方を工夫すればよりシンプルになります。

22
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
28