Linux シェルスクリプト: コマンドライン引数を取得する ($1, $@, $*)

コマンドライン引数の基本 ($1 ~ $9)

bash シェルスクリプト実行時にコマンドラインで渡した引数を参照するには、次のようなパラメーター参照用の特殊変数を使用します。

  • $1 … 第 1 パラメーター
  • $2 … 第 2 パラメーター
  • $3 … 第 3 パラメーター

パラメーターの参照には、$1$9 が使用できます。 使用することは余りないと思いますが、10 番目以降のパラメータを参照したいときは、${10} のように数値を {} で囲めば参照できます。

sample.sh
#!/bin/bash

echo '1 番目: ' "$1"
echo '2 番目: ' "$2"
echo '3 番目: ' "$3"
実行結果
$ ./sample.sh AAA BBB "CCC  DDD"
1 番目: AAA
2 番目: BBB
3 番目: CCC  DDD

コマンドライン引数を指定しなかった場合は、対応する変数は空になります。

$ ./sample.sh AAA
1 番目: AAA
2 番目:
3 番目:
☝️ 変数はダブルクォートで囲む echo の引数として変数の値を渡すときは、"$1" のようにダブルクォートで囲む癖をつけましょう。 $1 のようにそのまま記述してしまうと、変数の値として連続するスペースが含まれていたときに 1 つのスペースにまとめられてしまいます。
☝️ 引数とパラメーターの違い 正確には、引数 (arguments) という用語は、スクリプトや関数を呼び出す側が渡す値のことを示し、パラメーター (parameters) という用語は、呼び出される側のスクリプトや関数が、その値を参照するときに使う変数のことを示します。 ただ、どちらのケースでもパラメーターという用語を使っているドキュメントもよく見かけるので、あまり気にしないのがよさそうです。

コマンドライン引数にデフォルト値を指定する (${1:-XXX})

コマンドライン引数が指定されなかったときに使用する値として、デフォルト値を指定しておくこともできます。 デフォルト値は、パラメーター参照時に ${1:-デフォルト値} という形で指定します。 下記の例では、1 番目、2 番目、3 番目のコマンドラインパラメーターのデフォルト値を、それぞれ AAABBBCCC に設定しています。

sample.sh
#!/bin/bash

echo '1 番目: ' "${1:-AAA}"
echo '2 番目: ' "${2:-BBB}"
echo '3 番目: ' "${3:-CCC}"
実行結果(1 番目の引数のみ指定した場合)
$ ./sample.sh 100
1 番目: 100
2 番目: BBB
3 番目: CCC

指定されたコマンドライン引数の数を取得する ($#)

シェルスクリプト実行時に渡されたパラメータの数は、$# という特殊変数で取得することができます。

sample.sh
#!/bin/bash

echo 引数の数: $#
実行結果
$ ./sample.sh
引数の数: 0

$ ./sample.sh AAA
引数の数: 1

$ ./sample.sh AAA "BBB CCC DDD"
引数の数: 2

コマンドライン引数をループで順番に処理する ($@, $*)

for ループを使う方法

シェルスクリプト実行時に渡されたコマンドライン引数は、特殊変数 $@ を使って参照することができます。 以下の例では、for in ループを使って、$@ の要素を 1 つずつ取り出して処理しています(おまけでカウンター変数 $count をインクリメントしながらループしてます)。

sample.sh
#!/bin/bash

count=1
for arg in "$@"; do
  echo "$count: $arg"
  let count=$count+1
done
実行結果
$ ./sample.sh AAA BBB "CCC DDD"
1: AAA
2: BBB
3: CCC DDD

実は、特殊変数の指定部分 in "$@" は省略して記述することができます。

sample.sh
#!/bin/bash

count=1
for arg; do
  echo "$count: $arg"
  let count=$count+1
done

明示的に in "$@" を記述する場合は、$@ の部分をダブルクォートで囲むことを忘れないようにしてください(詳しくは後述)。

while ループを使う方法

shift コマンドを実行することで、$1$9 に格納されたパラメータを 1 つずつ前にシフトすることができます。 shift コマンドを実行するたびに $1 に格納されていたパラメータは破棄され、パラメータ数を表す $# の値が 1 つずつ減っていきます。

下記の例では、パラメータ数 ($#) が 1 以上の間、処理を続ける while ループを定義しています。 $1 はコマンドラインパラメータの最初の要素を参照する変数ですが、直後の shift によってパラメータを 1 つずつシフトしているので、結果としてすべてのパラメータを順番に参照することができます。

sample.sh
#!/bin/bash

count=1
while [ "$#" -ge "1" ]; do
  echo "$count: $1"
  shift
  let count=$count+1
done
実行結果
$ ./sample.sh AAA BBB CCC
1: AAA
2: BBB
3: CCC

引数全体を 1 つの文字列として取得する ($*)

$@ と似た特殊変数に $* があります。 どちらもパラメータ全体を表す特殊変数ですが、$@ が各パラメータを個別に保持しているのに対し、$* はすべてのパラメータを結合した 1 つの文字列 になっています。 それぞれ、ダブルクォートで囲んで参照した場合と、囲まずに参照した場合の展開方法も含めて理解しておきましょう。 下記のようなサンプルコードと、その振る舞いを対応付けて覚えてしまうのが手っ取り早いです。

sample.sh
#!/bin/bash

echo -e '\n=== "$@" の場合 ==='
for arg in "$@"; do
  echo "$arg"
done

echo -e '\n=== $@ の場合 ==='
for arg in $@; do
  echo "$arg"
done

echo -e '\n=== "$*" の場合 ==='
for arg in "$*"; do
  echo "$arg"
done

echo -e '\n=== $* の場合 ==='
for arg in $*; do
  echo "$arg"
done
実行結果
$ ./sample.sh "100 200" "CCC DDD"

=== "$@" の場合 ===
100 200
CCC DDD

=== $@ の場合 ===
100
200
CCC
DDD

=== "$*" の場合 ===
100 200 CCC DDD

=== $* の場合 ===
100
200
CCC
DDD

1 番目の方法が、おそらく想定通りの振る舞いに近いと思います。 "$@" と指定することで、内部的には下記のようにそれぞれの要素をダブルクォート (") で囲んで指定されたものとみなされるため、正しく 2 つのパラメータとしてハンドルされます。

for arg in "100 200" "AAA BBB"; ...

2 番目の方法のように $@ をダブルクォートで囲まずに渡すと、下記のように各要素が展開して指定されたものとみなされます。 for ループはスペース区切りで各要素が渡されていると判断するため、結果的に 4 つの要素として処理されてしまいます。

for arg in 100 200 AAA BBB; ...

3 番目の方法のように "$*" と指定すると、下記のように全てのパラメータをスペースでつなげ、さらに全体をダブルクォート (") で囲んで指定されたものとして処理されます。 つまり、ループは "100 200 AAA BBB" という文字列 1 回分しか回りません。

for arg in "100 200 AAA BBB"; do

4 番目の方法のように $* と指定した場合は、2 番目の方法と同様に、すべての要素がスペース区切りで渡されたとみなされます(4 つの要素として処理されます)。

for arg in 100 200 AAA BBB; ...

(コラム)$* が使用する IFS 変数について

前述のとおり、全てのパラメータは $* あるいは $@ で参照できます。 $* は環境変数 IFS に設定された最初のセパレータで区切られた単一の文字列として評価され(デフォルトはスペース)、$@ はスペースで区切られた複数の文字列として評価されます。

例えば、次のように実行した場合、

$ ./sample.sh aaa bbb ccc ddd

それぞれの値は以下のようになります。

  • $*"aaa bbb ccc ddd"
  • $@"aaa" "bbb" "ccc" "ddd"
$ ./sample.sh "aaa bbb" "ccc ddd"

とした場合は、

  • $*"aaa bbb ccc ddd"
  • $@"aaa bbb" "ccc ddd"

となります。 $* はこのような性質を持つため、2 つ目の例のように、スペースを含むパラメータが渡されたときに for ループでうまく扱うことができません。

sample.sh(おそらく間違った実装例)
#!/bin/bash

for x in $*; do
  echo "$x"
done
実行結果(4つのパラメータとして扱われてしまう)
$ ./sample.sh "aaa bbb" "ccc ddd"
aaa
bbb
ccc
ddd

実は、IFS 変数の値は変更できる ため、次のようにして改行 1 つ分に変更してやることで、for ループがうまく回るようになります。

sample.sh(推奨はしないがうまく動作する実装例)
#!/bin/bash

IFS='
'

for x in $*; do
  echo "$x"
done

unset IFS  # デフォルトに戻す(スペースで結合)
実行結果(ちゃんと 2 つのパラメータとして扱われる)
$ ./sample.sh "aaa bbb" "ccc ddd"
aaa bbb
ccc ddd

うまく動作するとはいえ、これはとてもトリッキーな方法なので、パラメータをループ処理するときは素直に "$@" を使って次のように書きましょう。

for arg in "$@"; do
  echo "$arg"
done

関連記事