目標
- その都度に設定ファイルを指定するタイプのスクリプト(
homebrew
で入れたmarp-cli
とか)の実行をより便利にしたい - コマンドライン(CL)オプション(OPT)を使いたい(
marp
ならば--pdf
の有無で出力をhtmlかpdfで切り替えられる)
具体的には
その引数を忠実にパースして対応させたコマンドを呼び出せるようにする
下準備
実行環境の整理
パーミッションについて
これを読んでね
スクリプト置き場
~/zscripts/bin
にパスを通して諸々のシンボリックリンクを入れる,元のスクリプトは
~/zscripts/test
のように分類することにした
シンボリックリンクを生成する際は必ずフルパスを指定して生成すること,ln -s
する時は相対パス扱いで通っちゃうけど実際に走らせる時はシェル側が絶対パスとして使ってくる,つまり
pwd # => ~/zscripts ln -s test/testZsh1 bin
は動いてしまうのだが(これは何も間違ってはいない,この時のzsh
から見ればtest/testZsh1
は存在するし,bin/testZsh
はちゃんと生成される),これを実行しようとすると
testZsh1 # => zsh「$PATHを読むぞ,~/zscripts/binを見るぞ,testZsh1があったぞ」 # => zsh「えーとtest/Zsh1に行け…?そんな場所ねぇよ!」 # => zsh「zsh: command not found」
という結果になる,ただしこの状態でも$PATH
上にシンボリックリンクは存在するため,zsh
は可能な限りコマンド補完を提供してくれる,うーんこれはかなり紛らわしいね
現状ではln
に引数対象の絶対パスの補完機能はない,というよりもシンボリックリンクではなくハードリンクならiモード番号をまんま参照するためこんなことは起きない,シンボリックリンクリンクに特有の現象と考えて諦めて受け入れるしかなさそう
パスを通す
export PATH=~/zscripts/bin:$PATH
実行ファイル作成
以下の作業はパターン化しよう
cd ~/zscripts/test touch testZshS1 chmod 0755 testZsh1 # <-rwxr-xr-x ln -s ~/zscripts/test/testZsh1 ~/zscripts/bin/ # <-最後にスラッシュつけないとディレクトリと認識されないので注意 vi testZsh1
#!/bin/zsh # this is testZsh1 echo "this is testZsh1"
% testZsh1
# => this is testZsh1
予約済み変数(特殊変数)
シェバンにzsh
で実行するよう指示されたシェルスクリプトは,起動時にユーザが操作しているシェルからzsh
のプロセスに渡される,つまりは「ログイン者と対話するシェル」と「スクリプトを実行するシェル」が必ず同じである必要がない.しかしそれでもzsh
で環境を統一することをオススメしたい理由として,zsh
が対話シェルとして渡す特殊変数と実行シェルとして受け取る特殊変数の扱いを共通化することによる利点がある.読み物が一つの同じドキュメントだけで済むために大きく労力を削減できるし,権限管理などもやりやすい上にコーディングも多少は楽になるだろうとと思われるからだ(最近になって「移行しておいてよかった」と思い始めている,それでもGoogle先生はよくzsh
を無視してbash
の記事を出してくるけど).
シェルスクリプトはシェバンの書きようによってはsh
やbash
に実行させることも可能である,それ故にシェルを切り替えた時に迂闊なミスが起こらないよう,シェルごとにスクリプトの管理フォルダと対応するパスを切り分けるべきだと考えている.
そしてログインシェルとスクリプトシェルの間でやり取りされる情報を利用するための特殊変数がzsh
には用意されている,manページから以下のコマンドで抽出できるので確認しよう
# ログインシェルが渡すもの man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS SET BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'
ちなみに元から利用できる環境変数などは以下で抽出できる
# ログインシェルが使うもの man zshparam | col -b | expand -t 4 | pcre2grep -Moe ' {0}(PARAMETERS USED BY THE SHELL\n)( {5,}(\S* *)*\S*\n*)*'
シェルスクリプトでは変数名の前に$
をつけて展開できるが,これも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
と同じっぽい
man zshoptions | col -b | expand -t 4 | pcre2grep -Moe ' {0}(SINGLE LETTER OPTIONS\n)( {3,}(\S* *)*\S*\n*)*'
仕様などは以下を参考に
実験して確認する
#!/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', @: '${@}'"
引数を入れたり消したりして試してみよう,この時点で半角スペースによる配列化が自動で実行されていることがわかるだろう
% 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}))
については先に挙げたリファレンスにも載ってる)
他のやり方などは以下を参考に
zparseopts
について
zsh
をセットで導入すれば元から使えるやつらしい,他にも候補があるらしいけどzsh
上の普遍性を考慮してこれにした
扱い上はzshmodules
の一部なのでmanページは以下で抽出できる
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つのオプションに入れたいとなると,流石に*
に対する処理を手動で実装した方が早そう,今回はやらんけど…
テンプレート
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 -x
が[-x]
となるところを[-x, -x]
にできる
+:
は引数を連結させる- 連想配列に影響が出る,配列は倍プッシュされる
-x a -x b
なら{-x: ab}
と[-x, a, -x ,b]
になる
+::
は引数を完全に連結させる- 配列に影響が出る
- 連想配列は
+:
と変わらない
- 連想配列は
-x a -x b
なら{-x: ab}
と[-xa, -xb]
になる
- 配列に影響が出る
:-
・::-
は…ほとんど意味ない- まず
-
をつけるだけで::
と同じ副作用が必ず出現する+:
と+::
みたいに有用と思われる違いがない
- 引数の置き換えは配列でも連想配列でも重複の呼び出しで勝手に発生するので…
- つまり
:-
も::-
も使い道がない,無視して良い
- つまり
- まず
実験して確認する
#!/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]}"
% 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
コマンドを改善
詳細については今は省く(また別のところでやるかも)が,CLIから叩けるmarp
はvscode拡張機能版とは異なり設定ファイルを-c
オプションで指定できる(その設定ファイルも万能ではない)が,しかしmarp
本体に記憶させることができない.そのためシェルスクリプトで事前に定めたパスを必ず指定できるように仕向けようと考えた,あとはvscodeに機能を追加するよりもzsh
のシェルスクリプトを書く練習から始めたかった.
…とりあえず-m pdf
と-m html
と-m pptx
で使い分けられるようにした.ちなみにmarp
はまだ全く使いこなせていない.
#!/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
% 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
の制御構文については以下で抽出できる
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)