😀
Bashスクリプトの引数解析をgetoptsで行う際の問題と解決策
何?
Bashスクリプトで実行時引数の解析を行う時に用いるBash組み込みのパーサコマンドgetoptsは、「オプション引数の前に位置引数がある」ような場合、うまく処理できない。この場合エラーを出すようにする方法を考えた。
なお、競合するgetoptコマンド単体を用いるとこの問題は解決する。どうしてもgetoptsがいい場合には今回の方法を用いるといい。
具体例
optstringがd:で、同時に位置引数を受け取りたい。
hoge.sh
#!/bin/bash
DIR=''
while getopts d: OPT
do
case "$OPT" in
d) DIR=$OPTARG ;;
*) exit 1
esac
done
echo "DIR=$DIR"
echo "before: $@($#)"
shift $((OPTIND-1))
echo "after: $@($#)"
のようなパーサに対して、実行時引数の順序を「オプション引数の後に位置引数」とすると、
Terminal
# オプション引数の後に位置引数
$ ./hoge.sh -d ./test hello
DIR=./test
before: -d ./test hello(3)
after: hello(1)
となり、オプション引数は処理され、shiftの後に、位置引数が位置パラメータ$@に残っていることがわかる。
しかし、「オプション引数の前に位置引数」とすると、
Terminal
# オプション引数の前に位置引数
$ ./hoge.sh hello -d ./test
DIR=
before: hello -d ./test(3)
after: hello -d ./test(3)
となり、オプション引数は処理されていないばかりか、shiftの後であっても、すべての引数が残っていることがわかる。
getoptsを素で使う以上は、後者のような場合エラーを出してexit 1したい。
解決策
競合のgetoptコマンドを用いて以下のようにする。
<details>
<summary>diff</summary>
<div>
hoge.sh
#!/bin/bash
DIR=''
while getopts d: OPT
do
case "$OPT" in
d) DIR=$OPTARG ;;
*) exit 1
esac
done
+ args=($(getopt :d "$@")) argnum="$#"
- echo "DIR=$DIR"
- echo "before: $@($#)"
shift $((OPTIND-1))
+ if [ "${args[0]}" != "--" ] && [ "$argnum" -eq "$#" ]
+ then
+ echo 'cmd is placed before the options?' >&2
+ exit 1
+ fi
+ echo "DIR=$DIR"
+ echo "before: $args($argnum)"
echo "after: $@($#)"
</div>
</details>
hoge.sh
#!/bin/bash
DIR=''
while getopts d: OPT
do
case "$OPT" in
d) DIR=$OPTARG ;;
*) exit 1
esac
done
args=($(getopt :d "$@")) argnum="$#"
shift $((OPTIND-1))
if [ "${args[0]}" != "--" ] && [ "$argnum" -eq "$#" ]
then
echo 'cmd is placed before the options?' >&2
exit 1
fi
echo "DIR=$DIR"
echo "before: $args($argnum)"
echo "after: $@($#)"
こうすると
Terminal
$ ./hoge.sh -d ./test hello
DIR=./test
before: -d(3)
after: hello(1)
Terminal
$ ./hoge.sh hello -d ./test
cmd is placed before the options?
ハンドル出来た。
参考
- @b4b4r07さんのbash によるオプション解析の
getoptsに関する記述
デメリット
- コマンドの引数のあとでオプションが使用できない
bash スクリプトでオプション解析をする際には、bash 組み込みコマンドの
getoptsを使っていれば大抵の場合は問題ありません。しかし、getoptsだと、コマンドラインの後半でオプションを指定できないことに気がつきました。 具体的には、Usage: command.bash [-a] [-d dir] item1 item2 ...のような構文の場合に、
-d dirをitemの後ろに指定しても、期待した処理がされません。
Discussion