シェルスクリプトの引数とは、./backup.sh data.csv /tmp/out のようにスクリプト名の後ろに並べて渡す値のことで、スクリプトの中では $1・$2…(1つずつ)、$@(全部)、$#(個数)という特殊変数で受け取ります。 -v や -n 3 のようなオプションは、シェルに組み込まれている getopts で解析するのが基本です。

この記事では、特殊変数の一覧から、"$@" と "$*" の違い、shift、個数チェックと usage(使い方の表示)、既定値、getopts のコピペ用の雛形、ロングオプション(--verbose 形式)、関数の引数までを順に説明します。掲載したコードはすべて bash 5.2.21(Ubuntu 24.04)で実行し、出力をそのまま載せています。 sh でも動く範囲は dash 0.5.12 で確かめました。空白入りの引数、不正なオプション、値の付け忘れといった「つまずきやすい入力」を実際に与えた結果も載せているので、手元の挙動と見比べながら読めます。

シェルスクリプト引数の早見表

まずは特殊変数の一覧です。迷ったら「1つずつは "$1"、全部は "$@"、個数は $#」と覚えておけば用が足ります。

変数 意味 ./vars.sh apple "red wine" での値
$0 スクリプト自身の名前(呼び出したときのパス) ./vars.sh
$1 〜 $9 1〜9番目の引数(位置パラメータ) $1 は apple、$2 は red wine
${10} 以降 10番目以降の引数。波かっこが必須 (引数が10個以上あるとき)
$# 引数の個数($0 は数えない) 2
"$@" すべての引数を1つずつ別の単語として展開 apple と red wine の2語
"$*" すべての引数を1つの文字列に連結(区切りは IFS の先頭文字、通常は空白) apple red wine の1語
$? 直前のコマンドの終了コード(0 が成功) 直前の結果による
$$ このシェル(スクリプト)のプロセスID 数値
$! 最後にバックグラウンドで起動したコマンドのプロセスID 数値

やりたいことから探すときは、次の逆引き表を使ってください。

やりたいこと 書き方
1番目の引数を使う "$1"
全部の引数を別のコマンドにそのまま渡す cmd "$@"
全部の引数を順に処理する for a in "$@"; do ...; done
引数の個数を調べる [ "$#" -ge 1 ]
省略されたら既定値を使う out=${2:-./out}
引数がなければエラーにする : "${1:?ファイル名が必要です}"
先頭の引数を捨てて前に詰める shift(n 個なら shift n)
-v -n 3 などのオプションを解析する while getopts ":vn:" opt; do case ... esac; done
オプションの後の引数を取り出す shift $((OPTIND - 1))
最後の引数を使う(bash) "${@: -1}"
--verbose 形式に対応する case + shift の手書き、または util-linux の getopt

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプト引数の基本|$1・$@・$#とgetoptsの使い方」

引数を受け取る基本($0・$1・$#・$?)

特殊変数を全部表示してみる

次のスクリプトを vars.sh という名前で保存し、引数を2つ付けて実行します。2つ目の引数には空白を含めています。

#!/usr/bin/env bash
echo "\$0 = $0"
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$# = $#"
echo "\$@ = $@"
echo "\$* = $*"
set -- a b c d e f g h i j k
echo "\$10  = $10"
echo "\${10} = ${10}"
echo "\${11} = ${11}"
true;  echo "true  の直後の \$? = $?"
false; echo "false の直後の \$? = $?"
ls /nonexistent 2>/dev/null; echo "ls 失敗の直後の \$? = $?"
sleep 0.1 &
bgpid=$!
[ "$bgpid" -gt 0 ] && echo "\$! は数値(バックグラウンドのPID)"
[ "$$" -gt 0 ] && echo "\$\$ は数値(このシェルのPID)"
wait
$ bash vars.sh apple "red wine"
$0 = vars.sh
$1 = apple
$2 = red wine
$# = 2
$@ = apple red wine
$* = apple red wine
$10  = a0
${10} = j
${11} = k
true  の直後の $? = 0
false の直後の $? = 1
ls 失敗の直後の $? = 2
$! は数値(バックグラウンドのPID)
$$ は数値(このシェルのPID)

読み取れるポイントは3つです。

  • "red wine" のようにクォートで囲んで渡すと、空白を含んだまま1つの引数になります。 $# が 2 になっていることで確認できます。
  • $10 は「$1 の後ろに文字 0」と解釈されます。 set -- a b c ... で引数を入れ替えた後、$10 は a0 になりました。10番目以降は ${10} のように波かっこを付けます。
  • $? は直前の1コマンドの結果だけを持ちます。 echo を挟むと echo の結果(0)に上書きされるため、後で使うなら rc=$? のようにすぐ変数へ移します。

途中に出てきた set -- 値1 値2 ... は、位置パラメータを丸ごと入れ替える書き方です。引数の処理を試すときや、後述の getopt の結果を受け取るときに使います。

$0 でスクリプト名を表示する

$0 には「呼び出したときの書き方」がそのまま入ります。./vars.sh と呼べば ./vars.sh、/home/me/bin/vars.sh と呼べばそのフルパスです。usage に表示するときは、$(basename "$0") でファイル名だけにすると読みやすくなります。

"$@" と "$*" とクォートなしの違い

全部の引数を表す書き方は4通りあり、空白を含む引数を渡したときに差が出ます。 次の quote.sh で、受け取った引数の個数と中身を表示して比べます。

#!/usr/bin/env bash
show() { echo "  個数=$#"; for a in "$@"; do echo "  [$a]"; done; }
echo '1) "$@"';  show "$@"
echo '2) "$*"';  show "$*"
echo '3) $@(クォートなし)'; show $@
echo '4) $*(クォートなし)'; show $*
IFS=,
echo '5) IFS=, のときの "$*"'; show "$*"

引数は "my file.txt"(空白1つ)、"b c"(空白2つ)、d の3個です。

$ bash quote.sh "my file.txt" "b  c" d
1) "$@"
  個数=3
  [my file.txt]
  [b  c]
  [d]
2) "$*"
  個数=1
  [my file.txt b  c d]
3) $@(クォートなし)
  個数=5
  [my]
  [file.txt]
  [b]
  [c]
  [d]
4) $*(クォートなし)
  個数=5
  [my]
  [file.txt]
  [b]
  [c]
  [d]
5) IFS=, のときの "$*"
  個数=1
  [my file.txt,b  c,d]
書き方 受け取った個数 何が起きたか 向いている用途
"$@" 3 元の引数がそのまま保たれる 別のコマンドや関数へ渡す、for で回す
"$*" 1 全部が1つの文字列に連結される ログやメッセージに1行で出す
$@(クォートなし) 5 空白で分割し直される 基本的に使わない
$*(クォートなし) 5 同上 基本的に使わない
"$*"(IFS=, のとき) 1 IFS の先頭文字で連結される カンマ区切りなどの文字列を作る

引数をそのまま受け渡したいときは、必ず "$@" とダブルクォートで囲みます。 クォートを外すと、空白での分割に加えて、* などのワイルドカードの展開(パス名展開)も起こります。a.txt・b.txt・c.txt があるフォルダで、文字列としての '*.txt' を1つ渡して確かめます。

#!/usr/bin/env bash
count() { echo "$#"; }
echo "\"\$@\" の個数: $(count "$@")"
echo "\$@   の個数: $(count $@)"
$ bash glob.sh '*.txt'
"$@" の個数: 1
$@   の個数: 3

クォートなしでは *.txt が3つのファイル名に展開されました。「渡された値をそのまま使う」ために、"$1" も "$@" もダブルクォートで囲む、と決めておくと安心です。

shift で引数を前に詰める

shift は、先頭の引数($1)を取り除き、残りを1つずつ前に詰めるコマンドです。$2 が $1 に、$3 が $2 になり、$# は1減ります。shift 2 のように数を指定すると、その個数だけまとめて詰めます。

#!/usr/bin/env bash
echo "開始: \$#=$#  \$1=$1"
shift
echo "shift 後: \$#=$#  \$1=$1"
shift 2
echo "shift 2 後: \$#=$#  \$1=$1"
while [ "$#" -gt 0 ]; do
  echo "処理: $1"
  shift
done
echo "終了: \$#=$#"
shift 2>/dev/null; echo "引数0個で shift の終了コード: $?"
$ bash shift.sh a b c d e
開始: $#=5  $1=a
shift 後: $#=4  $1=b
shift 2 後: $#=2  $1=d
処理: d
処理: e
終了: $#=0
引数0個で shift の終了コード: 1

while [ "$#" -gt 0 ]; do ... shift; done は「引数がなくなるまで先頭から1つずつ処理する」定番の形で、後半のロングオプションの解析でも使います。引数が残っていない状態で shift すると終了コードが 1 になるため、set -e(エラーで止める設定)を使うスクリプトでは、残りの個数を確かめてから shift します。

引数の個数チェックと usage の書き方

引数が足りないまま処理を進めると、空の値でコマンドが動いてしまいます。スクリプトの冒頭で $# を確かめ、合わないときは usage(使い方)を標準エラー出力に出して、終了コード 2 で終えるのが定番です。次の usage.sh は「必須1個+省略できる1個」の例です。

#!/usr/bin/env bash
set -u
usage() {
  cat <<USAGE >&2
使い方: $(basename "$0") <入力ファイル> [出力フォルダ]
  入力ファイル   必須。読み込むファイル
  出力フォルダ   省略可。既定値は ./out
USAGE
  exit 2
}
[ "$#" -ge 1 ] && [ "$#" -le 2 ] || usage
input=$1
outdir=${2:-./out}
echo "input=$input"
echo "outdir=$outdir"
$ bash usage.sh
使い方: usage.sh <入力ファイル> [出力フォルダ]
  入力ファイル   必須。読み込むファイル
  出力フォルダ   省略可。既定値は ./out
(終了コード 2)

$ bash usage.sh data.csv
input=data.csv
outdir=./out
(終了コード 0)

$ bash usage.sh data.csv /tmp/result
input=data.csv
outdir=/tmp/result

$ bash usage.sh a b c
使い方: usage.sh <入力ファイル> [出力フォルダ]
  (以下同じ)
(終了コード 2)

usage を書くときの慣習を表にまとめます。

項目 慣習 理由
必須の引数 <名前> と山かっこで書く 省略できないことが伝わる
省略できる引数 [名前] と角かっこで書く 省略できることが伝わる
繰り返せる引数 <ファイル>... 複数指定できることが伝わる
出力先 エラー時は標準エラー出力(>&2) パイプでつないだ先に説明文が混ざらない
終了コード 使い方の誤りは 2、-h で表示したときは 0 多くのコマンドがこの使い分けをしている

個数チェックの書き方一覧

条件 書き方
ちょうど2個 [ "$#" -eq 2 ] || usage
1個以上 [ "$#" -ge 1 ] || usage
1〜2個 [ "$#" -ge 1 ] && [ "$#" -le 2 ] || usage
0個のとき [ "$#" -eq 0 ] && usage

set -u と未指定の引数

set -u は、未定義の変数を使ったときにエラーで止める設定です。引数を付けずに $1 を参照すると、次のメッセージで止まります。

#!/usr/bin/env bash
set -u
echo "first=$1"
$ bash nounset.sh
nounset.sh: line 3: $1: unbound variable
(終了コード 1)

set -u を使うスクリプトでは、個数チェックを先に行うか、${1:-} のように既定値つきで参照します。 set -u を含む安全な書き出しについては、当サイトの「シェルスクリプトの書き方」の記事で扱っています。

省略されたときの既定値(${1:-値})

引数が省略されたときの値は、変数展開の書き方で1行にできます。

#!/usr/bin/env bash
echo "1) \${1:-guest}  -> ${1:-guest}"
echo "2) \${1-guest}   -> ${1-guest}"
echo "3) \${2:+あり}    -> ${2:+あり}"
: "${3:?第3引数が必要です}"
echo "ここには来ません"

1回目は空文字 "" を1つ渡し、2回目は引数なしで実行しました。

$ bash defaults.sh ""
1) ${1:-guest}  -> guest
2) ${1-guest}   -> 
3) ${2:+あり}    -> 
defaults.sh: line 5: 3: 第3引数が必要です
(終了コード 1)

$ bash defaults.sh
1) ${1:-guest}  -> guest
2) ${1-guest}   -> guest
3) ${2:+あり}    -> 
defaults.sh: line 5: 3: 第3引数が必要です
(終了コード 1)
書き方 未指定のとき 空文字 "" のとき 用途
${1:-値} 値を使う 値を使う 既定値(いちばんよく使う)
${1-値} 値を使う 空文字のまま 空文字を有効な指定として扱いたいとき
${1:+値} 空 空 指定されたときだけ文字を足す
${1:?文} 文を出して終了 文を出して終了 必須の引数を1行で確かめる

コロンの有無で「空文字を未指定と同じに扱うか」が変わります。 通常はコロン付き(:-)を選ぶと、"" を渡された場合も既定値になり、扱いやすくなります。

数値かどうかを確かめる

引数はすべて文字列として届きます。回数や件数として使う前に、case で数字だけかどうかを確かめておくと、後の計算で意図しない動きになりにくくなります。

#!/usr/bin/env bash
case "${1:-}" in
  ''|*[!0-9]*) echo "エラー: 回数は0以上の整数で指定してください: '${1:-}'" >&2; exit 2 ;;
esac
echo "回数=$1"
$ bash num.sh 12
回数=12
$ bash num.sh 1a
エラー: 回数は0以上の整数で指定してください: '1a'
(終了コード 2)

getopts でオプションを解析する(コピペ用の雛形)

getopts は、-v や -n 3 のような1文字オプションを解析するシェルの組み込みコマンドです。bash だけでなく sh でも使え、追加のインストールは要りません。

完成形の雛形

次の opts.sh は、フラグ(-v)、値を取るオプション(-n 回数・-o 出力先)、ヘルプ(-h)、位置引数(ファイル名)に対応した雛形です。そのまま保存して、オプションの文字と変数名を書き換えて使えます。

#!/usr/bin/env bash
set -eu

usage() {
  cat <<USAGE >&2
使い方: $(basename "$0") [-v] [-n 回数] [-o 出力先] [-h] <ファイル>...
  -v         詳しく表示する
  -n 回数    繰り返す回数(既定値: 1)
  -o 出力先  出力ファイル(既定値: out.txt)
  -h         この説明を表示する
USAGE
}

# 既定値
verbose=0
count=1
output=out.txt

# 先頭の : でエラーメッセージを自前で出す
while getopts ":vn:o:h" opt; do
  case "$opt" in
    v) verbose=1 ;;
    n) count=$OPTARG ;;
    o) output=$OPTARG ;;
    h) usage; exit 0 ;;
    :)  echo "エラー: -$OPTARG には値が必要です" >&2; usage; exit 2 ;;
    \?) echo "エラー: -$OPTARG は使えないオプションです" >&2; usage; exit 2 ;;
  esac
done
shift $((OPTIND - 1))   # 解析済みのオプションを取り除く

if [ "$#" -lt 1 ]; then
  echo "エラー: ファイルを1つ以上指定してください" >&2
  usage
  exit 2
fi

echo "verbose=$verbose count=$count output=$output"
echo "位置引数は $# 個"
for f in "$@"; do
  echo "  [$f]"
done
$ bash opts.sh -v -n 3 -o result.txt a.txt "b c.txt"
verbose=1 count=3 output=result.txt
位置引数は 2 個
  [a.txt]
  [b c.txt]

$ bash opts.sh -vn3 a.txt
verbose=1 count=3 output=out.txt
位置引数は 1 個
  [a.txt]

-vn3 のようにオプションをまとめて書く形や、値を空白なしで続ける形にも、そのまま対応しています。

オプション文字列の読み方

getopts ":vn:o:h" opt の最初の引数(オプション文字列)は、次のルールで読みます。

書き方 意味
v -v を受け付ける(値なしのフラグ)
n: -n を受け付け、直後の : は「値を取る」 の意味
先頭の : エラーメッセージを getopts に出させず、自分で処理する(サイレントモード)

getopts が使う変数は3つです。

変数 中身
opt(名前は自由) 見つかったオプションの文字。不正なときは ?、値が足りないときは :(サイレントモードの場合)
OPTARG 値を取るオプションの値。サイレントモードのエラー時は「問題のあったオプションの文字」
OPTIND 次に調べる引数の位置(1始まり)

不正なオプションと値の付け忘れを再現する

雛形に、定義していない -x と、値を付けない -n を渡した結果です。

$ bash opts.sh -x a.txt
エラー: -x は使えないオプションです
使い方: opts.sh [-v] [-n 回数] [-o 出力先] [-h] <ファイル>...
  (以下、usage の表示)
(終了コード 2)

$ bash opts.sh -n
エラー: -n には値が必要です
使い方: opts.sh [-v] [-n 回数] [-o 出力先] [-h] <ファイル>...
  (以下、usage の表示)
(終了コード 2)

先頭の : を付けない場合と付けた場合で、opt と OPTARG がどう変わるかを、次の小さなスクリプトで比べます(silent.sh は "n:"、silent2.sh は ":n:")。

#!/usr/bin/env bash
while getopts "n:" opt; do      # silent2.sh では ":n:"
  echo "opt=[$opt] OPTARG=[${OPTARG-未設定}]"
done
$ bash silent.sh -x
silent.sh: illegal option -- x
opt=[?] OPTARG=[未設定]

$ bash silent.sh -n
silent.sh: option requires an argument -- n
opt=[?] OPTARG=[未設定]

$ bash silent2.sh -x
opt=[?] OPTARG=[x]

$ bash silent2.sh -n
opt=[:] OPTARG=[n]
先頭の : なし 先頭の : あり(サイレントモード)
不正なオプション -x getopts が英語のメッセージを出す。opt は ?、OPTARG は未設定 メッセージなし。opt は ?、OPTARG は x
値の付け忘れ -n getopts が英語のメッセージを出す。opt は ? メッセージなし。opt は :、OPTARG は n
2つを区別できるか できない(どちらも ?) できる(? と :)

先頭に : を付けると、「使えないオプション」と「値が必要」を区別して、自分の言葉でメッセージを出せます。 雛形の case に :) と \?) の2つの分岐があるのはこのためです。? は case の中でワイルドカード(任意の1文字)として働くので、\? とエスケープして書きます。

OPTIND と shift $((OPTIND - 1))

OPTIND の動きを表示して確かめます。

#!/usr/bin/env bash
echo "開始時 OPTIND=$OPTIND"
while getopts ":vn:" opt; do
  echo "opt=$opt OPTARG=${OPTARG-} 次に見る位置 OPTIND=$OPTIND"
done
echo "ループ後 OPTIND=$OPTIND"
shift $((OPTIND - 1))
echo "残り: $*"
$ bash optind.sh -v -n 5 file1 file2
開始時 OPTIND=1
opt=v OPTARG= 次に見る位置 OPTIND=2
opt=n OPTARG=5 次に見る位置 OPTIND=4
ループ後 OPTIND=4
残り: file1 file2

ループを抜けた時点で OPTIND は「最初の位置引数の番号」(ここでは 4)を指しています。shift $((OPTIND - 1)) で解析済みの3個を取り除くと、$1 以降が位置引数だけになります。 この1行は getopts とセットで書く、と覚えておくとよいでしょう。

オプションの後の位置引数と、つまずきやすい3つの入力

雛形に、少し変わった並びの引数を渡した結果です。

$ bash opts.sh -n 2 -- -v a.txt
verbose=0 count=2 output=out.txt
位置引数は 2 個
  [-v]
  [a.txt]

$ bash opts.sh a.txt -v
verbose=0 count=1 output=out.txt
位置引数は 2 個
  [a.txt]
  [-v]

$ bash opts.sh -n -v a.txt
verbose=0 count=-v output=out.txt
位置引数は 1 個
  [a.txt]
入力 結果 理由と対処
-n 2 -- -v a.txt -v が位置引数になる -- は「ここでオプションは終わり」の合図。- で始まるファイル名を渡したいときに使う
a.txt -v -v がオプションとして扱われない getopts は最初の位置引数で解析を終える。オプションは位置引数より前に書く。後ろにも書けるようにしたいときは、後述の case + shift か getopt を使う
-n -v a.txt count に -v が入る 値を取るオプションは次の単語を無条件に値として受け取る。数値チェックなどの検証を足すと気づける

3つ目は、count の受け取り直後に前述の「数値かどうかを確かめる」case を入れておくと、-v が値として渡された時点でエラーにできます。

ロングオプション(--verbose)に対応する2つの方法

getopts が扱えるのは1文字オプションだけです。--verbose や --output=log.txt のようなロングオプションに対応する方法は、主に2つあります。

方法 追加のインストール ロングオプション オプションを位置引数の後ろに書く -vo x のまとめ書き 動く環境
getopts 不要 非対応 非対応 対応 bash・sh
case + shift の手書き 不要 対応 対応(書き方次第) 自分で実装が必要 bash(配列を使わなければ sh も可)
util-linux の getopt Linux では通常入っている 対応 対応 対応 Linux。macOS などの標準の getopt は別の実装

case + shift の手書き

while と shift で引数を先頭から順に見ていく方法です。仕組みが見えやすく、どの環境の bash でも動きます。

#!/usr/bin/env bash
set -eu
verbose=0
output=out.txt
args=()

while [ "$#" -gt 0 ]; do
  case "$1" in
    -v|--verbose) verbose=1; shift ;;
    -o|--output)
      [ "$#" -ge 2 ] || { echo "エラー: $1 には値が必要です" >&2; exit 2; }
      output=$2; shift 2 ;;
    --output=*)   output=${1#*=}; shift ;;
    -h|--help)    echo "使い方: $(basename "$0") [-v|--verbose] [-o|--output 出力先] <ファイル>..."; exit 0 ;;
    --)           shift; args+=("$@"); break ;;
    -*)           echo "エラー: $1 は使えないオプションです" >&2; exit 2 ;;
    *)            args+=("$1"); shift ;;
  esac
done

echo "verbose=$verbose output=$output"
echo "位置引数は ${#args[@]} 個"
for f in "${args[@]}"; do echo "  [$f]"; done
$ bash long.sh --verbose --output result.txt a.txt
verbose=1 output=result.txt
位置引数は 1 個
  [a.txt]

$ bash long.sh a.txt --output=log.txt -v "b c.txt"
verbose=1 output=log.txt
位置引数は 2 個
  [a.txt]
  [b c.txt]

$ bash long.sh --color a.txt
エラー: --color は使えないオプションです
(終了コード 2)

$ bash long.sh --output
エラー: --output には値が必要です
(終了コード 2)

$ bash long.sh -o x -- --verbose
verbose=0 output=x
位置引数は 1 個
  [--verbose]

ポイントは次の4つです。

  • 値を取るオプションは shift 2(オプション名と値の2個を進める)。値がない場合に備えて、先に $# を確かめます。
  • --output=値 の形は ${1#*=} で = より後ろを取り出します。${変数#パターン} は「先頭からパターンに合う部分を取り除く」変数展開です。
  • 位置引数は配列 args にためるので、オプションと位置引数を自由な順で書けます。
  • -*) の分岐を *) より前に置くと、定義していないオプションをエラーにできます。

この書き方は -vo x のようなまとめ書きには対応していません。必要な場合は次の getopt が向いています。

util-linux の getopt

getopt(末尾に s が付かない)は外部コマンドで、Linux の util-linux に含まれる版は、ロングオプション・まとめ書き・並べ替えに対応しています。手元の版は getopt from util-linux 2.39.3 です。

getopt 自体は「引数を整った順番に並べ直して出力する」コマンドです。まず出力だけを見てみます。

$ getopt -o vo:h -l verbose,output:,help -- a.txt --output=log.txt -v "b c.txt"
 --output 'log.txt' -v -- 'a.txt' 'b c.txt'

オプションが前に集められ、-- の後ろに位置引数が並びました。--output=log.txt は --output 'log.txt' の2語に整えられています。この出力を eval set -- で位置パラメータに戻し、case + shift で読むのが定番の形です。

#!/usr/bin/env bash
set -eu
parsed=$(getopt -o vo:h -l verbose,output:,help -n "$(basename "$0")" -- "$@") || exit 2
eval set -- "$parsed"

verbose=0
output=out.txt
while true; do
  case "$1" in
    -v|--verbose) verbose=1; shift ;;
    -o|--output)  output=$2; shift 2 ;;
    -h|--help)    echo "使い方: $(basename "$0") [-v] [-o 出力先] <ファイル>..."; exit 0 ;;
    --)           shift; break ;;
  esac
done

echo "verbose=$verbose output=$output"
echo "位置引数は $# 個"
for f in "$@"; do echo "  [$f]"; done
$ bash getopt.sh a.txt --output=log.txt -v "b c.txt"
verbose=1 output=log.txt
位置引数は 2 個
  [a.txt]
  [b c.txt]

$ bash getopt.sh -vo log.txt a.txt
verbose=1 output=log.txt
位置引数は 1 個
  [a.txt]

$ bash getopt.sh --verb a.txt
verbose=1 output=out.txt
位置引数は 1 個
  [a.txt]

$ bash getopt.sh --color a.txt
getopt.sh: unrecognized option '--color'
(終了コード 2)

$ bash getopt.sh --output
getopt.sh: option '--output' requires an argument
(終了コード 2)
指定 意味
-o vo:h 1文字オプションの定義(書き方は getopts と同じ。: は値を取る)
-l verbose,output:,help ロングオプションの定義(カンマ区切り)
-n 名前 エラーメッセージの先頭に出す名前
-- "$@" ここから後ろが解析する引数

--verb が --verbose として通っているとおり、util-linux の getopt はロングオプションの前方一致による省略を受け付けます。 また、エラーメッセージは getopt が英語で出します。

util-linux 版かどうかは getopt -T の終了コードが 4 かどうかで判定できます(手元でも 4 でした)。macOS などに標準で入っている getopt は別の実装で、ロングオプションに対応していないことが一般に知られています(この記事では未実行です)。配布先の環境が決まっていないスクリプトでは、case + shift の手書きのほうが移植しやすい選択です。

どの方法を選ぶか

状況 向いている方法
1文字オプションだけで足りる getopts(短く書けて sh でも動く)
--verbose 形式がほしい。Linux 以外でも動かしたい case + shift の手書き
ロングオプションも、まとめ書きも、順不同もほしい。Linux で動けばよい util-linux の getopt
オプションが多く、サブコマンドもある Python など、引数解析の標準ライブラリがある言語への切り替えも選択肢

関数の引数($1 は関数の中で別物になる)

シェルの関数も、スクリプトと同じく $1・$@・$# で引数を受け取ります。関数の中の $1 は「その関数に渡された1番目」で、スクリプトの $1 とは別です。

#!/usr/bin/env bash
greet() {
  echo "関数の中: \$#=$#  \$1=$1  \$2=${2:-(なし)}  \$0=$0"
  echo "FUNCNAME=${FUNCNAME[0]}"
}
echo "スクリプト: \$#=$#  \$1=$1"
greet "hello world"
greet "$@"
echo "呼び出し後もスクリプトの \$1=$1"

add() { echo $(( $1 + $2 )); }
sum=$(add 3 4)
echo "sum=$sum"

is_even() { [ $(( $1 % 2 )) -eq 0 ]; }
if is_even 10; then echo "10 は偶数"; fi
is_even 7; echo "is_even 7 の \$? = $?"

parse() {
  local OPTIND opt
  while getopts ":a:" opt; do echo "  関数内 getopts: $opt=$OPTARG"; done
}
parse -a 1
parse -a 2
$ bash func.sh A B
スクリプト: $#=2  $1=A
関数の中: $#=1  $1=hello world  $2=(なし)  $0=func.sh
FUNCNAME=greet
関数の中: $#=2  $1=A  $2=B  $0=func.sh
FUNCNAME=greet
呼び出し後もスクリプトの $1=A
sum=7
10 は偶数
is_even 7 の $? = 1
  関数内 getopts: a=1
  関数内 getopts: a=2
ポイント 説明
$1・$@・$# 関数の中では関数の引数に切り替わり、関数を抜けると元に戻る
$0 関数の中でもスクリプト名のまま。関数名は bash の ${FUNCNAME[0]} で取れる
スクリプトの引数を関数に渡す greet "$@" と明示的に渡す
値を返す echo で出力して $(関数 引数) で受け取る
成功・不成功を返す 終了コード(return 0 / return 1、または最後のコマンドの結果)を if や $? で見る
関数の中で getopts を使う local OPTIND を書く。OPTIND はシェル全体で共有されるため、local にすると2回目以降の呼び出しも先頭から解析できる

sh でも動く範囲(POSIX との違い)

#!/bin/sh で始まるスクリプトは、環境によって dash など bash 以外のシェルで実行されます。この記事の内容の多くは sh でも動きますが、配列と一部の展開は bash 専用です。次の posix.sh を dash 0.5.12 で実行しました。

#!/bin/sh
set -eu
verbose=0
name=guest
while getopts ":vn:" opt; do
  case "$opt" in
    v) verbose=1 ;;
    n) name=$OPTARG ;;
    :)  echo "エラー: -$OPTARG には値が必要です" >&2; exit 2 ;;
    \?) echo "エラー: -$OPTARG は使えないオプションです" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose name=$name 残り=$#"
for a in "$@"; do echo "  [$a]"; done
echo "10番目: ${10:-なし}"
$ dash posix.sh -v -n taro "a b" c
verbose=1 name=taro 残り=2
  [a b]
  [c]
10番目: なし

$ dash posix.sh -z
エラー: -z は使えないオプションです
(終了コード 2)

一方、bash 専用の書き方を dash に渡すと次のようになります。

$ dash arr.sh a b          # 中身は args=("$@")
arr.sh: 2: Syntax error: "(" unexpected

$ dash last.sh a b c d     # 中身は "${@: -1}"
last.sh: 3: Bad substitution
書き方 sh(dash で確認) bash
$1・${10}・$#・"$@"・"$*"・$?・$$・$! 動く 動く
shift・shift n・set -- 動く 動く
${1:-値}・${1:?文}・${1#パターン} 動く 動く
getopts(先頭 :・OPTARG・OPTIND) 動く 動く
case + shift のロングオプション解析(配列なし) 動く 動く
配列 args=("$@")・"${args[@]}" Syntax error 動く
"${@: -1}"・"${@:2:2}"(引数の切り出し) Bad substitution 動く
local OPTIND dash では動く(local は POSIX の規格外) 動く
${FUNCNAME[0]} 使えない 動く

配列を使うなら 1 行目を #!/usr/bin/env bash にする、sh で動かすなら配列を使わない、のどちらかに揃えるのが分かりやすい方針です。bash では、最後の引数と一部の切り出しを次のように書けます。

$ bash last.sh a b c d
最後の引数: d            ← "${!#}"
最後の引数: d            ← "${@: -1}"
2番目から2個: b c        ← "${@:2:2}"
先頭以外: b c d          ← "${*:2}"

つまずきやすい点のチェック表

症状 原因 直し方
空白入りのファイル名が2つに分かれる $1 や $@ をクォートしていない "$1"・"$@" とダブルクォートで囲む
10番目の引数が a0 のようになる $10 が $1 + 0 と解釈されている ${10} と書く
$? がいつも 0 になる 間に別のコマンド(echo など)を挟んだ 直後に rc=$? で受ける
$1: unbound variable で止まる set -u の下で未指定の引数を参照した 個数チェックを先に書く、または ${1:-}
オプションの後のファイル名が取れない shift $((OPTIND - 1)) を書いていない getopts のループの直後に書く
illegal option -- x と英語で出る オプション文字列の先頭に : がない ":vn:" のように先頭に : を付け、\?) と :) の分岐を書く
a.txt -v の -v が効かない getopts は最初の位置引数で解析を終える オプションを先に書く。順不同にしたいなら case + shift か getopt
関数内の getopts が2回目から動かない OPTIND が前回の値のまま 関数の先頭に local OPTIND
Syntax error: "(" unexpected sh(dash)で配列を使った シバンを bash にする、または配列を使わない

よくある質問

Q. $@ と $* はどちらを使えばよいですか? A. 引数を別のコマンドや関数へ渡す、for で1つずつ処理する、という用途では "$@" を使います。空白を含む引数もそのまま保たれます。"$*" は全部を1つの文字列にまとめるので、ログに「受け取った引数: ...」と1行で出すようなときに向いています。どちらもダブルクォートで囲むのが前提です。

Q. getopts と getopt は何が違いますか? A. getopts はシェルの組み込みコマンドで、1文字オプション(-v・-n 3)だけを扱い、sh でも動きます。getopt は外部コマンドで、util-linux に含まれる版はロングオプション(--verbose)やオプションと位置引数の順不同に対応しています。1文字オプションで足りるなら getopts、ロングオプションがほしいなら case + shift の手書きか util-linux の getopt を選びます。

Q. 引数の個数に上限はありますか? A. $1〜$9 の後も ${10}、${11} と続けて参照でき、シェルの書き方としての上限はありません。実際には、OS がコマンドラインの合計の長さに上限を設けています(Linux では getconf ARG_MAX で確認できます)。大量のファイル名を渡すときは、xargs や find -exec で分けて渡す方法があります。

Q. 引数ではなく、パイプで渡された入力を読みたいときは? A. パイプやリダイレクトの入力は、引数($1 など)ではなく標準入力に届きます。while IFS= read -r line; do ...; done で1行ずつ読めます。「引数でファイル名が指定されたらそのファイル、なければ標準入力」という動きにしたいときは、cat -- "${1:--}" のように -(標準入力)を既定値にする書き方があります。

まとめ

  • 引数は "$1"(1つずつ)、"$@"(全部)、$#(個数)で受け取る。 10番目以降は ${10} と波かっこを付ける。
  • "$@" は引数をそのまま保ち、"$*" は1つの文字列に連結する。 クォートなしでは空白の分割とワイルドカードの展開が起こる。
  • 冒頭で $# を確かめ、合わなければ usage を標準エラー出力に出して終了コード 2 で終える。 既定値は ${2:-値}、必須の確認は ${1:?文}。
  • getopts は while getopts ":vn:" opt + case + shift $((OPTIND - 1)) の3点セット。 先頭の : で「使えないオプション(?)」と「値が必要(:)」を区別できる。
  • ロングオプションは case + shift の手書きか、util-linux の getopt。 どこでも動かしたいなら手書き、まとめ書きや順不同まで任せたいなら getopt。
  • 関数の中の $1・$@ は関数の引数。 スクリプトの引数を渡すときは 関数 "$@"、関数内の getopts には local OPTIND。
  • sh で動かすなら配列と "${@: -1}" は使わない。 それ以外の基本(getopts を含む)は dash でも動く。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプト引数の基本|$1・$@・$#とgetoptsの使い方」

株式会社RJCでは、シェルスクリプトの引数処理やオプション解析の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?