26
18

More than 5 years have passed since last update.

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

Posted at

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

文字に色をつける方法として、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

リンク集:

26
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
18