「Batch Programming Considered Harmful」コレクションの追加

素敵過ぎて気が滅入る。

さすが:

 1 c:\Domestos>cd c:/Users/hhsprings/wk
 2 c:\Users\hhsprings\wk>
 3 
 4 c:\Users\hhsprings\wk>md tmp
 5 
 6 c:\Users\hhsprings\wk>md tmp/hoge
 7 コマンドの構文が誤っています。
 8 
 9 c:\Users\hhsprings\wk>md tmp\hoge
10 
11 c:\Users\hhsprings\wk>copy *.py tmp/hoge
12 コマンドの構文が誤っています。
13 
14 c:\Users\hhsprings\wk>copy *.py tmp\hoge
15 xxx.py
16         1 個のファイルをコピーしました。
17 
18 c:\Users\hhsprings\wk>copy *.py "tmp/hoge"
19 xxx.py
20 tmp/hoge\xxx.py を上書きしますか? (Yes/No/All): All
21         1 個のファイルをコピーしました。
22 
23 c:\Users\hhsprings\wk>

ちょっと Python subprocess から domestos にご用があって、改めて気付いた。こんな振る舞いだったとは…。

ちょっと昔の話になるけれど、Windows 9x 時代は実は「ワイルドカードを解釈して複数ファイル指定とみなす」のはコマンドインタプリタの仕事ではなく、各アプリケーションの仕事だった。それ用のライブラリまでご丁寧に用意されていて、そのライブラリを使って作ったものだけが「ワイルドカード」を扱えた。今でもそうなのかもしれないけれど、少なくともエンドユーザがこのことでストレスを感じることはなくなった。

ワイルドカード問題は少なくとも見かけ上は解決しているわけだけれど、結局のところこれは「コマンドインタプリタとコマンドの責務分担」の根本的な誤りに起因していて。つまり「本来コマンドインタプリタがやるべきことをアプリケーションに丸投げする」という、「それやっちゃったらシェルじゃないやん」てことを「いまだに」踏襲してるがために、こうしたヘンチクリンなことが今でも起こる。

そう、多分上の「ヘンチクリンな振る舞い」は、

  1. 二重引用符で囲まないものはコマンドインタプリタが解釈する
  2. 二重引用符で囲むとコマンドに垂れ流される
  3. コマンドが POSIX 形式のスラッシュ区切りを許すかどうかはコマンド次第

という間違った設計に基いている。1. から既におかしいんだぞ。なんで勝手に解釈するか。それはあんたの仕事ではない。(この理解は間違ってるかもしれないのでワタシを信用し過ぎないで。もしかしたら「二重引用符内にコマンドインタプリタが魔法をかける」という、もっと凶悪な仕様かもしんないし。)

でなぁ…。こんなことやってるとさ、まさしくこの問題にぶつかっちゃうわけだ。全体ではスラッシュで統一したいのに、バカに渡すときだけ是が非でもバックスラッシュにせねばならぬ、というわけだ。「全体ではスラッシュで統一」を諦めることは非常に不愉快なことで、特にログファイルが肥大するのが最悪だ。

こんなんで置換するしかないのかなぁと:

注: エスケープ文字「^」に未対応
 1 import re
 2 
 3 def _sub_pathstr(commandline):
 4     r"""
 5     >>> print(_sub_pathstr('ren build\\tmp/abc.dll build/tmp/bcd.dll'))
 6     ren build\tmp\abc.dll build\tmp\bcd.dll
 7     >>> print(_sub_pathstr('copy build\\tmp/abc.dll ../..\\Release/'))
 8     copy build\tmp\abc.dll ..\..\Release\
 9     >>> print(_sub_pathstr('copy ..\\tmp/abc.dll ../..\\Release/'))
10     copy ..\tmp\abc.dll ..\..\Release\
11     >>> print(_sub_pathstr('copy /Y ..\\tmp/abc.dll ../..\\Release/'))
12     copy /Y ..\tmp\abc.dll ..\..\Release\
13     >>> print(_sub_pathstr('copy /Y "..\\tmp/abc.dll" "../..\\Release/"'))
14     copy /Y "..\tmp/abc.dll" "../..\Release/"
15     >>> print(_sub_pathstr('copy /Y "..\\tmp/\\abc.dll" "../..\\Release/"'))
16     copy /Y "..\tmp/\abc.dll" "../..\Release/"
17     >>> print(_sub_pathstr('build\\tmp/abc.dll ../..\\Release/'))
18     build\tmp\abc.dll ..\..\Release\
19     >>> print(_sub_pathstr('build\\tmp/abc.dll'))
20     build\tmp\abc.dll
21     >>> print(_sub_pathstr('build\\tmp\\/\\//\\//\\abc.dll'))
22     build\tmp\abc.dll
23     >>> print(_sub_pathstr('copy "..\\""tm p""/\\abc.dll" "..\\Release/"'))
24     copy "..\""tm p""/\abc.dll" "..\Release/"
25     >>> print(_sub_pathstr(
26     ...     'c:\\Program Files (x86)/Microsoft Visual Studio\\2017/'))
27     c:\Program Files (x86)\Microsoft Visual Studio\2017\
28     """
29     #
30     def _lexer(s):
31         _tokd = {
32             "root": [
33                 (re.compile(r'"'), 'L', 'dq'),
34                 (re.compile(r'([^\s"]+[/\\])+[^\s"]*?'), "P", ""),
35                 (re.compile(r'[^"]'), 'L', ''),
36                 ],
37             "dq": [
38                 (re.compile(r'""'), 'L', ''),
39                 (re.compile(r'\\"'), 'L', ''),
40                 (re.compile(r'"'), 'L', '#pop'),
41                 (re.compile(r'[^"]+'), 'L', ''),
42                 ],
43             }
44         states = ["root"]
45         state = states[0]
46         while s:
47             for rgx, tok, trans in _tokd[state]:
48                 m = rgx.match(s)
49                 if m:
50                     yield tok, m.group(0)
51                     if trans:
52                         if trans == "#pop":
53                             states.pop(-1)
54                             state = states[-1]
55                         else:
56                             states.append(trans)
57                             state = trans
58                     s = s[m.span()[1]:]
59                     break
60     #
61     v = ""
62     for tok, s in _lexer(commandline):
63         if tok == "L":
64             v += s
65         else:
66             v += re.sub(r"/+", r"\\", s.replace("\\", "/"))
67     return v
68 
69 
70 if __name__ == '__main__':
71     import doctest
72     doctest.testmod()

なお、この問題については二重引用符そのものをエスケープする術問題も一緒にくっついてくる。引用先、「怒り」が伝わってきてオモロイよ、読んでみる事をオススメする。