すこし記事が長いため、簡単なアウトラインを書いておきます。要点だけ掴みたい場合は、最終項の「まとめ」を読むのがいいかもしれません。
- コマンドライン引数の一般的な解析手法
- それぞれの特徴 〜 getopt と getopts の違い
- getopts(メリット・デメリット)
- getopt(メリット・デメリット)
- 自前で解析しちゃう(唯一のデメリット)
- まとめ
=============================
コマンドライン引数を処理する一般的な手法として、
- getopts
- getopt
- shift などで自力で解析
といった具合に、上から順に考えつくかと思います。getopt(3)
は UNIX において、コマンドの引数を処理する一般的な C 言語のライブラリ関数です。それを用いて実装されたコマンドが getopt(1)
です。Bourne シェル系だと内部関数になりますが、同系統の getopts
という関数があります。
getopt
だと外部コマンドであるため、色々と面倒なことがあるので、Bourne シェル系だと getopts
を使った方が余計なことがなくて良いとされています。
それぞれの特徴 〜 getopt と getopts の違い
getopts
$ type getopts
getopts is a shell builtin
メリット
とりあえずgetopts
は bash のビルドインコマンドであることです。ゆえに bash スクリプトとの親和性が高いとされます。getopts
は while
ループと case
文を用いることが定石です。getopts
は第一引数に、使用したいオプション文字列を受け付けます。もし、そのオプションが引数を取る場合はコロンを後に付けると、OPTARG
変数にその値が保持されます。疑問符が返ってきたときは、無効なオプションが渡された時です。break
で抜けるか、使い方を表示して exit
が一般的です。そして、最後に処理した引数の数だけ、shift
し終了です。
説明文の羅列だけでは分かりづらいので以下、テンプレートです。
#!/bin/bash
usage_exit() {
echo "Usage: $0 [-a] [-d dir] item ..." 1>&2
exit 1
}
while getopts ad:h OPT
do
case $OPT in
a) FLAG_A=1
;;
d) VALUE_D=$OPTARG
;;
h) usage_exit
;;
\?) usage_exit
;;
esac
done
shift $((OPTIND - 1))
ここでの例では、-a
オプションは引数を取らず、-d
オプションは引数が必要としています。また、-h
オプションまたはその他に関しては、ヘルプ表示を行うusage_exit
関数を実行し終了しています。
while getopts ad:h OPT
にてオプション解析をしていきますが、ここで指名していないオプション(例えば -b など)を指定した場合やオプションに引数が必要なのに無い場合など、
./test.sh: option requires an argument -- d
./test.sh: illegal option -- x
といったようなエラー報告が行われます。ここで while getopts :ad:h OPT
というように先頭にコロンを置くことで、こういったエラー表示は行われなくなりますので、自前でのエラー処理が行えます。
デメリット
- コマンドの引数のあとでオプションが使用できない
bash スクリプトでオプション解析をする際には、bash 組み込みコマンドの getopts
を使っていれば大抵の場合は問題ありません。しかし、getopts
だと、コマンドラインの後半でオプションを指定できないことに気がつきました。 具体的には、
Usage: command.bash [-a] [-d dir] item1 item2 ...
のような構文の場合に、-d dir
を item
の後ろに指定しても、期待した処理がされません。
- ロングオプションが使用できない
--help
や --version
などのようなロングオプションが使用できないことです。一文字オプションでは文字の競合などが起こり、どのような規則で命名されたのか分かりにくく、これが必要なときもあります。
getopt
$ type getopt
getopt is /usr/bin/getopt
メリット
上で挙げた getopts
のデメリットである、引数のあとにオプションを使用できない件と、ロングオプションの件を 条件付きで 満たしています。
一般に 使用される getopt
のテンプレートは以下です。
set -- 'getopt ad: "$@"'
if [ $? != 0 ]; then
echo "Usage: $0 [-a] [-d dir]" 1>&2
exit 1
fi
for OPT in "$@"
do
case $OPT in
-a) A_FLAG=1
shift
;;
-d) B_ARG=$2
shift 2
;;
--) shift
break
;;
esac
done
getopt
の機能を拡張したものが getopts
であるため、一般に使用する場合その優位性は getopts
にあります。しかし、既存のシェルスクリプトでは getopt
によって書かれたものも多く存在するので、すくなくとも読めたほうが良いです。
デメリット
getopt
は 外部コマンドのため、バージョンや環境によって差異があります。それが、BSD 系と GNU 系の違いです。
BSD系 実装 | GNU 実装 | |
---|---|---|
主なOS | Mac OS X | Ubuntu |
ロングオプション | 不可能 | 可能 |
引数のあとにオプション | 不可能 | 可能 |
つまり、getopt
は使用する環境によって大きく処理が異なります。
なら、BSD でも GNU のものを使えばいいじゃないか!ということで GNU 版 getopt
のソースを入手しても、Mac OS X ではコンパイルは出来ません。
$ gcc -o getopt getopt.c
Undefined symbols for architecture x86_64:
"_main", referenced from:
start in crt1.10.6.o
ld: symbol(s) not found for architecture x86_64
collect2: ld returned 1 exit status
exit 1
Ubuntu 上ではコンパイルも実行も可能ですが、当然ながら Mac OS X 上では作動してくれません。
$ ./getopt
-bash: ./getopt: cannot execute binary file
exit 126
$ file getopt
getopt: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped
しかし、完全に Ubuntu でしか使用しないなら getopt
の優位性は逆転します。getopts
のデメリットを解消できるので、以下に使用テンプレを載せておきます。
OPT=`getopt -o ab:c --long long-a, long-b:,long-c -- "$@"`
if [ $? != 0 ] ; then
exit 1
fi
eval set -- "$OPT"
while true
do
case "$1" in
-a | --long-a)
# -a のときの処理
shift
;;
-b | --long-b)
# -b のときの処理
shift 2
;;
-c | --long-c)
# -c のときの処理
shift
;;
--)
shift
break
;;
*)
echo "Internal error!" 1>&2
exit 1
;;
esac
done
GNU 版の optget
は、ロングオプションも使用でき、
$ getopt ab:c -b aaa -a bbb -c
-b aaa -a -c -- bbb
というように、-c
オプションの前にあるコマンドの引数が、順番が入れ替えられて正常に出力される。
結果として、
Ubuntu なら getopts << getopt
Mac OS X なら getopts >> getopt
であると言えます。
重大なバグに関して
getopt には重大な落とし穴があります。それは、スペースや特殊文字が引数に含まれていた場合、正しく処理できないということです。あるシェルスクリプトで getopt を使った場合、空白や特殊文字は絶対に使うな、という注意書きが必要になります。これを何とかしのごうとするのは容易ではありません。
いっそ自前で解析しちゃう
これらの組み込み関数や外部コマンドを使用しない簡単な例として、以下のように書くことが一般的であるといえるでしょう。
for OPT in "$@"
do
case $OPT in
'-a' )
FLAG_A=1
;;
'-b' )
FLAG_B=1
VALUE_B=$2
shift 2
;;
esac
shift
done
if [ "$FLAG_A" ]; then
echo "Option -a specified."
fi
if [ "$FLAG_B" ]; then
echo "Option -b $VALUE_B specified."
fi
このままでは、ロングオプションとコマンドの引数のあとでオプション指定の件が満たされていません。BSD 系の getopt
状態です。これらのデメリットを解消するため、実装していきましょう。
getopt(s) をめぐる差異は非常に厄介なので、shift
しながらゴリゴリ処理してしまうパターン改。
以下、ソース。
PROGNAME=$(basename $0)
VERSION="1.0"
usage() {
echo "Usage: $PROGNAME [OPTIONS] FILE"
echo " This script is ~."
echo
echo "Options:"
echo " -h, --help"
echo " --version"
echo " -a, --long-a ARG"
echo " -b, --long-b [ARG]"
echo " -c, --long-c"
echo
exit 1
}
for OPT in "$@"
do
case "$OPT" in
'-h'|'--help' )
usage
exit 1
;;
'--version' )
echo $VERSION
exit 1
;;
'-a'|'--long-a' )
if [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]; then
echo "$PROGNAME: option requires an argument -- $1" 1>&2
exit 1
fi
ARG_A="$2"
shift 2
;;
'-b'|'--long-b' )
if [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]; then
shift
else
shift 2
fi
;;
'-c'|'--long-c' )
shift 1
;;
'--'|'-' )
shift 1
param+=( "$@" )
break
;;
-*)
echo "$PROGNAME: illegal option -- '$(echo $1 | sed 's/^-*//')'" 1>&2
exit 1
;;
*)
if [[ ! -z "$1" ]] && [[ ! "$1" =~ ^-+ ]]; then
#param=( ${param[@]} "$1" )
param+=( "$1" )
shift 1
fi
;;
esac
done
if [ -z $param ]; then
echo "$PROGNAME: too few arguments" 1>&2
echo "Try '$PROGNAME --help' for more information." 1>&2
exit 1
fi
コマンドの引数($@ にあたるもの)は param
変数に保持されます。ゆえにコマンドが引数の必要を絶対としない場合は、下からの5行はコメントアウトしてください。
唯一のデメリット
GNU 版
getopt
と肩を並べた以上のサンプルコードですが、組み込み関数getopts
・BSD 版getopt
・GNU 版getopt
では問題なかった、-ab
というようなオプションの複数同時指定ができません。-a -b
というようにバラさなければいけないのです。もちろん実装できなくもないですが、オプション解析がスクリプト本体の行数を超えかねないので、簡潔さを優先させ断念しました。
(以下 2014/10/10 追記)
と以前は書いていたのですが単純ですが以下の方法で -ab
、-ba
というようなオプションの併記も可能です。
declare -i argc=0
declare -a argv=()
while (( $# > 0 ))
do
case "$1" in
-*)
if [[ "$1" =~ 'n' ]]; then
nflag='-n'
fi
if [[ "$1" =~ 'l' ]]; then
lflag='-l'
fi
if [[ "$1" =~ 'p' ]]; then
pflag='-p'
fi
shift
;;
*)
((++argc))
argv=("${argv[@]}" "$1")
shift
;;
esac
done
bash の組み込みである [[
を使用して正規表現的に「含まれているか」を調べてオプションを設定しています。
実引数(オプションを除く引数)の数 argc
や実引数 argv
も取得できます。
よって直前の shift2.sh
のスクリプトと組み合わせると、
- ショートオプション/ロングオプション対応
$ command --long arg1 arg2
- オプションの後付け可能
$ command -s arg1 arg2 --long
- オプションの複数併記可能
$ command -ab -dc arg1
- 環境に依存しない
これらの問題を克服して bash でオプション解析が可能になります。
まとめ
-
getopts
は bash の組み込みコマンドである。ロングオプションは使用不可能、コマンドライン行後半でのオプション指定は不可能 - BSD 版
getopt
はgetopts
に劣るといえる - しかし、GNU 版
getopt
はロングオプションが使用でき、コマンドライン行後半でのオプション指定は可能であるが、BSD 環境での使用はできない -
getopt
は外部コマンドのため、以上のような環境での違いもあり、重大なバグも存在するためその使用は推奨されない - 自前での解析は、数々の問題点をすべて解消できる。 しかし唯一、オプションの複数同時指定ができないという問題が発生する(2014/10/10 追記)
- やっぱり自前解析が一番いいとおもう(2014/10/10 現在の結論)