Posted at

Windows向けのプログラムでANSIエスケープシーケンスを使うには

コマンドラインで動作するプログラムを書く場合、文字に色を付けると目立つので見栄えが良い。

文字に色をつける方法として、Unixの世界ではANSIエスケープシーケンスを使うのが一般的である。しかし、Windowsの世界ではANSIエスケープシーケンスは長らく二級市民だった。

この記事では、2018年現在の、WindowsでANSIエスケープシーケンスを使うための方法をまとめてみる。


Unixの場合

一応、Unixの場合をおさらいしておく。

Unixで使われるターミナルエミュレーターは、まず間違いなくANSIエスケープシーケンスに対応している。

ただし、ANSIエスケープシーケンスを使った色付けが有効なのは、ターミナルが相手の場合である。標準出力をリダイレクトしたり、パイプで流したりする場合は色付けをしないようにしたい。そこで、「標準出力がターミナルに繋がっているか」を検出する必要がある。

標準出力がターミナルに繋がっているか検出するには、<unistd.h>isatty関数を使う。isatty関数の引数はファイル記述子 (file descriptor) なので、C言語のファイルポインタFILE *を渡したい場合は<stdio.h>fileno関数を使って変換してやる。


unistd.h

int isatty(int fd);



stdio.h

int fileno(FILE *stream);


なお、これらの関数はいずれもC言語の標準で定められているわけではない(たぶんPOSIXには規定がありそう)。

これらを使うサンプルコードは、次のようになる:

#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[]) {
if (isatty(fileno(stdout))) {
fputs("Detected a TTY\n", stderr);
fputs("\033[31mRED \033[32mGREEN \033[34mBLUE\033[0m\n", stdout);
} else {
fputs("Not a TTY\n", stderr);
}
}


Windowsの場合

Windowsのコンソールは伝統的にANSIエスケープシーケンスには対応してこなかったが、最近のWindows 10(TH2/1511以降)では条件付きでANSIエスケープシーケンスに対応するようになった。

また、Windowsのコンソールは伝統的に非力だったので、色々な代替物(コンソール・ターミナルエミュレーター)が開発されてきた。このような代替物では独自にANSIエスケープシーケンスに対応している場合がある。あるいは、ansiconのようにANSIエスケープシーケンスをWin32 API呼び出しに置き換えるフィルターも開発されてきた。

ということで、ANSIエスケープシーケンスへの対応状況を表にすると、次のようになる:

標準のコンソール
ConEmu, ansicon, MinTTY等

古いWindows
非対応
対応

最新のWindows 10
SetConsoleMode呼び出しで対応
対応


コンソール(ターミナル)の検出

Unixの場合と同様、まずは標準出力がコンソール(ターミナル)に繋がっているかを検出したい。


_isatty

Microsoftの提供するCランタイムライブラリーではPOSIXライクな関数を提供しており、その中にisattyfilenoの対応物もある。ただし、「C言語の標準ではない」ことを強調してか、先頭にアンダースコアをつけて_isatty, _filenoという名前になっている。


io.h

int _isatty(int fd);



stdio.h

int _fileno(FILE *stream);


Win32 API流に言えば、GetFileType関数がFILE_TYPE_CHARを返すことを確認すれば良いだろう。


コンソールエミュレーターの検出

Windows標準のコンソールエミュレーターの代替物として、ConEmuやConsoleZ等がある。こういうエミュレーターでは、後述するMinTTYを除いて_isattyが真を返すようだ。

これらのコンソールエミュレーター等を個別に検出したい場合は、環境変数をチェックすればよい。

最近はcmderというのもあるようだが、アレのコンソール部分はConEmuを使っているようなので、個別には取り上げない。


MinTTYの検出

残念ながら、_isattyではCygwin/MSYS界隈でよく使われるターミナルエミュレーターMinTTYを検出できない。

そういう場合、MinTTYユーザーはwinptyという補助プログラムを介することになるのだが、実行されているプログラム自身がMinTTYを検出して個別対応するという方法もある。

具体的な方法は、検索キーワード「mintty GetFileInformationByHandleEx」でググると出てくるので割愛する。

(万が一MinTTYがConPTY APIに対応するようなことがあれば、この項目はアップデートが必要になるかもしれない)


ANSIエスケープシーケンスを有効化する

さて、_isatty等の方法で標準出力がコンソール(ターミナル)に繋がっていることが判明したとしよう。この時点ですぐにANSIエスケープシーケンスを出力…とできれば良いのだが、2つの理由でそれはできない:


  • 古いWindowsコンソールはANSIエスケープシーケンスに対応していない

  • 新しいWindowsコンソールではAPIを呼び出さないとANSIエスケープシーケンスが有効にならない

これらの問題は、SetConsoleMode APIを呼び出すことで解決される。具体的には、SetConsoleModeに定数ENABLE_VIRTUAL_TERMINAL_PROCESSINGを含む値を渡してやることで、


  • 新しいWindowsコンソールではANSIエスケープシーケンスが有効になる

  • 古いWindowsコンソールではAPI呼び出しが失敗し、GetLastError()ERROR_INVALID_PARAMETER (0x57)を返す

となる。

ファイル記述子からWindows API呼び出しに使うHANDLEを取得するには、_get_osfhandle関数を使う。

コードで書くなら

// stream に対してANSIエスケープシーケンスを有効化

// 成功すれば true, 失敗した場合は false を返す
bool enable_virtual_terminal_processing(FILE *stream) {
HANDLE handle = (HANDLE)_get_osfhandle(_fileno(stream));
DWORD mode = 0;
if (!GetConsoleMode(handle, &mode)) {
// 失敗
return false;
}
if (!SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
// 失敗
// 古いWindowsコンソールの場合は GetLastError() == ERROR_INVALID_PARAMETER
return false;
}
return true;
}

となるだろう。

なお、ANSIエスケープシーケンスに最初から対応している代替コンソールエミュレーターであれば、わざわざ「有効化する」手順は必要ない。


まとめ

まとめると、「WindowsでANSIエスケープシーケンスが使用可能か判断し、可能であれば出力するメッセージを色づけする」プログラムの概観は次のようになる:

#include <stdio.h>

#include <io.h>
#include <windows.h>

bool is_mintty(FILE *stream) {
// 略
}

// Windows標準のコンソールにおいて、ANSIエスケープシーケンスを有効化する
bool enable_virtual_terminal_processing(FILE *stream) {
// 略
}

// ConEmuやANSICON等、ANSIエスケープシーケンスを解釈するコンソールエミュレーターの下で実行されているか確認する
bool is_3rdparty_console(FILE *stream) {
const char *s = getenv("ConEmuANSI");
if (s && strcmp(s, "ON") == 0) {
// ConEmu
return true;
} else if (getenv("ANSICON") != NULL) {
// ansicon
return true;
} else if (is_mintty(stream)) {
// MinTTY
return true;
}
return false;
}

int main(int argc, char *argv[]) {
if (_isatty(_fileno(stdout)) || is_mintty(stdout)) {
fputs("Detected a TTY\n", stderr);
if (is_3rdparty_console(stdout) || enable_virtual_terminal_processing(stdout)) {
fputs("\033[31mRED \033[32mGREEN \033[34mBLUE\033[0m\n", stdout);
} else {
fputs("Failed to enable virtual terminal\n", stdout);
}
} else {
fputs("Not a TTY\n", stderr);
}
}

この記事に書いたテクニックは、拙作LaTeX処理自動化ツールcluttexで利用している:isatty.lua

リンク集: