😀

Bashスクリプトの引数解析をgetoptsで行う際の問題と解決策

2021/09/28に公開

何?

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?

ハンドル出来た。

参考

デメリット

  • コマンドの引数のあとでオプションが使用できない

bash スクリプトでオプション解析をする際には、bash 組み込みコマンドの getopts を使っていれば大抵の場合は問題ありません。しかし、getopts だと、コマンドラインの後半でオプションを指定できないことに気がつきました。 具体的には、

Usage: command.bash [-a] [-d dir] item1 item2 ... 

のような構文の場合に、-d diritem の後ろに指定しても、期待した処理がされません。

GitHubで編集を提案

Discussion