zshで簡単にコマンドライン オプションを解析する

シェルスクリプトでコマンドライン引数をオプションとして解析したいときは、getoptsを使うのが一般的なやり方だ。
でも、getoptsはGNUスタイルの長いオプションに対応していないので、--helpとかのオプションを受け付けれないという欠点がある。そんなときでも、zshならzparseoptsというコマンドを使えば長いオプションでも解析できるので紹介する。

## 基本パターン
まずは一番簡単なパターンとして、次の3つのオプションを解析する例を紹介する(mvコマンドのオプションをイメージしている)。どのオプションも引数は取らない。

- -v
- --help
- --version

このオプションをパースするには、zparseoptsというzshの組み込みコマンドを使って、次のように書く。

```zsh:mv1.zsh
#!/bin/zsh

local -A opthash
zparseopts -D -A opthash -- -help -version v

if [[ -n "${opthash[(i)--help]}" ]]; then
  # --helpが指定された場合
  echo "--help option"
fi

if [[ -n "${opthash[(i)--version]}" ]]; then
  # --versionが指定された場合
  echo "--version option"
fi

if [[ -n "${opthash[(i)-v]}" ]]; then
  # -vが指定された場合
  echo "-v option"
fi

# オプションでない引数が$@に残る
echo "normal arguments : $@"
```

zparseoptsの行では、最後の`-help -version v`の部分で、どういう名前のオプションに対応しているのかを指定している。こんなふうに、オプション名の先頭の`-`を取り除いた名前を並べて指定する。

その前の部分で、zparseopts自体のオプションとして`-D`と`-A opthash`を指定している。

`-D`は、シェルスクリプト自体の元の引数のうち、オプションとしてみなした部分は取り除くという意味。デフォルトの動作としては、zparseoptsを呼び出した後でも`$@`(シェルスクリプト自体の引数)は変わらない。でも`-D`を指定すれば、zparseoptsを呼び出した後はオプションとみなした部分が`$@`から取り除かれる。つまり、オプションでない通常の引数だけが`$@`に残るようになる。普通はこの方が便利なので、`-D`は指定しておいたほうが良いと思う。

その次の`-A opthash`というのは、`opthash`という名前の連想配列にオプションとみなした引数を格納する、という意味になる。連想配列の中身は「キー=オプション名」「値=オプション引数(引数を取らない場合は空文字列)」になる。例えば、もし-vと--helpというオプションが指定されたとすると、opthashの値は`{ -v => '', --help => '' }`というふうになる。もちろん、opthashという名前は別に何でも良い。

オプションが指定されたかの判定は、その下のif文でやっている。これは、例えばひとつ目のif文の場合は「opthash連想配列に--helpというキーが含まれているか」ということを意味している。このようにzshで連想配列を扱う方法については、詳しくは次の記事を参照するとよい。

- [zshの連想配列の使い方まとめ - Qiita](http://qiita.com/mollifier/items/ba956b7c07acdabd74a7)

このシェルスクリプトを実際に呼び出したときの例は次の通り。

```
# オプションなし
% ./mv1.zsh from to
normal arguments : from to
```

```
# -vオプションと通常引数
% ./mv1.zsh -v from to
-v option
normal arguments : from to
```

```
# --help, --version, -vオプションと通常引数
% ./mv1.zsh --help --version -v from to
--help option
--version option
-v option
normal arguments : from to
```

うまくいけてる。

## オプションが引数を取る場合
次はオプションが引数を取る場合の例として、さっきのオプションに加えて`-S`オプションが引数を取る場合を紹介する。

- -v
- --help
- --version
- -S SUFFIX (何らかの文字列を引数に取る)

このオプションをパースするには次のように書く。

```zsh
#!/bin/zsh

local -A opthash
zparseopts -D -A opthash -- -help -version v S:

if [[ -n "${opthash[(i)--help]}" ]]; then
  # --helpが指定された場合
  echo "--help option"
fi

if [[ -n "${opthash[(i)--version]}" ]]; then
  # --versionが指定された場合
  echo "--version option"
fi

if [[ -n "${opthash[(i)-v]}" ]]; then
  # -vが指定された場合
  echo "-v option"
fi

if [[ -n "${opthash[(i)-S]}" ]]; then
  # -Sが指定された場合
  echo "-s option : ${opthash[-S]}"
fi

echo "normal arguments : $@"
```

最後の`S:`というところが今回追加した箇所。こんなふうにオプション名の後ろに`:`を付けると、そのオプションは引数を取る、という意味になる。オプション引数の値は連想配列の値として格納されるので、この例の場合は-Sオプションの引数は`${opthash[-S]}`で参照できる。

これを呼び出したときの例は次の通り。

```
# -Sオプションとその引数を指定
% ./mv2.zsh -S .bak from to
-s option : .bak
normal arguments : from to
```

```
# -Sオプションとその引数、--help, --version, -vオプションを指定
% ./mv2.zsh -S .bak --help --version -v from to
--help option
--version option
-v option
-s option : .bak
normal arguments : from to
```

```
# -Sオプションを2回指定
% ./mv2.zsh -S .bak -S .old from to
-s option : .old
normal arguments : from to
```

最後の例のように、引数を取る同じオプションを2回以上指定したときは後に指定したほうで上書きされる。

## 同じ意味の別名のオプションがある場合
最後に、同じ意味を表す別名のオプションがある場合について紹介する。今回のオプションの例は以下。

- -v, --verbose
- -S, --suffix SUFFIX
- --help
- --version

`-v`と`--verbose`は、同じ意味の別名のオプションとする。つまり、このシェルスクリプトでは、`-v`と`--verbose`のどちらのオプションを指定しても同じ動作になるようにする。同様に、`-S`と`--suffix`も同じ意味とする。また、`-S`と`--suffix`はどちらも引数を取る。

これをパースする書き方は以下(\\で複数行に分割しているのには深い意味はない。単に行が長くなったので分割してるだけ)。

```zsh
#!/bin/zsh

local -A opthash
zparseopts -D -M -A opthash -- \
  -help \
  -version \
  v -verbose=v \
  S: -suffix:=S

if [[ -n "${opthash[(i)--help]}" ]]; then
  # --helpが指定された場合
  echo "--help option"
fi

if [[ -n "${opthash[(i)--version]}" ]]; then
  # --versionが指定された場合
  echo "--version option"
fi

if [[ -n "${opthash[(i)-v]}" ]]; then
  # -vまたは--verboseが指定された場合
  echo "-v option"
fi

if [[ -n "${opthash[(i)-S]}" ]]; then
  # -Sまたは--suffixが指定された場合
  echo "-s option : ${opthash[-S]}"
fi

echo "normal arguments : $@"
```

まず、zparseopts自体に`-M`というオプションを追加している。こうすると、<オプションの別名>=<オプション名>という形でオプションの別名が指定できる。これは、<オプションの別名>で指定したオプションの値が<オプション名>と同じ名前で格納される、という意味になる。この例の場合`v -verbose=v`と指定しているので、コマンドライン引数として-vと--verboseのどちらを指定してもopthash連想配列には`{ -v => '' }`という値が格納される。

これを呼び出したときの例は次のとおり。

```
# -vオプションを指定
% ./mv3.zsh -v from to
-v option
normal arguments : from to
```

```
# --verboseオプションを指定
# -vオプションのときと同じif文の中に入っていることに注意
% ./mv3.zsh --verbose from to
-v option
normal arguments : from to
```

```
# -Sオプションを指定
% ./mv3.zsh -S .bak from to
-s option : .bak
normal arguments : from to
```

```
# --suffixオプションを指定
# -Sオプションのときと同じif文の中に入っていることに注意
% ./mv3.zsh --suffix .bak from to
-s option : .bak
normal arguments : from to
```

```
# 全部のオプションを指定
% ./mv3.zsh --help --version -v --verbose -S .bak --suffix .old from to
--help option
--version option
-v option
-s option : .old
normal arguments : from to
```

これを使うと、別名オプションのときでも処理を一箇所にまとめて書けるので便利。

## 注意
連想配列の変数名として`options`というのを使おうとしたんだけど、次のようなエラーが出てしまった。

```
./zoptions.zsh:local:6: options: can't change type of autoloaded parameter
```

どうやらzsh自身が`options`という名前の変数を使っているようで、自分で作ったスクリプトでその名前の変数を使えないみたい。

こんな感じで、zparseoptsを使うとgetoptsよりも簡単に高機能なオプション解析ができるようになるので、試してみてください!

## 参考

- [man zshmodulesのTHE ZSH/ZUTIL MODULE](http://zsh.sourceforge.net/Doc/Release/Zsh-Modules.html#The-zsh_002fzutil-Module)のzparseopts
- [ZSH-LOVERS(1)](http://grml.org/zsh/zsh-lovers.html)
- [zshの連想配列の使い方まとめ - Qiita](http://qiita.com/mollifier/items/ba956b7c07acdabd74a7)