コンソールウィンドウのタイトルをセットする(Windows)

title。以上。

ウソである。これだけが目的とちゃう。

Windows の「コンソールウィンドウ」も vt100 の流れをくむ正統な「端末」である。「Unix でないので端末とは呼びません」なんてことをのたまう方がおられようが、vt100 互換なら端末であろ。

なわけなので、text コマンドを使わずに Unix でも通用する「端末シーケンスを送信」することによっても、タイトルバーテキストを変えることが出来る。なんか「デフォルトでゴテゴテしたタイトルバー操作をする PS1」を仕込んでた linux ディストリビューションがあった記憶があるんだけど気のせいか? それはともかくとして、「text コマンドなしで」PS1 にタイトルバーテキスト変更を仕込む基本は:

1 export PS1="\[\033]0;MYTITLE\007\]bash$ "

てな感じ。

そうなんだけどね、実際のニーズって本当はこんなに単純ではなくてね、普通これって「迷子になりにくいように」するためにこういうことをしたいわけで、つまりたとえば:

ワタシの場合はこんな、「MS な開発環境(環境変数)がセットアップされた状態の cmd.exe 始まり」であることを、以後 bash を起動しようがわかるようにしときたい、てわけだ。なんせ何種類もの開発環境を持ってるんでね、わからなくなると非常に弱る。

でこれが簡単に実現できるのか、つまり「元のウィンドウタイトルを取れるの?」てことだが、「unix なら簡単だぜっ」とか「windows だけが例によって難しいぜっ」とかってことはなく、元々「送信オンリー」つぅか「書き込み専用」という仕様なので、「少なくとも移植性のある手段でこれを取得することは出来ない」という点においては unix だろうが windows だろうが一緒。

unix の方は試そうとしてないけど、windows の方はもうこれは、「Windows のシカケ」を多少なりとも知ってないと「どう難しいのかが想像つかない」だろうなと思う。一言で、「Windows ではプロセスの親子関係を簡単に調べる手段はない」てこと。unix みたいなプロセス ID で木をなしてるのと違い、Windows は「あらゆるものが Window で、全てがウィンドウハンドルを持っていて、プロセス ID は飾りです、エラい人にはそれがわからんのです」。

なんでもかんでもがウィンドウで、ウィンドウハンドルでもって木構造をなしてるんだったら一緒じゃんか、と思いたいんだけど実際はそうはいかない。「そういうインフラ(API)に欠ける」から。だから「プロセスツリー」を表示するだけのアプリケーションが Windows では少ない。(Windows な API を駆使して自作しようと思うなら、何度も「全ウィンドウ列挙」が必要になることにびっくりすると思う。)

これを少し改善しようと登場したのが win9x 時代の dbghelp32.dll、win2k あたりからの toolhelp32.dll で、無論これを使えば少しは簡単にこの手のことが出来なくはないんだけれど、印象はやっぱり変わらず、つまりあんまし使いやすくない。

てわけでこれだけのことをしたくても手持ちがどうしても少ないわけで、入り口からまぁ果てそうになる。

Python な oss の psutil を使うと「親子関係の把握」だけは相当やりやすいんだけれど、ウィンドウタイトルにまでは関知してくれないので、別の手段で取得しなければならない。ところが Windows は例によって「プロセス ID は飾り」なので、何をするにもウィンドウハンドルが必要だが無論 psutil はこれにも関知しない。手詰まりか?

sysinternals には pslist というコンソールアプリケーションがいて、これを「/t」付きで起動すると、目的の「プロセスツリー」を表示出来るものの、これも同じくウィンドウタイトルは教えてくれない。

唯一、DOS コマンドの tasklist だけがウィンドウタイトルとプロセス ID を紐付けてくれる:

1 c:> tasklist /v /FO CSV 
2 "イメージ名","PID","セッション名","セッション#","メモリ使用量","状態","ユーザー名","CPU 時間","ウィンドウ タイトル"
3 "System Idle Process","0","Services","0","24 K","Unknown","NT AUTHORITY\SYSTEM","63:50:12","N/A"
4 "System","4","Services","0","11,128 K","Unknown","N/A","0:10:30","N/A"
5    ...
6 "chrome.exe","5388","Console","1","96,036 K","Unknown","hhsprings-PC\hhsprings","0:00:08","N/A"
7 "cmd.exe","4804","Console","1","4,104 K","Running","hhsprings-PC\hhsprings","0:00:00","x64_x86 Cross Tools Command Prompt for VS 2017"
8    ...

ゆえに、ひとまずは psutil か pslist と tasklist の2つの道具立てから始めるのが最も手っ取り早い…、はず。

なわけでさ、こんなんを書いてはみるわけだ:

 1 #! c:/Python27/python
 2 import csv
 3 import subprocess
 4 
 5 import psutil  # pip install psutil
 6 
 7 reader = csv.reader(
 8     subprocess.check_output(
 9         ["tasklist", "/FO", "CSV", "/v", "/NH"]
10         ).strip().split("\r\n"))
11 titles = {}
12 for line in reader:
13     del line[2:-1]
14     _, pid, title = line
15     titles[int(pid)] = title.decode("mbcs")
16 parent = psutil.Process().parent()
17 title = "N/A"
18 while parent and title == "N/A":
19     title = titles[parent.pid]
20     parent = parent.parent()
21 basetitle = title.split(" - ")[0]
22 res = (
23     r'''export PS1=''' + \
24     r'''"\[\033]0;%s - \s-\v[$SHLVL]\007\]''' + \
25     r'''\[\e[35m\]\W\$\[\e[m\] "''') % basetitle
26 print(res)

shbang がいつものワタシと違うのにはまさに意味があって、これこそが「プロセスツリーは飾りです」にモロに関係してて。普段ワタシがやってる:

1 #! /bin/env python

という記述がまさにこの「プロセスツリー、のようなもの」のチェインを見事なまでに打ち砕いてしまうのだ。この場合 env は「アタシが元いた cmd.exe」の子孫とならない。

で、

1 #! c:/Python27/python

であれば万事オッケーなのか、というとこれもそうではなくて、結局「どのように起動したのか」に繊細に影響されて、まさに「うまくいったりいかなかったり」。歯痒いのな。

仮にうまくいくとすれば。先のスクリプトを仮に「xxx.py」としてパスが通った場所にあるとして、

1 me@host: ~$ eval `xxx.py`

とすれば、うまくいけばこうなる:

そもそもこういった仕組みである都合、仮にいつでもうまくいく手段だとしてもおいしくないかもしれない。というのもまさしく「.bashrc とかに書けない」から。書けないというかそうすることに意味はない、てことでしょ。

まぁこれ以上突き詰めようとはワタシは今のところ思えないんで、先のスクリプトが「運よくうまくいく」ことにひとまず賭け続けようと思う。