Windows Terminal なら余裕。
はじめに
Windows で標準で使えるスクリプト言語と言えば
- コマンドプロンプトのバッチファイル
- Windows Script Host (WSH) の JScript
- Windows PowerShell(PowerShell 5.1系)
の3つだと思います。厳密には WSH では VBScript も使えますが,非推奨になっているので新たに作るものでは VBScript を使わないほうが良いでしょう1。
で,これらのスクリプト環境でカラー出力したいときがあります。
このうち PowerShell はカラー出力に対応しています。バッチファイルは VT100 準拠のエスケープシーケンスを利用してカラー出力可能です2。ところが WSH ではエスケープシーケンスを通してくれないのでカラー出力できないのです。
プログラミング言語としてはバッチファイルや PowerShell スクリプトよりも JScript のほうが遥かに親しみ易いと思うので,可能な限り WSH を使いたい方も多いでしょう。
ということで WSH でなんとかカラー出力できないか悪戦苦闘した記事です。
PowerShell スクリプトの場合
まずは PowerShell スクリプトの例を見てみます。PowerShell の場合,色を名前で指定できるので非常に分かり易いですね3。なお,背景色もオプション -BackgroundColor
で指定できます。ちなみに色の順番は後述するエスケープシーケンスの番号に合わせています。
Write-Host -ForegroundColor darkred '暗い赤色'
Write-Host -ForegroundColor darkgreen '暗い緑色'
Write-Host -ForegroundColor darkyellow '暗い黄色'
Write-Host -ForegroundColor darkblue '暗い青色'
Write-Host -ForegroundColor darkmagenta '暗い紫色'
Write-Host -ForegroundColor darkcyan '暗い水色'
Write-Host -ForegroundColor gray '灰色'
Write-Host -ForegroundColor darkgray '暗い灰色'
Write-Host -ForegroundColor red '赤色'
Write-Host -ForegroundColor green '緑色'
Write-Host -ForegroundColor yellow '黄色'
Write-Host -ForegroundColor blue '青色'
Write-Host -ForegroundColor magenta '紫色'
Write-Host -ForegroundColor cyan '水色'
Write-Host -ForegroundColor white '白色'
Write-Host 'デフォルト'
PowerShell スクリプトの実行結果を示します。
コマンドプロンプトのバッチファイルの場合
一方,コマンドプロンプトのバッチファイルですが,VT100 準拠のエスケープシーケンスを使用すればカラー出力可能です4。なお,テキストエディタによってはエスケープコード ^[
(16進数で1Bh)を入力・表示するのに苦労するかもしれません。
バッチファイルの実行結果を示します。
Windows Script Host (WSH) の JScript の場合
JScript の例を示します。JScript ではエスケープコードを \x1b
と書けるのが良いですね。
WScript.Echo("\x1b[31m暗い赤色");
WScript.Echo("\x1b[32m暗い緑色");
WScript.Echo("\x1b[33m暗い黄色");
WScript.Echo("\x1b[34m暗い青色");
WScript.Echo("\x1b[35m暗い紫色");
WScript.Echo("\x1b[36m暗い水色");
WScript.Echo("\x1b[37m灰色");
WScript.Echo("\x1b[90m暗い灰色");
WScript.Echo("\x1b[91m赤色");
WScript.Echo("\x1b[92m緑色");
WScript.Echo("\x1b[93m黄色");
WScript.Echo("\x1b[94m青色");
WScript.Echo("\x1b[95m紫色");
WScript.Echo("\x1b[96m水色");
WScript.Echo("\x1b[97m白色");
WScript.Echo("\x1b[0mデフォルト");
ただし,カラー出力できません。うまくエスケープシーケンスを解釈してくれないようです。
ためしに出力をファイルに一旦リダイレクトしてから表示してみると上手くカラー表示できるので,エスケープシーケンス自体は正常に出力されており,エスケープシーケンスが解釈されないでスルー出力されていると思われます。
コンソールモードを調べる
Windows 10 以降,VT100 準拠のエスケープシーケンスがサポートされたとはいえ,デフォルトでは有効になっていない可能性があります。そこで下記のC言語プログラムを作成して確認することにしました。
#include <windows.h>
#include <stdio.h>
#include <string.h>
int main( int argc, char *argv[] ) {
// HANDLE handle = GetStdHandle( STD_OUTPUT_HANDLE );
HANDLE handle = CreateFile( "CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
DWORD mode;
GetConsoleMode( handle, &mode );
if( argc < 2 ) {
fprintf( stderr, "エスケープシーケンスは %s です。\n",
( mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING ) ? "<ON>" : "<OFF>" );
return 0;
} else if( !_stricmp( argv[1], "ON" ) ) {
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
} else if( !_stricmp( argv[1], "OFF" ) ) {
mode &= ~ENABLE_VIRTUAL_TERMINAL_PROCESSING;
} else {
fprintf( stderr, "エスケープシーケンスの ON と OFF を切り替えます。\n" );
fprintf( stderr, "\n" );
fprintf( stderr, "ESCAPE(.EXE) [ON | OFF]\n" );
return -1;
}
SetConsoleMode( handle, mode );
CloseHandle( handle );
return 0;
}
ビルド方法は cl
コマンド一発です。Visual Studio Community 2022 の 32bit 版Cコンパイラを使いました。
cl ESCAPE.C
引数なしで実行すると現在の設定値を表示します。やはり,デフォルトではエスケープシーケンス機能が OFF 状態になっているようです。
c:\Qiita>escape
エスケープシーケンスは <OFF> です。
パラメータを指定すると ON/OFF の設定を切り替えられます。残念ながら,プログラムが終了すると OFF 状態に戻ってしまうようです。
c:\Qiita>escape on
c:\Qiita>escape
エスケープシーケンスは <OFF> です。
解決法その1
先ほど作成したエスケープシーケンスを有効化させるプログラムを WSH の実行中に呼び出します。
var shell = WScript.CreateObject("WScript.Shell");
var proc = shell.Exec("ESCAPE ON");
while(proc.Status == 0) WScript.Sleep(100);
WScript.Echo("\x1b[31m暗い赤色");
WScript.Echo("\x1b[32m暗い緑色");
WScript.Echo("\x1b[33m暗い黄色");
WScript.Echo("\x1b[34m暗い青色");
WScript.Echo("\x1b[35m暗い紫色");
WScript.Echo("\x1b[36m暗い水色");
WScript.Echo("\x1b[37m灰色");
WScript.Echo("\x1b[90m暗い灰色");
WScript.Echo("\x1b[91m赤色");
WScript.Echo("\x1b[92m緑色");
WScript.Echo("\x1b[93m黄色");
WScript.Echo("\x1b[94m青色");
WScript.Echo("\x1b[95m紫色");
WScript.Echo("\x1b[96m水色");
WScript.Echo("\x1b[97m白色");
WScript.Echo("\x1b[0mデフォルト");
こうするとカラー出力が可能です。
解決法その2
エスケープシーケンスを有効化させるプログラムを起動し,そのまま WSH のプログラムを子プロセスとして実行させます。
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <errno.h>
int main( int argc, char *argv[] ) {
//--------------------------------------------------------------------------
// ヘルプメッセージ
//--------------------------------------------------------------------------
if( argc < 2 ) {
fprintf( stderr, "エスケープシーケンスを ON にしてコマンドを実行します。\n" );
fprintf( stderr, "\n" );
fprintf( stderr, "VT100ON(.EXE) [コマンド] [オプション...]\n" );
return -1;
}
//--------------------------------------------------------------------------
// エスケープシーケンスを有効化
//--------------------------------------------------------------------------
// HANDLE handle = GetStdHandle( STD_OUTPUT_HANDLE );
HANDLE handle = CreateFile( "CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
DWORD mode;
GetConsoleMode( handle, &mode );
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode( handle, mode );
CloseHandle( handle );
//--------------------------------------------------------------------------
// コマンドラインパラメータを引き継いで子プロセスを起動
//--------------------------------------------------------------------------
int ret = (int)_spawnvp( _P_WAIT, argv[1], &argv[1] );
//--------------------------------------------------------------------------
// エラーメッセージの表示
//--------------------------------------------------------------------------
char *msg = (char*)NULL;
switch( errno ) {
case E2BIG: msg = "引数リストが 1024 バイトを超えています。"; break;
case EINVAL: msg = "mode 引数が無効です。"; break;
case ENOENT: msg = "ファイルまたはパスが見つかりません。"; break;
case ENOEXEC: msg = "指定されたファイルが実行可能ファイルではないか、"
"無効な実行可能ファイル形式です。"; break;
case ENOMEM: msg = "新しいプロセスを実行するのに十分なメモリがありません。"; break;
}
if( msg != (char*)NULL ) fputs( msg, stderr );
return ret;
}
ビルド方法は cl
コマンド一発です。
cl VT100ON.C
この場合もカラー出力が可能です。なお,注意事項として WSH のインタプリタ cscript.exe
を明示して呼び出す必要があります。
解決法その3
先ほど作成したプログラム VT100ON.EXE
を使ってコマンドインタープリタ CMD.EXE
を子プロセスとして実行します。幸いなことにコマンドインタープリタはエスケープシーケンスの設定を親プロセスから引き継ぐようなのでデフォルトで ON 状態になっています。こうなれば,WSH でカラー出力が可能です。
解決法その4
コマンドプロンプトの起動直後からエスケープシーケンスが有効になるようなレジストリ設定があるといいなと思ってずぅぅぅっとネット検索を続けていたら,ようやく見つかりました。下記レジストリを追加すれば良いのです56。
キー名 | 値名 | 型 | 値 |
---|---|---|---|
HKCU\Console |
VirtualTerminalLevel |
REG_DWORD |
0x1 |
ただし,レジストリを追加した直後から使える訳ではありません。
いったんコマンドプロンプトを終了して,もう一度立ち上げ直すとレジストリの変更が有効になるようです。
解決法その5
エスケープシーケンスを有効にするC言語プログラムを作成したり,レジストリを書き換える必要がない方法です。標準出力ではなく,コンソール出力のデバイス CONOUT$
をオープンして書き込むという方法です。
var fs = WScript.CreateObject("Scripting.FileSystemObject");
var ts = fs.CreateTextFile("CONOUT$");
ts.WriteLine("\x1b[31m暗い赤色");
ts.WriteLine("\x1b[32m暗い緑色");
ts.WriteLine("\x1b[33m暗い黄色");
ts.WriteLine("\x1b[34m暗い青色");
ts.WriteLine("\x1b[35m暗い紫色");
ts.WriteLine("\x1b[36m暗い水色");
ts.WriteLine("\x1b[37m灰色");
ts.WriteLine("\x1b[90m暗い灰色");
ts.WriteLine("\x1b[91m赤色");
ts.WriteLine("\x1b[92m緑色");
ts.WriteLine("\x1b[93m黄色");
ts.WriteLine("\x1b[94m青色");
ts.WriteLine("\x1b[95m紫色");
ts.WriteLine("\x1b[96m水色");
ts.WriteLine("\x1b[97m白色");
ts.WriteLine("\x1b[0mデフォルト");
ただし,そのままではカラー出力できません。標準出力を NUL
デバイスにリダイレクトすると何故かカラー出力できるのです。
これで何故カラー出力できるのか?詳しい動作原理は分かっていません。仮にカラー表示できるのなら,NUL
デバイスにリダイレクトしなくてもカラー表示できて欲しいところです。この方法は,標準出力がファイルにリダイレクトされているかどうか判別する方法を模索しているときに偶然発見したものです。標準出力がファイルにリダイレクトされているときにはエスケープシーケンスを付けないようにするのが目的でした。リダイレクトの判別方法自体はまだ発見できていませんが,おそらく不可能だと考えています。
Windows Terminal ならどうなの?
ちなみに Windows Terminal ではデフォルトでエスケープシーケンスが ON になっていました。ということで,Windows Terminal であれば特に技巧を凝らさなくてもエスケープシーケンスを通してくれるのでカラー出力が可能です。
結論
- Windows 10 以降,VT100 準拠のエスケープシーケンスがサポートされ,コンソールプログラムでもエスケープシーケンス利用したカラー出力が可能になりました。ただし,デフォルトではエスケープシーケンスは OFF 状態になっています。このため,コマンドプロンプト上で動作するプログラムはエスケープシーケンスを有効化させるための Win32 API を呼び出す必要がありますが,当該プログラムが終了するとエスケープシーケンスの設定は元に戻ります。つまり,エスケープシーケンスの有効期間は ON にしたプログラムの実行中に限ります。
- ただし,バッチファイルでは何もしなくてもエスケープシーケンスを利用可能です。おそらく
echo
やtype
などのビルトインコマンドは内部的にエスケープシーケンスを ON にして実行しているものと推察されます。 - Windows Script Host(WSH)でエスケープシーケンスを利用する場合,下記5つの方法があります。
- その1)WSH スクリプト実行中にエスケープシーケンス有効化プログラムを実行
- エスケープシーケンスを ON にするプログラムを別途作成し,WSH のスクリプト内部から呼び出します。エスケープシーケンスを有効にするプログラムの実行が終了しても,コマンドインタプリタに処理を返さなければエスケープシーケンスの ON 状態を継続できるようです。
- その2)エスケープシーケンス有効化プログラムで WSH スクリプトを実行
- エスケープシーケンスを ON にするプログラムを別途作成し,そのまま子プロセスで WSH のスクリプトを実行するようにします。本記事では敢えてコマンドインタプリタを介さない
spawn
系の関数を使いましたが,コマンドインタプリタ自身はエスケープシーケンス設定を親プロセスから引き継ぐことが明らかになったので,コマンドインタプリタを介するsystem
関数でも良さそうです。 - その3)エスケープシーケンス有効化プログラムでコマンドインタプリタを起動
- エスケープシーケンスを ON にするプログラムを別途作成し,そのまま子プロセスでコマンドインタプリタ
CMD.EXE
を起動します。このコマンドインタプリタは親プロセスの設定を継承してエスケープシーケンスが有効になっているのでカラー出力可能です。 - その4)レジストリ書き換え
- 下記レジストリを追加します。残念ながらマイクロソフトから発信された公式情報を確認できませんでした。一番確かそうな情報が夏休みにマイクロソフトに来たインターン生由来って一抹の不安を感じるのは筆者だけでしょうか。
キー名 値名 型 値 HKCU\Console
VirtualTerminalLevel
REG_DWORD
0x1
- その5)コンソール出力デバイスに書き込む
- コンソール出力デバイス
CONOUT$
をオープンして書き込みます。ただし,そのままではカラー出力できません。何故か標準出力をNUL
デバイスにリダイレクトするとカラー出力できるようになります。ちなみに詳しい動作原理は分かっていません。
- Windows Terminal はデフォルトでエスケープシーケンスが ON 状態になっているので,とくに何の技巧を凝らすことなく普通にカラー出力可能です。
$\Huge \color{red}{\textsf{もうみんな Windows Terminal を使っちゃいなよ!!}}$
失敗または隘路事項
標準出力のハンドルを得るコードを探すと下記の例がよく見つかります。
HANDLE handle = GetStdHandle( STD_OUTPUT_HANDLE );
しかしながら,本記事の場合,直接コンソール出力のハンドルを得る下記のコードではないとうまく動作しません。
HANDLE handle = CreateFile( "CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
付録
コマンドプロンプトのデフォルトカラーと PowerShell におけるカラー名称,エスケープシーケンスコードおよびカラー名称の対応表を以下に示します。
ややこしいのは「Gray」が「白」で「DarkGray」が「明るい黒」あたりでしょうか。
デフォルト カラー |
PowerShell カラー名称 |
エスケープシーケンス | |
---|---|---|---|
コード | カラー名称 | ||
#0C0C0C |
Black | ESC [30m | 黒 |
#C50F1F |
DarkRed | ESC [31m | 赤 |
#13A10E |
DarkGreen | ESC [32m | 緑 |
#C19C00 |
DarkYellow | ESC [33m | 黄色 |
#0037DA |
DarkBlue | ESC [34m | 青 |
#881798 |
DarkMagenta | ESC [35m | マゼンタ |
#3A96DD |
DarkCyan | ESC [36m | シアン |
#CCCCCC |
Gray | ESC [37m | 白 |
#767676 |
DarkGray | ESC [90m | 明るい黒 |
#E74856 |
Red | ESC [91m | 明るい赤 |
#16C60C |
Green | ESC [92m | 明るい緑 |
#F9F1A5 |
Yellow | ESC [93m | 明るい黄色 |
#3B78FF |
Blue | ESC [94m | 明るい青 |
#B4009E |
Magenta | ESC [95m | 明るいマゼンタ |
#61D6D6 |
Cyan | ESC [96m | 明るいシアン |
#F2F2F2 |
White | ESC [97m | 明るい白 |