コマンドラインで動作するプログラムを書く場合、文字に色を付けると目立つので見栄えが良い。
文字に色をつける方法として、Unixの世界ではANSIエスケープシーケンスを使うのが一般的である。しかし、Windowsの世界ではANSIエスケープシーケンスは長らく二級市民だった。
この記事では、2018年現在の、WindowsでANSIエスケープシーケンスを使うための方法をまとめてみる。
Unixの場合
一応、Unixの場合をおさらいしておく。
Unixで使われるターミナルエミュレーターは、まず間違いなくANSIエスケープシーケンスに対応している。
ただし、ANSIエスケープシーケンスを使った色付けが有効なのは、ターミナルが相手の場合である。標準出力をリダイレクトしたり、パイプで流したりする場合は色付けをしないようにしたい。そこで、「標準出力がターミナルに繋がっているか」を検出する必要がある。
標準出力がターミナルに繋がっているか検出するには、<unistd.h>
のisatty
関数を使う。isatty
関数の引数はファイル記述子 (file descriptor) なので、C言語のファイルポインタFILE *
を渡したい場合は<stdio.h>
のfileno
関数を使って変換してやる。
int isatty(int fd);
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ライクな関数を提供しており、その中にisatty
とfileno
の対応物もある。ただし、「C言語の標準ではない」ことを強調してか、先頭にアンダースコアをつけて_isatty
, _fileno
という名前になっている。
int _isatty(int fd);
int _fileno(FILE *stream);
Win32 API流に言えば、GetFileType
関数がFILE_TYPE_CHAR
を返すことを確認すれば良いだろう。
コンソールエミュレーターの検出
Windows標準のコンソールエミュレーターの代替物として、ConEmuやConsoleZ等がある。こういうエミュレーターでは、後述するMinTTYを除いて_isatty
が真を返すようだ。
これらのコンソールエミュレーター等を個別に検出したい場合は、環境変数をチェックすればよい。
- ConEmuの場合、
ConEmuANSI
がON
かOFF
- ConsoleZの場合、
ConsoleZVersion
など- 環境変数周りのソースコード
- ConsoleZ自体はANSIエスケープシーケンスに対応しない(Windows標準のコンソールと同等)ので、わざわざこれを検出する意義は薄い
- ansiconの場合、
ANSICON
やANSICON_VER
など
最近は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
リンク集: