自作のzshスクリプトでzparseoptsを用いてコマンドラインオプションを解析したい

# 目標
+ その都度に設定ファイルを指定するタイプのスクリプト（`homebrew`で入れた`marp-cli`とか）の実行をより便利にしたい
+ コマンドライン（CL）オプション（OPT）を使いたい（`marp`ならば`--pdf`の有無で出力をhtmlかpdfで切り替えられる）



## 具体的には
その引数を忠実にパースして対応させたコマンドを呼び出せるようにする




# 下準備



## 実行環境の整理


### パーミッションについて
これを読んでね

https://soluna-eureka.hatenablog.com/entry/2021/12/31/233920


### スクリプト置き場

```zsh
~/zscripts/bin
```

にパスを通して諸々のシンボリックリンクを入れる，元のスクリプトは

```zsh
~/zscripts/test
```

のように分類することにした

シンボリックリンクを生成する際は必ずフルパスを指定して生成すること，`ln -s`する時は相対パス扱いで通っちゃうけど実際に走らせる時はシェル側が絶対パスとして使ってくる，つまり

```zsh
pwd
# => ~/zscripts
ln -s test/testZsh1 bin
```

は動いてしまうのだが（これは何も間違ってはいない，この時の`zsh`から見れば`test/testZsh1`は存在するし，`bin/testZsh`はちゃんと生成される），これを実行しようとすると

```zsh
testZsh1
# => zsh「$PATHを読むぞ，~/zscripts/binを見るぞ，testZsh1があったぞ」
# => zsh「えーとtest/Zsh1に行け…？そんな場所ねぇよ！」
# => zsh「zsh: command not found」
```

という結果になる，ただしこの状態でも`$PATH`上にシンボリックリンクは存在するため，`zsh`は可能な限りコマンド補完を提供してくれる，うーんこれはかなり紛らわしいね

現状では`ln`に引数対象の絶対パスの補完機能はない，というよりもシンボリックリンクではなくハードリンクならiモード番号をまんま参照するためこんなことは起きない，シンボリックリンクリンクに特有の現象と考えて諦めて受け入れるしかなさそう


### パスを通す

```zsh
export PATH=~/zscripts/bin:$PATH
```



## 実行ファイル作成
以下の作業はパターン化しよう

```zsh
cd ~/zscripts/test
touch testZshS1
chmod 0755 testZsh1 # <-rwxr-xr-x
ln -s ~/zscripts/test/testZsh1 ~/zscripts/bin/ # <-最後にスラッシュつけないとディレクトリと認識されないので注意
vi testZsh1
```

```zsh:testZsh1
#!/bin/zsh
# this is testZsh1

echo "this is testZsh1"
```

```zsh
% testZsh1
# => this is testZsh1
```



## 予約済み変数（特殊変数）
シェバンに`zsh`で実行するよう指示されたシェルスクリプトは，起動時にユーザが操作しているシェルから`zsh`のプロセスに渡される，つまりは「ログイン者と対話するシェル」と「スクリプトを実行するシェル」が必ず同じである必要がない．しかしそれでも`zsh`で環境を統一することをオススメしたい理由として，`zsh`が対話シェルとして渡す特殊変数と実行シェルとして受け取る特殊変数の扱いを共通化することによる利点がある．読み物が一つの同じドキュメントだけで済むために大きく労力を削減できるし，権限管理などもやりやすい上にコーディングも多少は楽になるだろうとと思われるからだ（最近になって「移行しておいてよかった」と思い始めている，それでもGoogle先生はよく`zsh`を無視して`bash`の記事を出してくるけど）．
シェルスクリプトはシェバンの書きようによっては`sh`や`bash`に実行させることも可能である，それ故にシェルを切り替えた時に迂闊なミスが起こらないよう，シェルごとにスクリプトの管理フォルダと対応するパスを切り分けるべきだと考えている．

そしてログインシェルとスクリプトシェルの間でやり取りされる情報を利用するための特殊変数が`zsh`には用意されている，manページから以下のコマンドで抽出できるので確認しよう

```zsh
# ログインシェルが渡すもの
man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS SET BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'
```

ちなみに元から利用できる環境変数などは以下で抽出できる

```zsh
# ログインシェルが使うもの
man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS USED BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'
```

シェルスクリプトでは変数名の前に`$`をつけて展開できるが，これも`zsh`も例外ではなく（以下で抽出できる）

```zsh
# zshにおける変数展開について
man zshexpn | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETER EXPANSION\n)( {5,}(\S* *)*\S*\n*)*'
```

，多くの場合は既に`$`がついた形式で覚えられがちなのだが，取り敢えず重要そうなものだけをドキュメント通りに並べると

+ `$`
  + シェルスクリプトとしての実行プロセスの`pid`
+ `0`
  + シェルスクリプトとしての呼び出し`command`
  + フルパスが得られる
+ `*`
  + 呼び出し時に与えられた全ての引数
  + 配列扱い
     + `bash`では文字列1個の扱いだったらしいな？
  + 引数がなければ未定義
+ `@`
  + `Same as argv[@] `らしい
  + 引数がなければ未定義
+ `argv`
  + `Same as *`らしい
  + こいつは変数として簡単に処理できる
     + `*`と`@`は処理が面倒だぞ（後述）
  + 引数がなくても`argv`は空の配列として定義される
+ `#`
  + 引数の個数
+ `?`
  + 1つ前の関数の終了状態を示す
  + つまり関数が評価されるたびに値が入れ替えられる
     + シェバンに`-e`があると異常終了で必ず止まるけど
     + `-u`すると未定義変数を参照した時に必ず止まるよ
+ `status`
  + `Same as ?`らしい

ちなみに`zsh`自身のオプションは以下で確認できる，`-eu`に関しては`bash`と同じっぽい

```zsh
man zshoptions | col -b | expand -t 4 | pcre2grep -Moe ' {0}(SINGLE LETTER OPTIONS\n)( {3,}(\S* *)*\S*\n*)*'
```

仕様などは以下を参考に

https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96



## 実験して確認する

```zsh:testZsh2
#!/bin/zsh -u
# this is testZsh2
# -eu するとエラーで爆死しにくくなるらしい，みんなは-eをつけよう

true=1
false=0
# これは単なるおまじない

echo "pid: '${$}'"
echo "command: '${0}'"
echo "length: '${#}'"

cat OMMCvsCCCCM.txt
# 当然だけどそんな下品なファイルはないので異常なまま終了する（status: 1）
echo "status: '${?}'"

if ((${*:+true})); then
    echo "* is defined"
    echo "for \$* : '${*}'"
    for ((i=1;i<=${#*};i++));do
        echo "for \$*[${i}]: '${*[i]}'"
    done
fi

if ((${@:+true})); then
    echo "@ is defined"
    echo "for \$@ : '${@}'"
    for ((i=1;i<=${#@};i++));do
        echo "for \$@[${i}]: '${@[i]}'"
    done
fi

if ((${+argv})); then
    echo "argv is defined"
    echo "length of argv: '${#argv}'"
fi

unset argv
echo "after unset of 'argv', *: '${*}'"
echo "after unset of 'argv', @: '${@}'"
```

引数を入れたり消したりして試してみよう，この時点で半角スペースによる配列化が自動で実行されていることがわかるだろう

```zsh
% testZsh2 -a b -c
# => pid: '[やる度に値が変わる]'
# => command: '[ここにフルパスが出てくる]'
# => length: '3'
# => cat: OMMCvsCCCCM.txt: No such file or directory
# => status: '1'
# => * is defined!
# => for $* : '-a b -c'
# => for $*[1]: '-a'
# => for $*[2]: 'b'
# => for $*[3]: '-c'
# => @ is defined!
# => for $@ : '-a b -c'
# => for $@[1]: '-a'
# => for $@[2]: 'b'
# => for $@[3]: '-c'
# => argv is defined!
# => length of argv: '3'
# => after unset of 'argv', *: ''
# => after unset of 'argv', @: ''
```

### `*`と`@`は定義されているか？
変数定義の確認方法として有名なものに`((${+xxx}))`があるものの，`zsh 5.8`においては`((${+*}))`と`((${+@}))`は`zsh: bad substitution`を吐き出す…変数ではなく演算子として認識されているらしく，これに引数の有無や数は関係ないと思われる
だから`true=1`を用意して`((${*:+true}))`と`((${@:+true}))`にする必要がある，これなら確実に先に`*`と`@`が展開されて`:`で評価して定義されていれば`+true`を返すことができる（`((${xxx:+yyy}))`については先に挙げたリファレンスにも載ってる）

他のやり方などは以下を参考に

https://unhexium.net/zsh/how-to-check-variables-in-zsh/#typeset--p-var-devnull-21




# `zparseopts`について
`zsh`をセットで導入すれば元から使えるやつらしい，他にも候補があるらしいけど`zsh`上の普遍性を考慮してこれにした
扱い上は`zshmodules`の一部なのでmanページは以下で抽出できる

```zsh
zscripts % man zshmodules | col -b | expand -t 4 | pcre2grep -Moe ' {7}(zparseopts.*\n)( {10,}(\S* *)*\S*\n*)*' 
```



## 特徴


### `zparseopts`に`*`を設定しなくても良い
どうやら勝手に`*`を覗いて変更してくるらしい，必要なのは動作の調整と扱うオプションの設定だけである


### 所用のハイフン数に制限なし
1つや2つに限らずいくらでも増やせる，使う機会があるかどうか？さぁ…


### オプション文字数に制限なし
`-x`も`-xx`も通る，`-`は1文字・`--`はそれ以外みたいな風潮はあるけど


### 楽に連想配列を使える
`-A`を入れると`-x y`から勝手に`-x: y`みたいな連想配列を作ってくれる
`-a`なら`-x y`をくっつけて`-xy`を生成して配列に代入してくるだけだけど…


### 1オプションには1設定
`-x y z -a -b -c`とかを入れると`z`以降が無視され`z -a -b -c`が余剰として扱われる，もちろん用意した連想配列は壊れる
複数の設定を1つのオプションに入れたいとなると，流石に`*`に対する処理を手動で実装した方が早そう，今回はやらんけど…



## テンプレート

```zsh
zparseopts -D -E -a optionArray -A optionPair a bc: -ddd:: --e:
```

### `-D`とは？
元の`*`からオプションとみなした部分を全て切り取ってくれる，複数のファイルを取り扱う場合はこれが必要になってくる
前述したようにパースが崩壊すると切り取れなかった部分が全て`*`に残ることになる


### `-E`とは？
オプション宣言の前に変数を置いたり合間に余剰な変数を置いたりしてもパースを続けてくれる，つまりはさっきの注意書きを無視できる，コマンド・ファイル・オプション・内容みたいにやる時には便利かも


### オプションの指定方法について
ハイフンが1つだけ減る（連想配列から取り出すときは減らしちゃダメだよ）

+ `-a`なら`a`
+ `-bc`なら`bc`
+ `--ddd`なら`-ddd`~~激安の殿堂ZOY~~
+ `---e`なら`--e`

また`:`の数にも意味がある，オプション後に引数について

+ 必ずなければ`a`
  + あるとエラー
+ 必ずあれば`a:`
  + ないとエラー
+ 分岐したければ`a::`
  + ないと空白の文字列が連想配列に代入される
  + ただし配列が完全に連結してしまう副作用がある
  + `-x a`が`[-x, a]`となるところを`[-xa]`にしてしまう
     + ループ処理を自前でやりたい場合とか逆に辛いかも？
  + 連想配列では`{-x :a}`で変わらない

さらに`+`・`+:`・`+::`と`:-`・`::-`は同じオプションを何回も呼び出した時に影響がある

+ `+`は倍プッシュを許可する
  + 配列に影響が出る
  + `-x -x`が`[-x]`となるところを`[-x, -x]`にできる
+ `+:`は引数を連結させる
  + 連想配列に影響が出る，配列は倍プッシュされる
  + `-x a -x b`なら`{-x: ab}`と`[-x, a, -x ,b]`になる
+ `+::`は引数を**完全に**連結させる
  + 配列に影響が出る
     + 連想配列は`+:`と変わらない
  + `-x a -x b`なら`{-x: ab}`と`[-xa, -xb]`になる
+ `:-`・`::-`は…ほとんど意味ない
  + まず`-`をつけるだけで`::`と同じ副作用が必ず出現する
     + `+:`と`+::`みたいに有用と思われる違いがない
  + 引数の置き換えは配列でも連想配列でも重複の呼び出しで勝手に発生するので…
     + つまり`:-`も`::-`も使い道がない，無視して良い

## 実験して確認する

```zsh:testZsh3
#!/bin/zsh -eu
# this is testZsh3

echo "all arguments : ${*}"

zparseopts -D -E -a optionArray -A optionPair a+ bc+:: -ddd+: --e:

if [[ -n "${optionPair[(i)-a]}" ]]; then
    echo "-a option enabled"
fi

if [[ -n "${optionPair[(i)-bc]}" ]]; then
    echo "-bc option enabled: '${optionPair[-bc]}'"
fi

if [[ -n "${optionPair[(i)--ddd]}" ]]; then
    echo "--ddd option enabled: '${optionPair[--ddd]}'"
fi

if [[ -n "${optionPair[(i)---e]}" ]]; then
    echo "---e option enabled: '${optionPair[---e]}'"
fi

echo "other arguments : $*"

if ((${+optionArray})); then
    for i in $optionArray; do
        echo "component: '${i}'"
    done
fi

if ((${+optionPair})); then
    for key in ${(@k)optionPair}; do
        echo "flags: ${key} => value: ${optionPair[$key]}"
    done
fi

echo "${optionPair}"

key="---e"
echo "${#optionPair[$key]}"
```

```zsh
% testZsh3 xyz stst -a -a -bc p -bc q --ddd daiou --ddd heika ---e rrr ---e r tsts
all arguments : xyz stst -a -a -bc p -bc q --ddd daiou --ddd heika ---e rrr ---e r tsts
-a option enabled
-bc option enabled: 'pq'
--ddd option enabled: 'daiouheika'
---e option enabled: 'r'
other arguments : xyz stst tsts
component: '-a'
component: '-a'
component: '-bcp'
component: '-bcq'
component: '--ddd'
component: 'daiou'
component: '--ddd'
component: 'heika'
component: '---e'
component: 'r'
flags: -a => value: 
flags: ---e => value: r
flags: -bc => value: pq
flags: --ddd => value: daiouheika
 r pq daiouheika
1
```



# `marp`コマンドを改善

https://github.com/marp-team/marp-cli

https://formulae.brew.sh/formula/marp-cli

詳細については今は省く（また別のところでやるかも）が，CLIから叩ける`marp`はvscode拡張機能版とは異なり設定ファイルを`-c`オプションで指定できる（その設定ファイルも万能ではない）が，しかし`marp`本体に記憶させることができない．そのためシェルスクリプトで事前に定めたパスを必ず指定できるように仕向けようと考えた，あとはvscodeに機能を追加するよりも`zsh`のシェルスクリプトを書く練習から始めたかった．
…とりあえず`-m pdf`と`-m html`と`-m pptx`で使い分けられるようにした．ちなみに`marp`はまだ全く使いこなせていない．

```zsh:marpit
#!/bin/zsh -eu
# this is marpit

template='[任意のパス]'

zparseopts -D -E -a optionArray -A optionPair i: o: m::

input='-i'
output='-o'
mode='-m'
pdf="pdf"
html="html"
pptx="pptx"

if ((${+optionPair[$mode]})); then
	type=${optionPair[$mode]}
	case $type in
	($pdf) marp -c ${template} --pdf ${optionPair[$input]}.md -o ${optionPair[$output]}.pdf;|
	($html) marp -c ${template} ${optionPair[$input]}.md -o ${optionPair[$output]}.html;|
	($pptx) marp -c ${template} --pptx ${optionPair[$input]}.md -o ${optionPair[$output]}.pptx;|
	esac
else
	echo marp --template ${template}  ${optionPair[$inout]} -0 ${optionPair[$output]}
fi
```

```zsh
% marpit -i test -o test -m html     
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  INFO ] test.md => test.html
% marpit -i test -o test -m pdf 
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  WARN ] Insecure local file accessing is enabled for conversion from test.md.
[  INFO ] test.md => test.pdf
% marpit -i test -o test -m pptx
[  INFO ] An EXPERIMENTAL transition support for bespoke template is enabled. It is using the shared element transition API proposal and it is not yet stable. Recommend to use
          with --preview option for trying transitions. Track the latest information at https://github.com/marp-team/marp-cli/issues/382.
[  INFO ] Converting 1 markdown...
[  WARN ] Insecure local file accessing is enabled for conversion from test.md.
[  INFO ] test.md => test.pptx
```

`zsh`の制御構文については以下で抽出できる

```zsh
man zshmisc | col -b | expand -t 4 | pcre2grep -Moe ' {0}(COMPLEX COMMANDS\n)( {7,}(\S* *)*\S*\n*)*'
```




# 感想
少しだけ仲良くなれたんじゃないかなと思います，以上で終わります




# おまけ
`man zshmodules`の`zparseopts`の部分を抜き出した

```
       zparseopts [ -D -E -F -K -M ] [ -a array ] [ -A assoc ] [ - ] spec ...
	      This builtin simplifies the parsing of options in positional
	      parameters, i.e. the set of arguments given by $*.  Each spec
	      describes one option and must be of the form `opt[=array]'.  If an
	      option described by opt is found in the positional parameters it
	      is copied into the array specified with the -a option; if the
	      optional `=array' is given, it is instead copied into that array,
	      which should be declared as a normal array and never as an
	      associative array.

	      Note that it is an error to give any spec without an `=array'
	      unless one of the -a or -A options is used.

	      Unless the -E option is given, parsing stops at the first string
	      that isn't described by one of the specs.  Even with -E, parsing
	      always stops at a positional parameter equal to `-' or `--'. See
	      also -F.

	      The opt description must be one of the following.  Any of the
	      special characters can appear in the option name provided it is
	      preceded by a backslash.

	      name
	      name+  The name is the name of the option without the leading `-'.
		     To specify a GNU-style long option, one of the usual two
		     leading `-' must be included in name; for example, a
		     `--file' option is represented by a name of `-file'.

		     If a `+' appears after name, the option is appended to
		     array each time it is found in the positional parameters;
		     without the `+' only the last occurrence of the option is
		     preserved.

		     If one of these forms is used, the option takes no
		     argument, so parsing stops if the next positional parameter
		     does not also begin with `-' (unless the -E option is
		     used).

	      name:
	      name:-
	      name:: If one or two colons are given, the option takes an
		     argument; with one colon, the argument is mandatory and
		     with two colons it is optional.  The argument is appended
		     to the array after the option itself.

		     An optional argument is put into the same array element as
		     the option name (note that this makes empty strings as
		     arguments indistinguishable).  A mandatory argument is
		     added as a separate element unless the `:-' form is used,
		     in which case the argument is put into the same element.

		     A `+' as described above may appear between the name and
		     the first colon.

	      In all cases, option-arguments must appear either immediately
	      following the option in the same positional parameter or in the
	      next one. Even an optional argument may appear in the next
	      parameter, unless it begins with a `-'.  There is no special
	      handling of `=' as with GNU-style argument parsers; given the spec
	      `-foo:', the positional parameter `--foo=bar' is parsed as `--foo'
	      with an argument of `=bar'.

	      When the names of two options that take no arguments overlap, the
	      longest one wins, so that parsing for the specs `-foo -foobar'
	      (for example) is unambiguous. However, due to the aforementioned
	      handling of option-arguments, ambiguities may arise when at least
	      one overlapping spec takes an argument, as in `-foo: -foobar'. In
	      that case, the last matching spec wins.

	      The options of zparseopts itself cannot be stacked because, for
	      example, the stack `-DEK' is indistinguishable from a spec for the
	      GNU-style long option `--DEK'.  The options of zparseopts itself
	      are:

	      -a array
		     As described above, this names the default array in which
		     to store the recognised options.

	      -A assoc
		     If this is given, the options and their values are also put
		     into an associative array with the option names as keys and
		     the arguments (if any) as the values.

	      -D     If this option is given, all options found are removed from
		     the positional parameters of the calling shell or shell
		     function, up to but not including any not described by the
		     specs.  If the first such parameter is `-' or `--', it is
		     removed as well.  This is similar to using the shift
		     builtin.

	      -E     This changes the parsing rules to not stop at the first
		     string that isn't described by one of the specs.  It can be
		     used to test for or (if used together with -D) extract
		     options and their arguments, ignoring all other options and
		     arguments that may be in the positional parameters.  As
		     indicated above, parsing still stops at the first `-' or
		     `--' not described by a spec, but it is not removed when
		     used with -D.

	      -F     If this option is given, zparseopts immediately stops at
		     the first option-like parameter not described by one of the
		     specs, prints an error message, and returns status 1.
		     Removal (-D) and extraction (-E) are not performed, and
		     option arrays are not updated.  This provides basic
		     validation for the given options.

		     Note that the appearance in the positional parameters of an
		     option without its required argument always aborts parsing
		     and returns an error as described above regardless of
		     whether this option is used.

	      -K     With this option, the arrays specified with the -a option
		     and with the `=array' forms are kept unchanged when none of
		     the specs for them is used.  Otherwise the entire array is
		     replaced when any of the specs is used.  Individual
		     elements of associative arrays specified with the -A option
		     are preserved by -K.  This allows assignment of default
		     values to arrays before calling zparseopts.

	      -M     This changes the assignment rules to implement a map among
		     equivalent option names.  If any spec uses the `=array'
		     form, the string array is interpreted as the name of
		     another spec, which is used to choose where to store the
		     values.  If no other spec is found, the values are stored
		     as usual.	This changes only the way the values are stored,
		     not the way $* is parsed, so results may be unpredictable
		     if the `name+' specifier is used inconsistently.

	      For example,

		     set -- -a -bx -c y -cz baz -cend
		     zparseopts a=foo b:=bar c+:=bar

	      will have the effect of

		     foo=(-a)
		     bar=(-b x -c y -c z)

	      The arguments from `baz' on will not be used.

	      As an example for the -E option, consider:

		     set -- -a x -b y -c z arg1 arg2
		     zparseopts -E -D b:=bar

	      will have the effect of

		     bar=(-b y)
		     set -- -a x -c z arg1 arg2

	      I.e., the option -b and its arguments are taken from the
	      positional parameters and put into the array bar.

	      The -M option can be used like this:

		     set -- -a -bx -c y -cz baz -cend
		     zparseopts -A bar -M a=foo b+: c:=b

	      to have the effect of

		     foo=(-a)
		     bar=(-a '' -b xyz)
```