ctypes.windll.shell32.ShellExecute例

いつものように自分だけのためのメモである。

とにかく毎日が CUI と GUI との間の頻繫な行き来なので、特に bash と emacs からの「なにかしらの起動」が結構鬱陶しい。普段は「cmd.exe」の「start」でどうにかごまかしていたが、日本語を含んだりすると途端に破綻したり、引用符の関係がぶっ壊れてたりで、まぁ「ないよりはマシ」でしかなかった。うーん、「start」って ShellExecute してくれてるだけだよなぁ、と、いまさら:

2020-10-24追記: 最初に挙げた版からちょっとだけ変えた。(拡張子補ってる部分。)
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import ctypes
 4 import sys
 5 import os
 6 
 7 
 8 if __name__ == '__main__':
 9     import argparse
10     parser = argparse.ArgumentParser()
11     parser.add_argument("command")
12     parser.add_argument("--cmdargs")
13     parser.add_argument(
14         "--verb",
15         choices=["edit", "find", "open", "print", "properties", "runas"],
16         default="open")
17     nShowCmd = (
18         ("SW_HIDE", 0),
19         ("SW_SHOWNORMAL", 1),
20         ("SW_SHOWMINIMIZED", 2),
21         ("SW_SHOWMAXIMIZED", 3),
22         ("SW_MAXIMIZE", 3),
23         ("SW_SHOWNOACTIVATE", 4),
24         ("SW_SHOW", 5),
25         ("SW_MINIMIZE", 6),
26         ("SW_SHOWMINNOACTIVE", 7),
27         ("SW_SHOWNA", 8),
28         ("SW_RESTORE", 9),
29         ("SW_SHOWDEFAULT", 10),
30     )
31     parser.add_argument(
32         "--showcmd",
33         choices=[m for m, v in nShowCmd],
34         default="SW_SHOWNORMAL")
35     args = parser.parse_args(sys.argv[1:])
36     cmd = os.path.normpath(args.command)
37     pth = os.environ["PATH"].split(";")
38     while not os.path.exists(cmd) and pth:
39         p = pth.pop(0)
40         cmd = os.path.normpath(os.path.join(p, args.command))
41         for ext in ("", ".exe",):
42             if os.path.exists(cmd + ext):
43                 cmd += ext
44                 break
45     if not os.path.exists(cmd):
46         raise OSError("{} was not found.".format(args.command))
47     ctypes.windll.shell32.ShellExecuteW(
48         0,
49         args.verb,
50         cmd,
51         args.cmdargs,
52         "",
53         dict(nShowCmd)[args.showcmd])

Windows 専用なので shebang が「py」なのは問題なかろ。あと PATH の展開はいわゆる「余計なお世話」かもしらん。真似したい場合、必要ないならそこは消して使っておくれ。それと verb は全然期待した動きしてないんだが…、こんなんだったっけ? win9x 時代に使ってた記憶だと、機能してたハズなんだが…。まぁいいや、ワタシは気にしない。


2021-04-09追記:
ShellExecuteを使う上記アプローチは日常で概ねいいんだけど、「DOS な start の方が楽」なケースもあることに気付いた。

ワタシはよくローカルな html ファイルを作るのだけれど、当然これは「ブラウザで開きたい」。ならば当然ながら「今開いてるブラウザ(ワタシの場合は Chrome)のタブとして開きたい」わけね。もちろん、生成した html がある場所をエクスプローラで開いて、Chrome 側には新規タブを開いて、その新規タブにドロップ、という手順を取れば目的のことが出来るわけで、「めんどうくさい」だけ、これは。けどそう、「めんどうくさい」ので、作業状態によっては「コマンドラインから開きたい」。

実際「コマンドラインから」の楽さというのは bash などの優秀なシェル機能の「補完」あってこそだが、とにかくそれに頼れるならラクチンだ、ってことなのだけれど、上記のスクリプトだと以下の代替が簡単には出来ない:

MSYS コマンドラインから。
1 [me@host: ~]$ cmd /c "start chrome -new-tab file://`pwd -W`/my_generated.html"

まずそもそもが「c:/Program Files (x86)/Google/Chrome/Application/」にパスを通しているユーザなんか普通は皆無だろうから、だからこそ「start」と「ShellExecute」が活きる。これらどちらかに依存しない場合、「パスを通す」か「フルパスで起動する」かするしかなく、「やぁねぇ」である。そして、まぁ chrome に与えるべき引数だよね、これがワタシが上で書いたスクリプトからそもそも与えるのがめんどい。

てわけで、「start と使い分けよう」てことだね、結局。


2021-04-10追記:
chrome -newtab 問題に関しては、結構使うので専用のを作っちゃった方が楽だね、てことで:

chrometabopen_with_localfile
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 from __future__ import unicode_literals
 4 
 5 import ctypes
 6 import sys
 7 import os
 8 #import pipes
 9 
10 
11 if __name__ == '__main__':
12     import argparse
13     parser = argparse.ArgumentParser()
14     parser.add_argument("localfile")
15     args = parser.parse_args(sys.argv[1:])
16     p = os.path.abspath(args.localfile).replace("\\", "/")
17     ctypes.windll.shell32.ShellExecuteW(
18         0,
19         "open",
20         "chrome",
21         '-newtab "file://{}"'.format(p),
22         "",
23         1)  # 1: SW_SHOWNORMAL

ファイル名に引用符を含む場合におかしくなる気がするんだけど、どう措置していいもんかわからん。そう、いつもの Windows の闇。


2021-04-27追記:
「chrome -newtab ザク」について、そこそこの機能拡張して gist た:

gist に貼り付ける直前に Tools/scripts/google.py とその前提となる webbrowser.py の存在を思い出して、慌ててコメントを書いたりもしましたが私は元気ですか?


2023-10-29追記:
相変わらずワタシのサイトは読まれるページと読まれないページの差が激しいなぁ、なんて思うも、よくよく考えればせいぜい all days での 5桁と2桁の差なのだから誤差ともいえるか、とも思う。とはいえやっぱりこのページは比較的よく読まれてるのね、と、いつものように読み返してみて…。

ちょっと今ちょっとしたトラブルについて調べているんだけれど、それがこのページの話とちょっと関係してて面白いかもなぁと思ったのでちょっとだけ追記しておく。まぁわかる人には自明なことだったんで、あえて言わなかったとも言えるんだけれど、実は上で貼り付けたビデオも、そのあとの追記での chrome タブの件も、「ShellExecute」例として相応しい大事な例が抜けている。

たとえば:

このように説明されているケースでは、ワタシの上のビデオで言うところの「x と命名したスクリプト」から起動出来る:

1 [me@host: ~]$ x tpm.msc

いつだってこちらの方が楽だ、なんて言うつもりは毛頭ないし、事実このアプローチが余計な心労(徒労)を招くこともあることは言っておきつつも、作業状況によってはこの方が楽なこともある、ということは知っておくと良い。

あとこれも説明を省いたが、この ShellExecute、実際は (CPython の Windows ネイティブ版)python の os.system 関数が呼び出すので、以下でも同じ目的のことは果たせる:

1 >>> import os
2 >>> os.system("msinfo32")
3 0
4 >>> os.system("tpm.msc")
5 0

ダイレクトに ctypes.windll.shell32.ShellExecuteW を呼び出すのと os.system を呼び出すのとではもちろん完全に同じにはならないが、それはほぼ「コマンドライン解析」部分の違いであるため、このように引数なしで起動出来るパターンでは普通は違いは生じない。

あとね、一番最初の書き出しがそもそも emacs だの bash だの言ってるし、「自分専用メモ」を宣言して始めてるネタなのであまり気にしなくてもいいのかも知らんけれど、さすがにあまりにも前提事項について書いてなかったので一応補足しておく。ひとつは、まずはこのネタの前提は「Unix もどき環境(ワタシの場合は MSYS)」前提で、その環境下での bash.exe の上から使うことを前提にしている。そして、emacs はこの MSYS とは直接の関係ないのだが、使うシェルを .emacs で設定していて、それがあるのでビデオのようなことが出来る。hello, emacs に補足の形で設定例を書いているので、興味がある人はそちらを見てほしい。


2023-11-21追記:
追記してまで書く内容かとも思うし、そもそも ShellExecute である必要もないネタではあるのだが、「専用のものを作っちまった方が楽」な一つ:

mpc
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import sys
 4 import ctypes
 5 
 6 
 7 if __name__ == '__main__':
 8     import argparse
 9     parser = argparse.ArgumentParser()
10     parser.add_argument("media")
11     parser.add_argument("--close", action="store_true")
12     args = parser.parse_args()
13     ctypes.windll.shell32.ShellExecuteW(
14         0,
15         "open",
16         "c:/Program Files/MPC-HC/mpc-hc64.exe",
17         ("" if not args.close else "/play /close ") + args.media,
18         "",
19         1)  # 1: SW_SHOWNORMAL

「ビデオを起動」というノリなら最初ので十分なのだけれど、そうではなくて、メディアプレイヤーとして Media Player Classic – Home Cinema をコマンドラインから直接起動したい、という際に使いたいものであり、なおかつ、「再生終了したら閉じなはれ」指定が簡単にできるようにしたもの。これってのは、コマンドラインからたとえば:

作ったスクリプトが mpc という名前でパスが通っているとして
1 [me@host: ~]$ for i in a.mkv b.mkv ; do mpc --close "$i"; done

といった具合に連続再生みたいなことを簡単にやりたい、ということ。プレイリストでは対応出来ない(/しにくい)使い方がわずかばかりあるのよね。

なお、mpc のコマンドラインオプションは、ヘルプ(→コマンドラインスイッチ(C))で参照出来る: