シェルスクリプトで引数をパースする

 自作のbashのシェルスクリプトで何らかのオプション、すなわちスクリプトの動作を変更するためのフラグを使えるようにしたいとしよう。このような場合、${#} を使って与えられた引数の個数を取得したり、${1:0:1}を使って最初の引数の最初の文字が「-」かどうかを判定したりするなど、直接自分でパースすることもできるが、自分でパースする場合には、与えられたオプションが何であるかとか、引数を取っているかどうかとかを確認するためにif/thencaseなどを使ったコードを自分でさらに書き足す必要があるだろう。

 しかしそうしたところで、例えば、続けてその後に引数を指定する必要のあるオプションに対して必要な引数をユーザが与えなかった場合、あなたの自作のルーチンはどうなるのだろうか? あるいは「-ab」のように、ユーザが2つのオプションをまとめて指定してスクリプトを呼び出した場合は? そのような場合もうまくパースできるのだろうか? スクリプトにオプションが必要ということはよくあることなので、シェルスクリプトではオプションのパースが必要になることがよくある。オプションのパースを行なう、より標準的な方法はないのだろうか?

(本記事は最近出版された「 bash Cookbook 」からの抜粋。)

 オプションのパースを行なう標準的な方法は、bashの組み込みコマンド「getopts」を使うことだ。以下に、getoptsのマニュアルページに載っている例を少しだけ改変したものを示す。

	#!/usr/bin/env bash
	# cookbook filename: getopts_example
	#
	# using getopts
	#
	aflag=
	bflag=
	while getopts 'ab:' OPTION
	do
	  case $OPTION in
	  a)	aflag=1
			;;
	  b)	bflag=1
			bval="$OPTARG"
			;;
	  ?)	printf "Usage: %s: [-a] [-b value] args\n" $(basename $0) >&2
			exit 2
			;;
	  esac
	done
	shift $(($OPTIND - 1))

	if [ "$aflag" ]
	then
	  printf "Option -a specified\n"
	fi
	if [ "$bflag" ]
	then
	  printf 'Option -b "%s" specified\n' "$bval"
	fi
	printf "Remaining arguments are: %s\n" "$*"

 この例では、2つのタイプのオプションがサポートされている。1つめのタイプのオプションは単純で、単独で使用され引数がないものだ。このタイプのオプションは通常、コマンドの動作を変更するフラグとなっている。例としてはlsコマンドの-lオプションなどがある。2つめのタイプのオプションは、引数が必要となるものだ。このタイプのオプションの例には、mysqlコマンドの-uオプション(「mysql -u sysadmin」などのようにしてユーザ名を与える)がある。それではgetoptsを使ってこの2つのタイプのオプションを使えるようにする方法を見ていこう。

 getoptsを使用するためには2つの引数を与える必要がある。

	getopts 'ab:' OPTION

 最初の引数はオプションで使用する文字のリストであり、2つめの引数はシェル変数の名前だ。上記のシェルスクリプトの例では、有効なオプションとして-a-bのみを定義しているので、getoptsの最初の引数には、それらの2文字(とコロン)だけが与えられている。ではコロンはどういう意味なのかというと、(例えば「-u ユーザ名 」や「-f ファイル名 」と同じような形で)-bに引数が必要であるということを示している。なおコロンは、引数を取る各オプション文字のすぐ隣に置く必要がある。したがって例えば引数を取るのが-aオプションだけの場合にはa:bとなる。

 getopts組み込みコマンドは、シェルスクリプトの引数リストをパースした際に見つけた値($1$2など)を2つめの引数で指定された変数に設定する。「-」で始まる引数はオプション引数とみなされ、「-」を除いた文字の部分だけが指定された変数(上記の例では$OPTION)に代入される。その後getoptsは真(0)を返すので、whileループがオプションを処理して、その後、引数がなくなるまで(またはユーザがオプションの終了を明示的に示すために入力した「--」を見つけるまで)getoptsが繰り返し呼び出されてオプションのパースが続けられる。最後にgetoptsが偽(非0)を返してwhileループが終了する。

 ループの中では、$OPTION変数に対してcase文を用いて、処理すべきオプション文字がパースによって見つかった場合に、フラグを設定したりオプションが指定された場合に行なうべき動作を行なったりしている。一方、引数を取るオプションの場合、その引数はシェル変数「$OPTARG」に設定される(なお$OPTARGは既定の名前であり、ここで変数として利用している$OPTIONとは関係ない)。ただし$OPTARG変数の値は、while文でループが繰り返し実行され、getoptsが呼び出される度にリセットされるため、別の変数に代入して保存しておく必要がある。

 例の中のcase文の3つめのパターンは「?」で、任意の一文字にマッチするシェルのパターンだ。getoptsは、定義されたオプション群(ここでは'ab:')以外のオプションを見付けた場合、変数(ここでは$OPTION)の中にリテラルの「?」を代入する。したがってcase文を \?) または ‘?’) として「?」そのものにマッチさせることもできるが、ここでのcase文のデフォルトとしては、任意の一文字にマッチするパターンとして「?」を使っておけばリテラルの「?」も含めた任意の一文字にマッチするので便利だ。

 使い方のメッセージを表示する部分では、マニュアルページに掲載されているスクリプト例に2点変更を加えた。1つめの変更点としては、起動時に使用される可能性のある余分なパス名をすべてなくしたスクリプト名を表示するために$(basename $0)を使用するようにした。2つめの変更点としては、使い方のメッセージを標準エラー出力(>&2)に出力するようにした。未知のオプションや引数不足の場合のgetoptsのエラーメッセージはすべて必ず標準エラー出力に出力されるので、使い方のメッセージもそれに合わせた。

 whileループが終了すると、次に以下のような行が実行される。

	shift $(($OPTIND - 1))

 上記の行ではshift文を使用して、$1$2のようなシェルスクリプトの位置パラメータを、与えられた数だけ前に移動している(つまり前方にあるパラメータを捨てている)。$OPTIND変数は、getoptsがパース時の位置を覚えておくために使用する、引数のインデックスだ。したがってこのshift文を実行することにより、パースの完了後、処理が終わったオプションをすべて捨てていることになる。例として以下のようなコマンドラインを考えてみよう。

	myscript -a -b alt plow harvest reap

 この場合、オプションのパースを終えた時点で$OPTINDは4になっている。($OPTIND-1)によりシフトが3回実行されるので、引数のうちオプション引数だけが捨てられることになり、例えば「echo $*」を実行してみると次のような結果になる。

	plow harvest reap

 したがって残りの(オプションではない)引数を(おそらくforループの中などで)利用しやすい状態になる。上記のスクリプト例では、最終行のprintfで残りの引数をすべて表示した。

Linux.com 原文