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`関数を使って変換してやる。

```c:unistd.h
int isatty(int fd);
```

```c:stdio.h
int fileno(FILE *stream);
```

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

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

```c
#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`という名前になっている。

```c:io.h
int _isatty(int fd);
```

```c:stdio.h
int _fileno(FILE *stream);
```

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

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

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

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

* ConEmuの場合、 `ConEmuANSI` が `ON` か `OFF`
    * [環境変数一覧](https://conemu.github.io/en/ConEmuEnvironment.html)
* ConsoleZの場合、 `ConsoleZVersion` など
    * [環境変数周りのソースコード](https://github.com/cbucher/console/blob/b4a5d847fefa33894b5d49f090cc295c96719925/Console/ConsoleHandler.cpp#L331)
    * ConsoleZ自体はANSIエスケープシーケンスに対応しない（Windows標準のコンソールと同等）ので、わざわざこれを検出する意義は薄い
* ansiconの場合、 `ANSICON` や `ANSICON_VER` など
    * [環境変数周りのソースコード](https://github.com/adoxa/ansicon/blob/0472db80b26761a90b3941a53c6e94ea43565c21/ANSI.c#L3867)

最近は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`関数を使う。

コードで書くなら

```cpp
// 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エスケープシーケンスが使用可能か判断し、可能であれば出力するメッセージを色づけする」プログラムの概観は次のようになる：

```cpp
#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](https://github.com/minoki/cluttex)で利用している：[isatty.lua](https://github.com/minoki/cluttex/blob/master/src/texrunner/isatty.lua)

リンク集：

- [ConEmu](https://conemu.github.io/)
- [ConsoleZ](https://github.com/cbucher/console/wiki)
    - [FAQ: Does ConsoleZ support ANSI escape sequences?](https://github.com/cbucher/console/wiki/FAQ#does-consolez-support-ansi-escape-sequences)
- [MinTTY](http://mintty.github.io/)
- [winpty](https://github.com/rprichard/winpty)
- [ANSICON](http://adoxa.altervista.org/ansicon/) ([GitHub](https://github.com/adoxa/ansicon))
- [SetConsoleMode function - Windows Console | Microsoft Docs](https://docs.microsoft.com/en-us/windows/console/setconsolemode)
- [Console Virtual Terminal Sequences - Windows Console | Microsoft Docs](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
- [visual studio - SetConsoleMode returning false when enabling ANSI color under Windows 10 - Stack Overflow](https://stackoverflow.com/questions/44482505/setconsolemode-returning-false-when-enabling-ansi-color-under-windows-10)