getopts - コマンド "getopt", "getopts" の使い方を把握する

目的

"getopt"は、Cライブラリもコマンド版も、どちらも使い方を覚えにくい。
ここでは、コマンド版 "getopt" と、sh/bash built-in の "getopts" の使い方をまとめる。

結果

  • sh/bash built-inのgetoptsが使える場合は、そちらを使った方が良い。
  • 外部コマンド getopt 使用時は、クォート処理に気をつける。
  • 速度的には、getoptとgetoptsどっちでも大して差はない。
getopts.sh
https://sssvn.jp/svn/spikelet/sh/getopts.sh
getopt-o.sh
https://sssvn.jp/svn/spikelet/sh/getopt-o.sh

以下、詳細。

getopt を使う(クォート考慮なし)

getopt(1)を参考にしつつ、素直に作成。

getopt.sh
https://sssvn.jp/svn/spikelet/sh/getopt.sh
#!/bin/sh
# sample code for getopt command.

OPT=`getopt abc:d: $*`  # calling getopt
if [ $? != 0 ]; then      # option error
  echo >&2 "usage: $0 [-ab] [-c arg1] [-d arg2] ..."
  exit 1
fi

set -- $OPT  # set result of getopt to arguments
echo BEFORE: $*

while [ $# -gt 0 ]; do
  case $1 in
    --) shift; break ;;
    -a) opt_a=true;;
    -b) opt_b=true;;
    -c) opt_c=$2; shift;;
    -d) opt_d=$2; shift;;
  esac
  shift
done

echo AFTER: opt_a=$opt_a, opt_b=$opt_b, opt_c=$opt_c, opt_d=$opt_d, rest=$*

getoptに渡す文字列は、Cライブラリのgetoptと同様に、

  • 後置パラメータ無しならそのオプション文字のみ
  • 後置パラメータ付き(必須)なら、文字+コロン

として、 その出力を "set --" を用いてコマンドライン引数に上書きすれば良い。

実行例1
(一見成功)
% ./getopt.sh -a -d foo bar
BEFORE: -a -d foo -- bar
AFTER: opt_a=true, opt_b=, opt_c=, opt_d=foo, rest=bar
% ./getopt.sh -l
getopt: invalid option -- l
usage: ./getopt.sh [-ab] [-c arg1] [-d arg2] ...

これで一件成功しているようだが、このスクリプトではクォート処理を無視している。
たとえば、以下のようにオプションを渡すと、誤ってパラメータが分割して渡される。

実行例2
(foo と bar が分割されてしまう)
% ./getopt.sh -a -d "foo bar" baz
BEFORE: -a -d foo -- bar baz
AFTER: opt_a=true, opt_b=, opt_c=, opt_d=foo, rest=bar baz quux

getopt に -o オプション付加

getopt(1)の QUOTING に、この解決策のヒントが書いてある。
旧来のgetoptでは、クォートを一切考慮しない。これは、作成した引数を処理するshell側がどうクォートを理解するかわからないので、getoptがそれを気にしてもムダ、という理由からだそう。

これに対する拡張として、shellの書式でquoteした結果を吐き出す機能が、今のgetoptには付いている。
これを使うには、以下のようにすれば良い。

getopt-o.sh
https://sssvn.jp/svn/spikelet/sh/getopt-o.sh
#!/bin/sh
# sample code for getopt command with -o option.
unset GETOPT_COMPATIBLE

OPT=`getopt -o abc:d: -- "$@"`  # calling getopt
if [ $? != 0 ]; then      # option error
  echo >&2 "usage: $0 [-ab] [-c arg1] [-d arg2] ..."
  exit 1
fi

eval set -- "$OPT"  # set result of getopt to arguments
echo BEFORE: "$@"

while [ $# -gt 0 ]; do
  case $1 in
    --) shift; break ;;
    -a) opt_a=true;;
    -b) opt_b=true;;
    -c) opt_c=$2; shift;;
    -d) opt_d=$2; shift;;
  esac
  shift
done

echo AFTER: opt_a=$opt_a, opt_b=$opt_b, opt_c=$opt_c, opt_d=$opt_d, rest="$@"
実行例3
(クォート処理も成功)
% ./getopt-o.sh -a -d "foo bar" baz quux
BEFORE: -a -d foo bar -- baz quux
AFTER: opt_a=true, opt_b=, opt_c=, opt_d=foo bar, rest=baz quux

先程との違いで、主な違いは以下:

  1. GETOPT_COMPATIBLE 変数を unset した
  2. getopt の書式変更 (-o オプション付加、$@ をクォートして渡す)
  3. set -- の書式変更 (変数はクォートして、eval 経由で実行)

(1)と(2)は、getopt にクォートを考慮させる指定。$*でなく$@にしたのは、クォートした時の形式が異なるため("$*"は引数全体をクォート、"$@"は個々の引数ごとにクォート)。

(3)は、getoptの出力がクォートされているので、それをshellが再度解釈するようにした。

この他に、getoptを実行しているshellの指定(-sオプション)もできるのだが、今どきcsh系列のシェルでscript書く人もいないだろうから、そこは無視する。

getopts を使う

shとbashのbuilt-inには、getoptsというものがある。
コマンドのgetoptと比べて、こちらの方が

  • 中間文字列を作らずに、解析とオプション処理を一気に実行するので、わかりやすい
  • クォート処理がデフォルトで入っているので気にする必要がない

という特徴があり、よりCのライブラリに近い間隔で使用できる。
また、エラーメッセージの行頭が "getopts" ではなくscript名になるので、script内で使いやすい。

getopts.sh
https://sssvn.jp/svn/spikelet/sh/getopts.sh
#!/bin/sh
# sample code for getopts built-in.

while getopts "abc:d:" flag; do
  case $flag in
    \?) OPT_ERROR=1; break;;
    a) opt_a=true;;
    b) opt_b=true;;
    c) opt_c="$OPTARG";;
    d) opt_d="$OPTARG";;
  esac
done

shift $(( $OPTIND - 1 ))

if [ $OPT_ERROR ]; then      # option error
  echo >&2 "usage: $0 [-ab] [-c arg1] [-d arg2] ..."
  exit 1
fi

echo AFTER: opt_a=$opt_a, opt_b=$opt_b, opt_c=$opt_c, opt_d=$opt_d, rest="$@"
実行例4
(クォートも正しく処理される)
% ./getopts.sh -a -d "foo bar" baz quux
AFTER: opt_a=true, opt_b=, opt_c=, opt_d=foo bar, rest=baz quux
% ./getopts.sh -l
./getopts.sh: illegal option -- l
usage: ./getopts.sh [-ab] [-c arg1] [-d arg2] ...

ただ、気に入らない点が一つだけ。変数部分を削除するための shift の引数が冗長に見える。
Cのライブラリと仕様を合わせたのだろうが、shell scriptであればループを添字でまわすよりも、値を文字列に入れてまわす方が適している。であれば、shiftをしやすいように、OPTINDは最後に処理した引数の添字にして欲しかった。

処理速度の比較

getops は shell built-in なので、外部コマンドの getopt よりは処理が速いと期待される。
参考のために、処理時間を比較しておく。

掲載したscriptを1000実行した結果(10回平均)の比較:

条件 script 経過秒 getoptsとの比較
getopt(-o無し) getopt.sh 4.90 3.2%増加
getopt(-o有り) getopt-o.sh 5.04 6.1%増加
getopts getopts.sh 4.75 -

オプション解析という一度きりの処理で、数%しか差が出なかった(まあオプション文字列も単純だし、当然か)。特に速度面では getopts はそれほど優位ではない。