はじめに
2020-08-12追記 この記事の内容を元に「getoptions」というライブラリを作りました。→「簡単に使えるエレガントなオプション解析ライブラリ(シェルスクリプト用)」
シェルスクリプト用のオプション解析コードのサンプルは検索するといくつも見つかるのですが、機能が不足していたり雛形とするには長くメンテナンスしづらいものばかりだったので作成しました。以下のような特徴があります。
- 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
FLAG_A
変数は、フラグが立っているときは 1
立っていないときは空文字になります。0
でないのは [ "$FLAG_A" ]
という短い書き方でフラグを判定することができるようにするためです。例では変数は空文字で初期化していますが、任意の値で初期化するとオプションを省略したときのデフォルト値となります。
このコードはオプションを環境変数に設定し、オプション以外の引数を位置パラメータ $@
に残します。多少分かりづらいと思うので説明します。
- まず
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:-1} ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
--) 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:-1} ;;
+ -p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
OPTARG
はさらに省略可能なオプション引数の値を入れた変数としても使用しています。${OPTARG:-1}
や ${OPTARG:-default}
の 1
や default
は(オプションがそのものではなく)オプションの引数が省略された場合(例 --opt-o
)のデフォルト値です。オプションの引数を空文字にした場合(例 --opt-o=
)もオプションの引数を省略されたとみなされます。つまり--opt-o
と--opt-o=
を区別することはできません。対応するショートオプション(-o
)では区別するのが不可能で、また getopt
と同じ動作であるため、仕様とします。
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:-1} ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
--) 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:-1} ;;
-p | --opt-p ) optional "$@" && shift; OPT_P=${OPTARG:-default} ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
- -?*) unknown "$@" ;;
+ [-+]?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
parse_options "$@"
eval "set -- $PARAMS"
OPTARG
をさらに反転させたフラグであるかを判断するために使用しています。
9. 同じオプションが複数回指定された場合の対応
同じオプションが複数回していされた場合の対応です。今までのコードは同じオプションが指定された場合、最後に指定されたオプションが有効になっていました。これはよくある実装の一つですが、場合によっては指定されたオプションの数だけ処理を行いたい場合もあります。一番簡単なのはオプションが指定されるたびに変数に値を追加していく方法です。例えば、-v
が指定された場合 VERBOSE
変数に v
を、もう一度指定された場合は vv
をと値を追加していきます。0
, 1
, 2
, ...と数値を使用しても良いと思います。オプション引数がある場合は、任意の区切り記号で区切って値を結合していきます。(例 カンマ区切りで foo,bar,baz
)
# 参考 foo,bar,baz のような文字をループで処理する場合
VALUES="foo,bar,baz"
# ループ処理しやすくするために空文字以外の場合に,を最後につける
VALUES=$VALUES${VALUES:+,}
while [ "$VALUES" ]; do
echo "${VALUES%%,*}"
VALUES=${VALUES#*,}
done
# または(位置パラメータ$@に代入する)
orig_ifs=$IFS
IFS=,
set -- $VALUES
IFS=$orig_ifs
上記のように一つの変数に複数の値を詰め込むことができるならば、この方法を使うのが簡単ですが、例えばファイル名の指定など、どんな文字にも対応したい場合もあります。bash など配列が使えるシェルであれば配列に入れていくだけでよいのですが、POSIX 準拠する場合は配列が使用できないので代わりに文字列をエスケープして詰め込んでいます。
FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''
push_end() { [ "${2#*\'}" = "$2" ] && eval "$1=\"\$$1 '\${3:-}\$2'\""; }
push() { push_end "$@" || push "$1" "${2#*\'}" "${2%%\'*}'\"'\"'"; }
# " # 処理的には意味がないコメント。Qiitaのシンタクスハイライトが混乱してるようなので
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; push ARG_I "$1" ;;
-j | --arg-j ) required "$@" && shift; push ARG_J "$1" ;;
-o | --opt-o ) optional "$@" && shift; push OPT_O "${OPTARG:-1}" ;;
-p | --opt-p ) optional "$@" && shift; push OPT_P "${OPTARG:-default}" ;;
--) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
[-+]?*) unknown "$@" ;;
*) param PARAMS $((OPTIND - $#))
esac
shift
done
}
(push
, push_end
関数を定義し、push
関数を使うようにしただけです。差分は省略します。)
このコードを使って、-i foo -i bar -i baz
のように指定すると ARG_I
変数に 'foo' 'bar' 'baz'
という文字列が代入されます。-i "It's a pen"
のようにシングルクォートが含まれている場合は、'It'"'"'s a pen'
というふうにエスケープされます。そしてこの値を使用する時は、eval
を使って位置パラメータ $@
に代入します。
args_i() {
printf '%s\n' "$@"
}
eval args_i "$ARG_I"
# または(現在の$@が上書きされます)
# eval set -- "$ARG_I"
# printf '%s\n' "$@"
対応してない機能について
- 単一の
-
で始まるロングオプション (例-exec
)- GNU 版の
getopt
で使用可能だが、find
コマンドと Java や PowerShell 関連でしか見たことがなく、シェルスクリプトで実装するものとしては需要は少ないと思われるため
- GNU 版の
-
+
で始まる結合されたオプション引数 (例+ovalue
)-
zsh
,ksh
,mksh
で使用可能だが、使われてる例を知らないため
-
- ロングオプション名を省略名で指定できる(例
--longoptions
の代わりに 省略名--long
が使える)- コードが長くなるし、個人的にあまり好きな機能ではないので
補足 (2020-11-03 追記) この記事を元に開発したオプション解析ライブラリ getoptions では、単一の -
で始まるロングオプションと、ロングオプション名の省略に対応しています。
さいごに
このコードは必要な部分をコピーして手直して組み込むことを想定したものですが、ライブラリの形にすると更に使いやすくなりそうです。気が向いたら作るかもしれません → 作りました。(この記事の冒頭参照)