bashシェルスクリプトで引数とオプションを解析する
先日、ghコマンドのエクステンションとして、gh graphというものを作りました。
こちらにオプションで表示を出し分ける機能を追加したのですが、シェルスクリプトでの引数解析に手こずったので考え方を残しておきます。
以下の記事がたいへん参考になりました。感謝!
サンプルスクリプト
本記事の説明をざっくりとまとめた基本的なスクリプトを作成しました。以下のGistに載せています。
コピーして実行してみてください。
基本戦略
whileループの中でcaseを使い、スクリプトの引数(optionもargumentも含む)を一つずつ処理します。
ループするごとにshiftで引数を取り除いていき、0個になるまで続けます。
while (( $# > 0 ))
do
case $1 in
-o)
echo "option o"
;;
-*)
echo "invalid option"
exit 1
;;
*)
echo "argument $1"
;;
esac
shift
done
上記の-oの箇所を-o | --optionのようにパイプで分割することで、ショート・ロングオプションの表記ゆれに対応できます。
マッチ範囲の関係上、引数(argument)の処理はオプションより後に書く必要があります。
なお、参考になるものを探していた際、for OPT in "$@"でループする例も見られましたが、オプションが引数を持つ場合など、一度に複数個の引数を扱いたい場合にうまくいきませんでした。
オプションの扱い
先頭に-がついている引数をオプションとします。
即終了するオプション
指定されていたら即終了するオプションの場合、case内部でexitします。
helpやversionなどで使われるかと思います。
while (( $# > 0 ))
do
case $1 in
# ...
-o | --option)
echo "option"
exit 0
;;
# ...
esac
shift
done
引数を持たないオプション
引数を持たず、指定されているかどうかを確認したいオプションの場合です。
指定されていた場合にその旨を変数に代入します。
while (( $# > 0 ))
do
case $1 in
# ...
-o | --option)
OPT=1
;;
# ...
esac
shift
done
複数回の指定を禁止する場合
変数の値が存在しているかを代入前に確認します。
無視する場合もあると思います。
while (( $# > 0 ))
do
case $1 in
# ...
-o | --option)
if [[ -n "$OPT" ]]; then
echo "Duplicated 'option'." 1>&2
exit 1
fi
OPT=1
;;
# ...
esac
shift
done
引数を持つオプション
引数を持つオプションの場合です。httpポートを指定したり、出力先を指定したりする例が考えられます。
以下の3通りの指定方法を想定しています[1]。
-o arg--option arg--option=arg
while (( $# > 0 ))
do
case $1 in
# ...
-o | --option | --option=*)
if [[ "$1" =~ ^--option= ]]; then
OPT=${1#--option=}
elif [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]; then
echo "'option' requires an argument." 1>&2
exit 1
else
OPT="$2"
shift
fi
;;
# ...
esac
shift
done
複数回の指定を禁止する場合、先頭にもう一つif判定を追加します。
if [[ "$1" =~ ^--option= ]]
イコールがついた形で指定された場合です。このとき$1は--option=argという値が入っています。
オプションに渡された引数はargなので、${name#pattern}形式の変数展開を使って先頭から--option=の部分を除去して変数に代入します。当初はsedで除去していたのですが変数展開を使ったほうが簡単ですね。
elif [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]
着目している引数($1)の次の引数($2)が存在しないか、先頭が-(つまりオプション)の場合です。
つまり引数が渡されていないため、エラーとします。
引数が任意の場合、ここでデフォルト値を代入します。
else
$2が存在する場合、それがオプションに渡された引数なので、変数に代入します。
オプションとその引数とで、スクリプトの引数を2つ消費するため、追加でshiftが必要です。
ショートオプションの複数指定
command -a -bをcommand -abのように指定する場合です。
while (( $# > 0 ))
do
case $1 in
# ...
-*)
if [[ "$1" =~ 'a' ]]; then
OPT_A=1
elif [[ "$1" =~ 'b' ]]; then
OPT_B=1
fi
;;
# ...
esac
shift
done
「複数回の指定を禁止」したい場合はその処理を追加してください。
この形式でさらに引数を受け入れる例もあるかもしれませんが、かなり作りづらいと思います。今回は考えていません。
想定していないオプション
先頭に-がついているのにここまでで捕獲されなかった場合、想定されていないオプションです。
エラーを出して終了します。
while (( $# > 0 ))
do
case $1 in
# ...
-*)
echo "Illegal option -- '$(echo $1 | sed 's/^-*//')'." 1>&2
exit 1
;;
# ...
esac
done
エラーを出さず無視する実装もあると思います。
前述の「ショートオプションの複数指定」がある場合、分岐の最後につなげます。
while (( $# > 0 ))
do
case $1 in
# ...
-*)
if [[ "$1" =~ 'a' ]]; then
OPT_A=1
elif [[ "$1" =~ 'b' ]]; then
OPT_B=1
else
echo "Illegal option -- '$(echo $1 | sed 's/^-*//')'." 1>&2
exit 1
fi
;;
# ...
esac
shift
done
その他の特殊な条件
以下の特殊な条件を設定する場合、ループ後にifで判定するのが扱いやすいと思います。
ループ中で判定を挟もうとすると記述が複雑になるためですが、パフォーマンス的にはループ中で確認したほうが勝ることもあるかもしれません。
- 指定必須のオプション
- 特定のオプションと一緒に設定する(または、一緒に設定してはいけない)オプション
引数の扱い
先頭に-がない引数を引数とします(変な日本語だ)。
*で捕獲し、順番に配列に代入していきます。
ARGS=()
while (( $# > 0 ))
do
case $1 in
# ...
*)
ARGS=("${ARGS[@]}" "$1")
;;
esac
shift
done
if [[ "${#ARGS[@]}" -lt 1 ]]; then
echo "Too few arguments." 1>&2
exit 1
elif [[ "${#ARGS[@]}" -gt 2 ]]; then
echo "Too many arguments." 1>&2
exit 1
fi
get_nth () {
local n=$1
shift
eval echo \$${n}
}
ARG1=$(get_nth 1 "${ARGS[@]}")
ARG2=$(get_nth 2 "${ARGS[@]}")
# ...
ループを抜けた後、引数の個数を検証します。上記では1から2個の引数を期待しています。状況によって調整してください。
その後、扱いやすくするため配列を分割しています。
この際、 シェルによって配列のインデックスが異なる ため、それを吸収するためにget_nth関数を作成しています。
配列を使わない場合
シェルスクリプトの引数は高々数個程度…と考えれば、配列を使わない実装もできます。
もし想定する引数が一つであれば、配列を使わず直接代入するのが楽でしょう。
ただ、順番に変数に代入していく処理を作る必要があるため、3つ以上になると厳しい気がします。
以下はARG1が必須、ARG2が任意の例です。
while (( $# > 0 ))
do
case $1 in
# ...
*)
if [[ -n "$ARG1" ]] && [[ -n "$ARG2" ]]; then
echo "Too many arguments." 1>&2
exit 1
elif [[ -n "$ARG1" ]]; then
ARG2="$1"
else
ARG1="$1"
fi
;;
esac
shift
done
if [[ -z "$ARG1" ]]; then
echo "'arg1' is required." 1>&2
exit 1
fi
if [[ -z "$ARG2" ]]; then
# default value
ARG2=arg
fi
おわりに
個人的に、シェルスクリプトは(シェルスクリプトで実行できる範囲のことなら)作りやすい・使いやすいのですが、引数の扱いをちゃんとわかっていなかったのでまとめておきました。
この記事でできないような引数処理は作りたくないかな…。
言語ごとに得意不得意もありますし、複雑なものにしすぎない設計が大切という話ですかね。
-
ショートオプションだとイコールを使わないイメージなのですが、どうなのでしょう ↩︎
Discussion
とても参考になる記事だと思います。シェルスクリプトを書く際にたびたび参考にさせてもらっています。
一点、確認させてください。
記事内のスクリプトで、
1&>2とありますが、これは1>&2の誤植でしょうか?読んでいただけて嬉しいです 🥰
ご指摘の通りtypoでしたので修正しました 😅 ありがとうございます!