bashlex だけでは大して救世主にはならないが explainshell.com は非常に面白い

shlex は言っちゃえば貧相。

ここ一週間ほどパーサジェネレータ関係のことをやってたので、ついでなので Unix 系 OS のシェルの lexer/parser の存在やら BNF 定義なんかを少しは探してみようと思った。

Unix 生まれもしくは Unix 育ちの「そこそこ」メジャーな OSS には、ごくたまーに「Windows で動きません!!!、sh.exe がないって言われるんです!!!!」という issue が挙がる。この手の issue に対するデベロッパの反応は本当に様々で、「じゃぁ出来るだけ Unix 依存を減らしていこうか」と考える者も少なからずいる。が、「別にええやん、cygwin とか MSYS があるんだからさ」とあまり真剣に対応しようと考えない者の方が多いかと思う。「依存するツールセットが便利で優秀で複雑」であるほど「Unix 的なものへの依存」は断ち切りがたく、結果として「bash 依存」から離れられない、ということが多くなる。

「便利で優秀で複雑」なツールが「シェルでないならば」、比較的「Windows 版バイナリ」が素直に手に入ることも多いので良いのだけれど、シェルや make などのビルドツールはこれはもう「ほぼ Cygwin 系一択 (MSYS ももとはといえば cygwin)」。ここまで規模の大きなものになると、「一つ一つ個々に Windows 版バイナリがある」ものをちまちま拾っていってどうにかなるものではない。例えば awk.exe だけ欲しい、なんてのは可能だが、「bash.exe」だけ拾ってきてもまず役には立たない。なぜなら「bash.exe」への依存というのは「ほぼ Unix システム全体への依存」とマインドとしては等しいからだ(膨大な Unix コマンドも暗に期待しているということ)。

従って、たとえ bash のグラマーが BNF などの形で入手できたり、独立した bash のパーサが実在していたとしても、「Unix コマンド群のエミュレート」もセットでない限りは、「Windows で動きません!!!、sh.exe がないって言われるんです!!!!」への解とはなりえない、普通は。ゆえ、どんな場合でもまっとうな正論は「清く正しい Unix エミュレーション(MSYS, MSYS2, and Cygwin, etc.)をインストールしなはれ」。揺るがない、これは。

「POSIX シェル」については、OpenGroup が BNF の形で定義している。ただ、世界の9割を占めるんではないかと思われる bash はこれではないのはご注意。POSIX なんか所詮後追いの掛け声。お飾りだ、役には立つまい、ほとんどの場合。(無論、GNU configure スクリプトみたいなミニマリスト相手には多分役に立つ。たぶんかなり POSIX sh の仕様から逸脱しないで書いてる。)

bash の EBNF がなぜかこんなところに無造作に置かれている。が、おぬしは一体なにものなのだ? もともと bash ソースコード中には BNF 定義そのものは書かれていないような記述もみつけたが、仮にあっても YACC (BISON) ソースの形だろうから、あっても脳内変換が苦痛だろう。手っ取り早くは、オライリー本の付録にぺろっと載ってる。Bash 2.0 のだって。で、jab に置かれてるのはこれそのものらしいな。

で。いたよ、bashlex。ply ベースなのが若干いやな気分もあるが、内容は期待通りというか想像通りのもので、先に宣言した通り「これだけあってもエンドユーザ目線での救世主には絶対にならない」:

 1 >>> import bashlex
 2 >>> parts = bashlex.parse('true && cat <(echo $(echo foo))')
 3 >>> for ast in parts:
 4 ...     print ast.dump()
 5 ListNode(pos=(0, 31), parts=[
 6   CommandNode(pos=(0, 4), parts=[
 7     WordNode(pos=(0, 4), word='true'),
 8   ]),
 9   OperatorNode(op='&&', pos=(5, 7)),
10   CommandNode(pos=(8, 31), parts=[
11     WordNode(pos=(8, 11), word='cat'),
12     WordNode(pos=(12, 31), word='<(echo $(echo foo))', parts=[
13       ProcesssubstitutionNode(command=
14         CommandNode(pos=(14, 30), parts=[
15           WordNode(pos=(14, 18), word='echo'),
16           WordNode(pos=(19, 30), word='$(echo foo)', parts=[
17             CommandsubstitutionNode(command=
18               CommandNode(pos=(21, 29), parts=[
19                 WordNode(pos=(21, 25), word='echo'),
20                 WordNode(pos=(26, 29), word='foo'),
21               ]), pos=(19, 30)),
22           ]),
23         ]), pos=(12, 31)),
24     ]),
25   ]),
26 ])
1 >>> list(bashlex.split('cat <(echo "a $(echo b)") | tee'))
2 ['cat', '<(echo "a $(echo b)")', '|', 'tee']
3 >>>
4 >>> import shlex  # standard library
5 >>> shlex.split('cat <(echo "a $(echo b)") | tee')
6 ['cat', '<(echo', 'a $(echo b))', '|', 'tee']

この bashlex を使って何かを作りたい、というのであれば、多分 nodevisitor を書けば良い。「エンドユーザ向けの救世主を作りたい」という野望に対して焦るつもりがなくボトムアップに何か作っていこうというつもりである場合に限れば、非常に使いやすい「ようにみえる」。例としてこんなシンプルなタスクが置かれてる。当面こんなんで良ければ、てことな。

繰り返すが、「今すぐ「sh.exe がないんです!!!!!!!」への救世主になったるぜ」のつもりなら、nodevisitor を書くことが意味することは、「リッチなジョブコントロールの実現」、「リッチなシグナルハンドラの実現」、「リッチなリダイレクトなどの連携の実現」、「シェルとは独立した Unix コマンド群のエミュレート」、「Unix の仮想デバイスのエミュレート(/dev/audio やら /dev/null やら /dev/hdaxxx やら)」の一切合財を背負わなければならない。すなわち「Cygwin/MSYS がやってきたこと/やっていること」全てをやろうとしなければならない。だから結局皆 Cygwin/MSYS を選ぶのだ。

というわけでこれその「だけで嬉しい」人々は非常に限られるわけだが、いやはや、これは違うぞ:

これ、ローカル PC でサーバとして動作させることが出来るとのこと。Cygwin は man ページもあるのでいいが MSYS には man がほぼ皆無。代わりに使えるか? と思ったりもする。(まぁでもこれも結局「Unix 依存」に等しいような気もするので、MSYS ユーザの助けにはあんましならんかもね。)