UNIX に慣れている人は、コマンドプロンプトを使いたがる。しかし全ての場合においてコンソールアプリケーションは有能では無いし、異常に長い引数は省略したい。Java VM で動く言語のクラスパス等はスクリプトに書きたいし、オフィシャルから Windows ユーザ向けに用意される物はだいたいバッチファイルだ。しかしながら Cygwin は標準提供物ではないし、嫌いだ。いや、大嫌いだ。
そこで私達は一般的に、バッチファイルという一見便利そげで実は非情なまでに我々に独特の仕様を強要するDSLを頻繁に使う事になる。
例えば Java VM 上で動く clojure を Windows 上で repl として使う場合、僕は以下の様なバッチファイルを「clj.bat」というファイル名にして使っている。
@echo off
set CLOJURE_EXT=%USERPROFILE%\.clojure.d
setlocal enabledelayedexpansion
if defined CLOJURE_EXT for %%E in ("%CLOJURE_EXT%\*") do set CP=!CP!;%%~fE
if not defined CLOJURE_JAVA set CLOJURE_JAVA=java
if exist .clojure for /F %%E in (".clojure\*") do set CP=!CP!;%%~fE
%CLOJURE_JAVA% %CLOJURE_OPTS% -cp "%CP%" clojure.main -i "%CLOJURE_EXT%\cljrc.clj" %1 %2 %3 %4 %5 %6 %7 %8 %9 --repl
これは別に大した物ではないし、僕にとって心地よく動作している。しかしこの repl を終了しようと、UNIX ユーザでは当たり前の呪文、そう「CTRL-C」をタイプすると事態が急転する。
C:\temp>clj.bat
Clojure 1.4.0
user=>
バッチ ジョブを終了しますか (Y/N)?
お前は俺が終了しろと言ったのが聞こえないのか?java は既に終了してしまってるのにこれ以上バッチファイルの実行を継続する理由など無い! start を絡ませればメッセージを出さなくする事も出来るが、その start を含んだコマンドをバッチファイルに書いてしまったら堂々巡りだ!我々はコマンドプロンプトからコマンド名で実行したんだ!
全世界の Windows ユーザがこの問題に悩んでいる。
How can I suppress the "terminate batch job" in cmd.exe - Stack Overflow中には cmd.exe にバイナリパッチを当てる人までいる。
I'm looking for a mechanism for suppressing the "Terminate batch job? (Y/N)" invitation that I get whenever I press CTRL-C in a program started from a batch file...
http://stackoverflow.com/questions/1234571/how-can-i-suppress-the-terminate-batch-job-in-cmd-exe
several useful patches for cmd.exe本来、CTRL-C をタイプしてプロセスを終了するという自由な行動が、非人道的な cmd.exe の仕様により不自由な操作を強要されている。(© EzoeRyou)
how to find what to patch using a recent version of IDA, it is quite easy to find where to patch cmd...
http://itsme.home.xs4all.nl/projects/misc/patching-cmdexe.html
これを解決しようと色々考えたが、以下の方法が良いのではないかと思う。
コマンドプロンプトをシェルの様に使う人はフルパスをタイプしないし、拡張子までタイプしない。例えば既述の「clj.bat」であれば「clj」とだけタイプする。拡張子を明示しない場合、Windows では PATHEXT 環境変数を ; で区切った順番に実行ファイルが探索される。通常、PATHEXT では「.bat」よりも「.exe」の方が先に来る。つまり「clj.bat」と同じ位置に「clj.exe」があれば、「clj.exe」が先に見つけられ実行される事になるのだ。
であれば、その exe からバッチファイルを起動し、プロセスグループに加えてやれば CTRL-C が伝播するはず。なお且つ対話形式で起動しない様にすれば「バッチ ジョブを終了しますか (Y/N)?」というふざけたメッセージをお目にかかる事はない。
よろしい、ならばコーディングだ。一番いい Vim を頼む。
#include <windows.h>
#include <string.h>
#include <stdio.h>
int
emsg() {
char* p = NULL;
DWORD err = GetLastError();
if (err == 0) return 0;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, 0, (LPTSTR)(&p), 0, NULL);
fputs(p, stderr);
LocalFree((LPVOID)p);
return err;
}
int
main(int argc, char* argv[]) {
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
int r;
char *p, *t;
t = p = strdup(GetCommandLine());
if (!p) return -1;
if (*p == '"')
do p++; while (*p && *p != '"');
else
while (*p && *p != ' ') p++;
if (!strncasecmp(p-4, ".exe", 4))
memcpy(p-4, ".bat", 4);
else {
char *comspec = getenv("COMSPEC"), *b;
if (!comspec) comspec = "CMD";
b = malloc(strlen(comspec) + 1 + 9 + strlen(t) + 5);
*b = 0;
strcat(b, comspec);
strcat(b, " /c call ");
strncat(b, t, (int) (p-t));
strcat(b, ".bat");
strcat(b, p);
free(t);
t = b;
}
si.cb = sizeof(si);
if (!CreateProcess(
NULL, t, NULL, NULL, TRUE,
CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_PROCESS_GROUP,
NULL, NULL, &si, &pi))
r = emsg();
else {
CloseHandle(pi.hThread);
if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
r = emsg();
else {
DWORD code = 0;
if (!GetExitCodeProcess(pi.hProcess, &code))
r = emsg();
else {
r = (int) code;
CloseHandle(pi.hProcess);
}
}
}
free(t);
return r;
}
ソースファイルは「batter.c」として保存し、コンパイルする。コマンドラインの第一引数に「.exe」が付いていたら「.bat」に書き換え、付いていなかったら付け足し、CREATE_NEW_PROCESS_GROUP を指定してプロセスを起動する。
これをコンパイルして出来上がった batter.exe を、例えば「clj.bat」が置いてあるフォルダに「clj.exe」というファイル名てコピーする。
では新しいコマンドプロンプトを起動し、「clj」とタイプしよう...
C:\temp>clj
Clojure 1.4.0
user=>
そして CTRL-C だ。
^C
C:\temp>
やった。俺は「バッチ ジョブを終了しますか (Y/N)?」メッセージを倒した。今後「clj.bat」を好き放題に編集しても構わない。
俺は勝った。