はじめに
シェルスクリプト用のオプション解析コードのサンプルは検索するといくつも見つかるのですが、機能が不足していたり雛形とするには長くメンテナンスしづらいものばかりだったので作成しました。以下のような特徴があります。
- POSIXシェル準拠なので bash 以外でも動作可能(dash, zsh, ksh, mksh, yash 等で動作確認)
- ロングオプションにも対応(
getopt
,getopts
未使用) - オプションとオプション以外の引数の順番を混在させることが可能
- 単一の
-
をオプションではなく引数として扱う - オプション解析を打ち切る
--
に対応 - 結合されたショートオプションに対応(例
-abc
- 省略可能なオプション引数に対応(例
-ovalue
,--option=value
) - フラグを反転させるオプションに対応(例
+o
,--no-option
) - 必要な機能のみ使えるように段階的に実装
- コードが短くメンテナンスしやすい
- shellcheck によるチェックと shellspec を使ったテストコードあり
ちなみに独自実装しているのは getopt
がロングオプション対応してるのが GNU 版だけで macOS では動かないからです。getopt
(と getopts
)を使用せずに、十分に置き換え可能な機能を実装しています。また本質的なコードよりもオプション解析コードの方が長いなんて事にならないよう短く仕上げています。そのため少し変わったコーディングスタイルになってますので気に入らない人は好きに書き換えてください。雛形(サンプルコード)として作成してるので自由に変更して使ってもらって構いません。
実際に動作するサンプルコードは こちら です。参考ですが getopt
, getopts
の詳細な解説は「シェルスクリプト オプション解析 徹底解説 (getopt / getopts)」で記事にしています。
※ これまで自分が書いていたコードをベースに機能追加して新たに作成したものなのでバグがあるかもしれません。使用する場合は十分確認をお願いします。
サンプルコード一覧
- オプション引数が必要ない場合
- オプション引数に対応する
- オプションとオプション以外の引数の順番の混在に対応する
- -- でオプション解析処理を打ち切る
- 結合されたショートオプションに対応する
- 結合されたオプション引数に対応する
- 省略可能なオプション引数に対応する
- +, --no- でフラグを反転させる
上から解説を加えつつ機能を実装しています。使用する際は全てを実装する必要はなく必要だと思う所までで十分です。殆どの場合 4. まで実装すれば必要十分だと思います。5. 以降を実装すると GNU 版の getopt
と比べても遜色がない機能になりますがコードの量が増えます。解説の都合上、一度定義した関数は省略しています。例えば unknown
関数は 1. で定義しています。
1. オプション引数が必要ない場合
全ての引数がオプションまたはオプション以外の引数で、オプション引数が必要なければ for
を使って簡潔に書くことができます。
abort() { echo "$*" >&2; exit 1; }
unknown() { abort "unrecognized option '$1'"; }
FLAG_A='' FLAG_B=''
for arg; do
case $arg in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-?*) unknown "$@" ;;
*) set -- "$@" "$arg"
esac
shift
done
このコードはオプションを環境変数に設定し、オプション以外の引数を位置パラメータ $@
に残します。多少分かりづらいと思うので説明します。
- まず
for arg in "$@"
により$@
がループの要素リストとしてメモリに保持されます。 - この状態で
for
ループの中でshift
を行っても、メモリに保持された方の$@
には影響がありません。
言い換えると、引数が5個ある場合、for
ループの中で何度shift
を行っても5回ループします。 - ループの中にある
shift
により、ループする前に$@
にあった引数はすべてなくなります。 - しかし、ループの中でオプション以外の引数を
set -- "$@" "$arg"
で$@
に追加しています。 - つまり、最初あった引数はなくなりますが、新たに追加した引数が
$@
に残ります。
for
を使うのはこのパターンだけです。なぜなら例えば --flag1 --file data.txt --flag2
という引数の場合、data.txt
の部分はオプション引数なので位置パラメータに残したくありません。ですが for
ループは引数を一つずつ繰り返すため data.txt
を飛ばすことができません。また -abc
や --option=value
のような一つの引数が複数の意味を持つ場合の対応も難しくなります。while
を使用すればそのような処理をシンプルに書くことができます。
2. オプション引数に対応する
オプション引数に対応させます。
required() { [ $# -gt 1 ] || abort "option '$1' requires an argument"; }
FLAG_A='' FLAG_B='' ARG_I='' ARG_J=''
while [ $# -gt 0 ]; do
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-?*) unknown "$@" ;;
*) break
esac
shift
done
1. との差分(関数と変数定義以外)
-for arg; do
- case $arg in
+while [ $# -gt 0 ]; do
+ case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
+ -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
+ -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-?*) unknown "$@" ;;
- *) set -- "$@" "$arg"
+ *) break
esac
shift
done
3. オプションとオプション以外の引数の順番の混在に対応する
オプションとオプション以外の引数の順番を混在できるようにします。
param() { eval "$1=\$$1\ \\\"\"\\\${$2}\"\\\""; }
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
2. との差分(関数と変数定義以外)
+parse_options() {
+ OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
case $1 in
-a | --flag-a) FLAG_A=1 ;;
@@ -12,10 +14,14 @@
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-?*) unknown "$@" ;;
- *) break
+ *) param PARAMS $((OPTIND - $#))
esac
shift
done
+}
+
+parse_options "$@"
+eval "set -- $PARAMS"
新しく増えたのは param
関数です。オプション以外の引数を取り出すための PARAMS
変数を組み立てています。変数名を変更できるようにするため eval
を使った読みづらいコードとなっていますが、最終的に PARAMS="$PARAMS \"\${$((OPTIND - $#))}\""
というコードを実行します。例えばコマンドライン引数が -a foo -b bar
の場合 PARAMS
変数には "${2}" "${4}"
という文字列が入ります。そしてこの文字列を eval "set -- $PARAMS"
で位置パラメータ $@
に取り出しています。bash 等であれば配列に引数を入れていくだけで良いのですが、POSIX 準拠の範囲では配列が使えないのでこのような方法を使っています。
また OPTIND
変数を本来とは違う用途で使用しているので注意してください。本来は getopts
コマンドによって使用されるシェル変数で、これを違う目的に使い回すのは一般に良くないことですが、シェルスクリプトには(POSIX準拠の範囲では)グローバル変数しかなく衝突を避けるためにこのようにしています。オプション解析を自前で行うので OPTIND
変数が本来の用途で使われることはまずないので無害でしょう。気になる方は別の名前に変更してください。
4. -- でオプション解析処理を打ち切る
引数の途中で --
が出てきたときに、そこでオプション解析処理を打ち切るための対応です。
params() { [ "$2" -ge "$3" ] || params_ "$@"; }
params_() { param "$1" "$2"; params "$1" $(($2 + 1)) "$3"; }
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
3. との差分(関数と変数定義以外)
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+ --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
新しく増えたのは params
関数です。--
が現れたらそれ以降の引数をすべて PARAMS
変数に追加しているだけです。
5. 結合されたショートオプションに対応する
ls -al
のようにショートオプションをつなげたものへの対応です。
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
case $1 in
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
esac
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
4. との差分(関数と変数定義以外)
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ]; do
+ case $1 in
+ -[!-]?*) OPTARG=$1; shift
+ set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+ esac
+
case $1 in
-a | --flag-a) FLAG_A=1 ;;
-b | --flag-b) FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
例えば -abcd
のようになってる場合-a -bcd
と最初の一文字と残りに分解しています。この処理をループのたびに行うことで、その他のコードを修正すること無く結合されたショートオプションに対応しています。
OPTIND
変数と同様に OPTARG
変数も本来の目的とは違った用途で使用しているので注意してください。OPTARG
は結合されたショートオプションを分解するためのワーク変数として使用しています。
6. 結合されたオプション引数に対応する
-i value
を -ivalue
、--arg-i value
を --arg-i=value
という指定の仕方もできるようにするための対応です。
noarg() { [ ! "$OPTARG" ] || abort "option '$1' doesn't allow an argument"; }
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ] && OPTARG=; do
case $1 in
--?*=*) OPTARG=$1; shift
set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
-[ij]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
esac
case $1 in
-a | --flag-a) noarg "$@"; FLAG_A=1 ;;
-b | --flag-b) noarg "$@"; FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
5. との差分(関数と変数定義以外)
parse_options() {
OPTIND=$(($# + 1))
- while [ $# -gt 0 ]; do
+ while [ $# -gt 0 ] && OPTARG=; do
case $1 in
+ --?*=*) OPTARG=$1; shift
+ set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+ -[ij]?*) OPTARG=$1; shift
+ set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
esac
case $1 in
- -a | --flag-a) FLAG_A=1 ;;
- -b | --flag-b) FLAG_B=1 ;;
+ -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
+ -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
「結合されたオプション引数」は 5. の「結合されたショートオプション」と区別する必要があるので、どれが「結合されたオプション引数」であるかを判定するコードが必要になります。上記コードの -[ij]?*)
が該当のコードです。(case
の部分と二重管理に定義する必要があるので少し嫌ですね。)
また OPTARG
は「結合されたオプション引数だったか?」を判定するフラグ変数としても再利用しています。
7. 省略可能なオプション引数に対応する
省略可能なオプション引数とは、「-i
または -ivalue
」「--arg-i
または --arg-i=value
」という指定ができるオプションです。-i value
や --arg-i value
という指定の仕方はできないので注意してください。(GNU 版の getopt
も同じ仕様です。)
optional() { [ "$OPTARG" ] && OPTARG=$2; }
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ] && OPTARG=; do
case $1 in
--?*=*) OPTARG=$1; shift
set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
-[ijop]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
esac
case $1 in
-a | --flag-a) noarg "$@"; FLAG_A=1 ;;
-b | --flag-b) noarg "$@"; FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
6. との差分(関数と変数定義以外)
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ] && OPTARG=; do
case $1 in
--?*=*) OPTARG=$1; shift
set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
- -[ij]?*) OPTARG=$1; shift
+ -[ijop]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
esac
case $1 in
-a | --flag-a) noarg "$@"; FLAG_A=1 ;;
-b | --flag-b) noarg "$@"; FLAG_B=1 ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+ -o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
+ -p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
OPTARG
はさらに省略可能なオプション引数の値を入れた変数としても使用しています。
8. +, --no- でフラグを反転させる
GNU 版の getopt
でも対応してない機能ですが、比較的よく使われている機能なので対応しています。
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ] && OPTARG=; do
case $1 in
--?*=*) OPTARG=$1; shift
set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
--no-*) unset OPTARG ;;
-[ijop]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+??*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
+*) unset OPTARG ;;
esac
case $1 in
[-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
[-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
[-+]?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
7. との差分(関数と変数定義以外)
parse_options() {
OPTIND=$(($# + 1))
while [ $# -gt 0 ] && OPTARG=; do
case $1 in
--?*=*) OPTARG=$1; shift
set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+ --no-*) unset OPTARG ;;
-[ijop]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
-[!-]?*) OPTARG=$1; shift
set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+ +??*) OPTARG=$1; shift
+ set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
+ +*) unset OPTARG ;;
esac
case $1 in
- -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
- -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
+ [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
+ [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
-i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
-j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
-o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
- -?*) unknown "$@" ;;
+ [-+]?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
FLAG_A
変数は、フラグが立っているときは 1
立っていないときは空文字になります。0
でないのは [ "$FLAG_A" ]
という短い書き方でフラグを判定することができるようにするためです。
OPTARG
をさらに反転させたフラグであるかを判断するために使用しています。
対応してない機能について
- 単一の
-
で始まるロングオプション (例-exec
)- GNU 版の
getopt
で使用可能だが、find
コマンドと Java や PowerShell 関連でしか見たことがなく、シェルスクリプトで実装するものとしては需要は少ないと思われるため
- GNU 版の
-
+
で始まる結合されたオプション引数 (例+ovalue
)-
zsh
,ksh
,mksh
で使用可能だが、使われてる例を知らないため
-
- オプション名を短縮名で指定できるようにする
- コードが長くなるし、個人的にあまり好きな機能ではないので
さいごに
このコードは必要な部分をコピーして手直して組み込むことを想定したものですが、ライブラリの形にすると更に使いやすくなりそうです。気が向いたら作るかもしれません。