Windows
console

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

リンク集: