7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

c#コンソールアプリケーションで標準エラー出力に色を付ける方法

Last updated at Posted at 2023-03-09

1.【概要】

本稿のタイトルを見て 「そんなの Console.ForegroundColor とか Console.BackgroundColor を変更すれば一発じゃん?」 と思われた方、少々お待ちください。
私も最初はそう思っていたのですが、それではうまくいかないケースがあったのです。
本稿では、具体的な問題の内容と、その原因、解決策の模索について述べます。

2.【対象となる環境】

  • OS: Windows系OS
  • .NET Runtime: .NET6.0 および .NET 7.0 (.NET6.0.14 および .NET7.0.3)
  • コンソール: コマンドプロンプト および Windows ターミナル版コマンドプロンプト

3.【どうして標準エラー出力に色を付けるのか?】

この理由はアプリケーションによって様々ですが、私の場合は「重要な情報は目立つように表示したいから」です。
今開発しているアプリケーションが、処理しているデータの詳細情報やら進捗情報やらを大量にコンソール上に垂れ流しているため、1行かそこらのエラーメッセージが普通に表示されたところで気づかないケースが結構あります。
そういう場合に、エラーメッセージや警告メッセージが色付きで表示されれば、アプリケーションの利用者もすぐ気づいて対処できるわけです。

例えば、こんなプログラムの場合を考えてみます。

using System;

namespace Experiment.ConsoleStandatdErrorWithColor.Experiment03
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            PrintInformationMessage("波〇砲 エネルギー充填率 0%...");
            Console.WriteLine("寿限無 寿限無 五劫の擦り切れ");
            PrintInformationMessage("充填率 20%...");
            Console.WriteLine("海砂利水魚の水行末");
            PrintInformationMessage("充填率 40%...");
            Console.WriteLine("雲来末 風来末");
            PrintInformationMessage("充填率 60%...");
            Console.WriteLine("食う寝るところに住むところ");
            PrintInformationMessage("充填率 80%...");
            Console.WriteLine("やぶら小路の藪柑子");
            PrintInformationMessage("充填率 100%...");
            Console.WriteLine("パイポ パイポ");
            PrintInformationMessage("充填率 120%...");
            PrintWarningMessage("艦長、もう危険です!");
            PrintInformationMessage("充填率 140%...");
            Console.WriteLine("パイポのシューリンガン");
            PrintInformationMessage("充填率 160%...");
            Console.WriteLine("シューリンガンのグーリンダイ");
            PrintInformationMessage("充填率 180%...");
            Console.WriteLine("グーリンダイのポンポコピーのポンポコナの");
            PrintInformationMessage("充填率 200%...");
            Console.WriteLine("長久命の長助");
            PrintErrorMessage("宇宙戦艦ヤ〇トは轟沈しました");
        }

        private static void PrintInformationMessage(string message)
        {
            Console.Error.WriteLine(message);
        }

        private static void PrintWarningMessage(string message)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Error.WriteLine(message);
            Console.ResetColor();
        }

        private static void PrintErrorMessage(string message)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.Error.WriteLine(message);
            Console.ResetColor();
            Console.Beep();
        }
    }
}

何をしているプログラムなのか意味不明だと思う人が多いでしょうね。私もそうです。
ポイントは以下の2点です。

  • 標準出力にはメインの処理結果であるテキストを出力する。
  • 標準エラー出力には、それ以外の進捗状況や警告、エラーメッセージを出力する。

このプログラムをビルドして実行するとこんな感じでコンソールに表示されます。
qiita-01.png
こうしてみると、進捗とかはともかく、警告とかエラーとかのメッセージは気が付きやすいですね。

4.【何が問題なのか?】

ここまでは期待した通りに動作しています。
では次に標準出力をファイルにリダイレクトしてみます。
experiment03.exe > foo.txt
こんな感じで実行してみます。

foo.txtの内容はこんな感じです。

寿限無 寿限無 五劫の擦り切れ
海砂利水魚の水行末
雲来末 風来末
食う寝るところに住むところ
やぶら小路の藪柑子
パイポ パイポ
パイポのシューリンガン
シューリンガンのグーリンダイ
グーリンダイのポンポコピーのポンポコナの
長久命の長助

うん、当たり前ですね。

では、コンソールには何が表示されたでしょうか?
qiita-02.png
表示された内容自体は正しいのです。しかし色がついていません。

つまり、標準出力をリダイレクトすると標準エラー出力の色が変更されないのです

5.【原因は何か?】

まずは自分のソースコードを見直してみますが、特に変なところは見当たりません。…ないですよね?

次は、困ったときの Google 検索頼みです。
検索してみたら、 stack overflow に こんな質問が見つかりました。
質問自体は本件には直接関係ないのですが、とある回答者の対処案に対して、2012年12月に Mark Lakata さんが「stdout をリダイレクトすると色の変更が stderr 出力に表示されない」という要旨のコメントをされています。
この質問者の問題が.NETのどのバージョンで起きたのかは不明ですが、結構昔からある問題であることは確かの様です。

仕方がないので、.NET のソースを見てみることにしました。

詳細な調査結果は割愛して結論だけを述べると、System.ConsoleクラスのForegroundColorプロパティとBackgroundColorプロパティの setter の内部実装 (Windows版) が、標準出力がリダイレクトされる状況を考慮しておらず、値が設定されないことがある」 ように見えます。
おそらく .NET のバグなのではないかと思います。

私なりの調査・分析結果 (英文) に興味のある方はご覧ください。

一応 .NET に対してバグ報告はしましたが、かなり昔からある問題のようなので、修正されるかどうかは非常に疑問です。

6.【どういう対策が可能か?】

まず、一番簡単なのは .NETの修正がされるのを待つこと です。
しかし、かなりの長期間既に存在していた問題のようなので、もし 「そしたらいっそ修正しなくてももう数年修正が遅れても困る人はあんまりいないんじゃね?」 というようなことを .NET の開発者が考えたとしても私はあまり驚きません。

もう一つは、他の手段を探すことです。
ぱっと思いつくアプローチとしては、Win32 API を直呼びする方法と、ANSIエスケープコードを使用する方法があります。

6.1 Win32 API を使用して色を変更する方法

おなじみ、Win32 API のマーシャリングを利用します。私自身としてはプラットフォーム間の互換性を損なうのであまり利用したくはないのですが、背に腹は代えられません。

まず、Win32 APIを宣言するための静的変数・静的メソッドのコードを以下のようにどこかに書きます。

/// <summary>
/// 不正なハンドルの値です。
/// </summary>
public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

/// <summary>
/// 標準ハンドルを取得します。
/// </summary>
/// <param name="nStdHandle">
/// 取得したいハンドルの種類です。
/// <list type="bullet">
/// <item>
/// <term>標準入力</term>
/// <description>(<see cref="uint"/>)(-10)</description>
/// </item>
/// <item>
/// <term>標準出力</term>
/// <description>(<see cref="uint"/>)(-11)</description>
/// </item>
/// <item>
/// <term>標準エラー出力</term>
/// <description>(<see cref="uint"/>)(-12)</description>
/// </item>
/// </list>
/// </param>
/// <returns>
/// <para>
/// 復帰値が <see cref="INVALID_HANDLE_VALUE"/> に等しい場合はエラーが発生したことを意味します。
/// エラーの原因は<see cref="Marshal.GetLastWin32Error"/>等で知ることができます。
/// </para>
/// <para>
/// 復帰値がそれ以外の場合は、<paramref name="nStdHandle"/> に対応するハンドルです。
/// </para>
/// </returns>
[DllImport("kernel32.dll")]
extern static IntPtr GetStdHandle(uint nStdHandle);

/// <summary>
/// コンソールに文字の前景色と背景色を設定します。
/// </summary>
/// <param name="hConsoleOutput">
/// コンソールに紐づけられたハンドルです。コンソールに紐づけられていれば標準入力/標準出力/標準エラー出力の何れのハンドルでも構いません。
/// </param>
/// <param name="wAttributes">
/// 8ビットの整数であり、下位4ビットが前景色を意味し、上位4ビットが背景色を意味します。
/// 前景色・背景色ともに<see cref="ConsoleColor"/>列挙体を整数にキャストして指定することができます。
/// </param>
/// <returns>
/// <para>
/// 復帰値が true の場合は正常復帰を意味します。
/// </para>
/// <para>
/// 復帰値が false の場合はエラー復帰を意味します。
/// エラーの原因は<see cref="Marshal.GetLastWin32Error"/>等で知ることができます。
/// </para>
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
extern static bool SetConsoleTextAttribute(IntPtr hConsoleOutput, short wAttributes);

次に使い方ですが、前景色を黄色、背景色を黒に変更したい場合は、以下のように呼び出します。
前景色だけとか背景色だけとかを変更することはできません。

var stdoutHandle = GetStdHandle(unchecked((uint)-11));
if (stdoutHandle == INVALID_HANDLE_VALUE)
    throw new Exception($"{nameof(GetStdHandle)}でエラーが発生しました。: error-code={Marshal.GetLastWin32Error()}");
var foregroundColor = ConsoleColor.Yellow;
var backgroundColor = ConsoleColor.Black;
if (!SetConsoleTextAttribute(stdoutHandle, (short)(((int)backgroundColor << 4) | (int)foregroundColor)))
    throw new Exception($"{nameof(SetConsoleTextAttribute)}でエラーが発生しました。: error-code= {Marshal.GetLastWin32Error()}");

SetConsoleTextAttribute()に渡すハンドルは 標準入力/1 標準出力/標準エラー出力の何れのハンドルでも構いません。
ただし、コンソールに紐づけられていないハンドル (つまりリダイレクトされているハンドル) を渡すとエラーとなることに注意してください。
例えば、標準出力がリダイレクトされている場合に、GetStdHandle(unchecked((uint)-11))で得たハンドルをSetConsoleTextAttribute()に渡すとエラーとなります。

6.2 ANSI エスケープコードを使用して色を変更する方法

ANSIエスケープコードというのは、コンソールを制御する特殊文字列の規約のことです。
この規約に定められた文字列をコンソールに出力するだけで、文字色の変更とか、カーソル位置の移動とか、カーソル位置から行末まで文字を消したりとか、いろいろなことができます。

この規約自体はかなり昔から規定されていて、UNIX系OSなどでは一般的に使用されていたのですが、Microsoft の OS (MS-DOS や Windows 系 OS) ではずっとサポートされていませんでした。2016年に Windows 10 バージョン 1511 になってやっとこっそりサポートされた2 らしいので、Windowsでも使用できることは広くは知られていないかもしれません。私が最近まで知らなかっただけかもしれませんが…

ともかく、Windows 10 以降であれば ANSI エスケープコードが使用できるはずなので、現実的な解決方法のはずです。
しかし、実際に利用するとなると不便な点がいくつか存在します。

6.2.1 問題点1) ANSI エスケープコードはややこしい

頑張れば覚えれないほどではないのですが、ソースコード上に書かれた ANSI エスケープコードを後から見直してそれが正しいコードかどうかぱっと見でわかるかというと、首をひねらざるを得ません。
なので、コンソールの色を表す既存の型 System.ConsoleColor と ANSI エスケープコードを対応付けるための拡張メソッドを作ってみました。

using System;

// 名前空間は適当に改変してください。
namespace Experiment.ConsoleStandatdErrorWithColor.Experiment04
{
    public static class ConsoleColorExtensions
    {
        /// <summary>
        /// コンソールの前景色を初期状態に戻す ANSI エスケープコードです。
        /// </summary>
        private const string _ansiEscapeCodeToResetForegroundColor = "\x1b[39m";

        /// <summary>
        /// コンソールの背景色を初期状態に戻す ANSI エスケープコードです。
        /// </summary>
        private const string _ansiEscapeCodeToResetBackgroundColor = "\x1b[49m";

        /// <summary>
        /// コンソールの前景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードを取得します。
        /// </summary>
        /// <param name="color">
        /// <see cref="ConsoleColor"/>型の値です。
        /// </param>
        /// <returns>
        /// コンソールの前景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードです。
        /// </returns>
        public static string ToForeGroundColorAnsiEscapeCode(this ConsoleColor color)
        {
            return
                color switch
                {
                    ConsoleColor.Black => "\x1b[30m",
                    ConsoleColor.DarkBlue => "\x1b[34m",
                    ConsoleColor.DarkGreen => "\x1b[32m",
                    ConsoleColor.DarkCyan => "\x1b[36m",
                    ConsoleColor.DarkRed => "\x1b[31m",
                    ConsoleColor.DarkMagenta => "\x1b[35m",
                    ConsoleColor.DarkYellow => "\x1b[33m",
                    ConsoleColor.Gray => "\x1b[37m",
                    ConsoleColor.DarkGray => "\x1b[90m",
                    ConsoleColor.Blue => "\x1b[94m",
                    ConsoleColor.Green => "\x1b[92m",
                    ConsoleColor.Cyan => "\x1b[96m",
                    ConsoleColor.Red => "\x1b[91m",
                    ConsoleColor.Magenta => "\x1b[95m",
                    ConsoleColor.Yellow => "\x1b[93m",
                    ConsoleColor.White => "\x1b[97m",
                    _ => _ansiEscapeCodeToResetForegroundColor,
                };
        }

        /// <summary>
        /// コンソールの背景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードを取得します。
        /// </summary>
        /// <param name="color">
        /// <see cref="ConsoleColor"/>型の値です。
        /// </param>
        /// <returns>
        /// コンソールの背景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードです。
        /// </returns>
        public static string ToBackGroundColorAnsiEscapeCode(this ConsoleColor color)
        {
            return
                color switch
                {
                    ConsoleColor.Black => "\x1b[40m",
                    ConsoleColor.DarkBlue => "\x1b[44m",
                    ConsoleColor.DarkGreen => "\x1b[42m",
                    ConsoleColor.DarkCyan => "\x1b[46m",
                    ConsoleColor.DarkRed => "\x1b[41m",
                    ConsoleColor.DarkMagenta => "\x1b[45m",
                    ConsoleColor.DarkYellow => "\x1b[43m",
                    ConsoleColor.Gray => "\x1b[47m",
                    ConsoleColor.DarkGray => "\x1b[100m",
                    ConsoleColor.Blue => "\x1b[104m",
                    ConsoleColor.Green => "\x1b[102m",
                    ConsoleColor.Cyan => "\x1b[106m",
                    ConsoleColor.Red => "\x1b[101m",
                    ConsoleColor.Magenta => "\x1b[105m",
                    ConsoleColor.Yellow => "\x1b[103m",
                    ConsoleColor.White => "\x1b[107m",
                    _ => _ansiEscapeCodeToResetBackgroundColor,
                };
        }
    }
}

使い方は以下のような感じです。
このコードは最初に示したサンプルプログラムの後半の改造版です。

private static void PrintWarningMessage(string message)
{
    // 前景色を黄色に変更するコードを標準エラー出力に出力
    Console.Error.Write(ConsoleColor.Yellow.ToForeGroundColorAnsiEscapeCode());

    Console.Error.WriteLine(message);

    // 前景色を初期状態に戻すコードを標準エラー出力に出力
    Console.Error.Write(((ConsoleColor)(-1)).ToForeGroundColorAnsiEscapeCode());
}

private static void PrintErrorMessage(string message)
{
    // 前景色を赤に変更するコードを標準エラー出力に出力
    Console.Error.Write(ConsoleColor.Red.ToForeGroundColorAnsiEscapeCode());

    Console.Error.WriteLine(message);

    // 前景色を初期状態に戻すコードを標準エラー出力に出力
    Console.Error.Write(((ConsoleColor)(-1)).ToForeGroundColorAnsiEscapeCode());
    Console.Beep();
}

この部分を最初のソースコードから置き換えれば万事OK!
…となればよかったのですが、一部そうはなっていません。
実験の結果、Windows (少なくとも Windows 10 では) のすべてのコンソールが ANSI エスケープコードに対応しているわけではないことがわかったからです。

6.2.2 問題点2) ANSI エスケープコードに未対応のコンソールがある

私が使っているOSは Windows 10 なのですが、 Windows 10 で使えるコンソールにはいくつか種類があります。
そのうち、以下の3種について実行結果を確認してみました。

  • コマンドプロンプト
  • コマンドプロンプト (Windows ターミナル版)
  • Visual Studio のデバッグモードコンソール (コンソールアプリケーションの開発中にデバッグモードで起動すると表示されるコンソール)

6.2.2.1 コマンドプロンプトの場合

まず、サンプルプログラムをコマンドプロンプト上で普通に実行した場合です。
qiita-03.png
ANSI エスケープコードが解釈されずにそのまま表示されているのがわかります。("・[93m"などの部分)

次に、試しに標準出力をリダイレクトしてみました。その表示内容が以下の通りです。
qiita-04.png
いったいどういう理由でリダイレクトしたときだけ正常に表示されるんですかね… もう意味が分かりません。
どんな実装になってるんですかね、コマンドプロンプトって。

2023年4月3日追記) Windows 10 版コマンドプロンプトは既定では非対応であるだけで、ANSI エスケープコードを認識させる方法があることがわかりました。詳細については「Windows10 版コマンドプロンプトでも ANSI エスケープコードを解釈させる方法について」を参照してください。

6.2.2.2 コマンドプロンプト (Windows ターミナル版) の場合

まずは普通に実行した場合です。
qiita-05.png
ちゃんと文字の色が変わっています。さすがに後発のアプリは違いますね。

一応、標準出力をリダイレクトした場合も試してみます。
qiita-06.png
こちらも期待した通りに表示されています。これが普通ですよね…

6.2.2.2 Visual Studio のデバッグモードコンソールの場合

以下が実行結果です。
リダイレクトは試せていません。
qiita-07.png

7.【まとめ】

ここまでのまとめです。

  • コンソールアプリケーションでConsole.ForegroubndColorConsole.BackgroubndColorを変更すれば標準エラー出力の前景色や背景色も変更できるが、Windows上でかつ標準出力がリダイレクトされていると色が変わらない。
  • おそらくは.NETのバグであるが、修正されるかどうかはわからない。
  • Win32 API を直接呼び出せば、標準出力がリダイレクトされていても標準エラー出力の色を変えることができるが、プラットフォーム間の互換性を犠牲にする。
  • ANSI エスケープコードを使用することにより、リダイレクトされていても標準エラー出力の色を変更することが出来て、しかも互換性を損なわない。

8.【追記】

2023年3月10日)

ちょっと気になって、Consoleクラスの内部実装コード (UNIX版) を調べてみました。

調べるまでもなく当たり前の話ではあったんですが、色の変更やカーソル位置の変更、その他諸々のコンソールに関する特殊操作は ANSI エスケープコードをコンソールに出力することにより実現されていました。

しかし、ソースコードを何度見ても、私にはその ANSI エスケープコードの出力先が標準出力に固定されているようにしか読めず、
え?これって、UNIX 系の .NET の実装でも、標準出力がリダイレクトされてると色の変更どころかConsoleクラスの動作が (Write()とかWriteLine()を除いて) いろいろアウトじゃね?
という怖い推測に至ってしまったのです。

あいにくと私には Linux などの UNIX 系 OS での .NET 実行環境がありませんので、この推測を検証する手段がありません。

もしこれが正しいとすると (多分正しいのですが)、 標準出力がリダイレクトされている場合に標準エラー出力に色をつけたりカーソル移動などもサポートするためには、かなり影響範囲の広い修正が必要になると思われます。
果たしてそんな修正が .NET に実施される可能性があるでしょうか?

先日のバグ報告も 「それは仕様です」 になるんだろうなぁ…
やっぱり自力での代替策を用意した方がよさそうに思えます。

2023年3月25日)

自分のWindows PCで、ubuntu 環境 (Linux on Windows) ができたので、前述の懸念を確認したみました。その結果、標準出力がリダイレクトされていると、やはりいろいろ正常に動作しませんでした。主なものは以下の通りです。

  • 標準エラー出力で出力したテキストへの装飾 (前景色/背景色の変更) がされない。
  • カーソル位置の取得/設定関係のメソッド/プロパティが正常に動作しない。
  • Console.Beep() で BEEP 音が鳴らない。
  • Console.Clear() で画面がクリアされない。

2023年4月3日)

1. Windows10 版コマンドプロンプトでも ANSI エスケープコードを解釈させる方法について

Windows10 のコマンドプロンプトはANSI エスケープコードが解釈されないと述べましたが、既定では解釈されないだけ であり、ANSIエスケープコードを解釈させる方法があることがわかりました。

Windows 10版コマンドプロンプトでも、以下のように GetConsoleMode / SetConsoleMode Win32 API を一度だけ使用すれば、それ以降は ANSI エスケープコードが解釈されるようになりました。

private static readonly uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public extern static bool GetConsoleMode(IntPtr hConsoleHandle, out uint mode);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public extern static bool SetConsoleMode(IntPtr hConsoleHandle, uint mode);

......

// consoleOutputHandle にはコンソールに紐づけられているハンドルが設定済みとする

// 現在のコンソールモード(フラグ)を取得する
GetConsoleMode(consoleOutputHandle, out var mode);

// コンソールモードに ENABLE_VIRTUAL_TERMINAL_PROCESSING がセットされていないのならセットする
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
{
    SetConsoleMode(consoleOutputHandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}

この方法を使用して最初のサンプルプログラムを書き換えると以下のようになります。

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace Experiment
{
    public static class Program
    {
        private static readonly uint STD_OUTPUT_HANDLE = unchecked((uint)-11);
        private static readonly uint STD_ERROR_HANDLE = unchecked((uint)-12);
        private static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
        private static readonly uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;

        /// <summary>
        /// エスケープコードの出力先である <see cref="TextWriter"/> オブジェクト
        /// </summary>
        private static TextWriter? _escapeCodeWriter = null;

        [DllImport("kernel32.dll")]
        private extern static IntPtr GetStdHandle(uint nStdHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private extern static bool GetConsoleMode(IntPtr hConsoleHandle, out uint mode);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private extern static bool SetConsoleMode(IntPtr hConsoleHandle, uint mode);

        public static void Main()
        {
            InitializeConsole();

            PrintInformationMessage("波〇砲 エネルギー充填率 0%...");
            Console.WriteLine("寿限無 寿限無 五劫の擦り切れ");
            PrintInformationMessage("充填率 20%...");
            Console.WriteLine("海砂利水魚の水行末");
            PrintInformationMessage("充填率 40%...");
            Console.WriteLine("雲来末 風来末");
            PrintInformationMessage("充填率 60%...");
            Console.WriteLine("食う寝るところに住むところ");
            PrintInformationMessage("充填率 80%...");
            Console.WriteLine("やぶら小路の藪柑子");
            PrintInformationMessage("充填率 100%...");
            Console.WriteLine("パイポ パイポ");
            PrintInformationMessage("充填率 120%...");
            PrintWarningMessage("艦長、もう危険です!");
            PrintInformationMessage("充填率 140%...");
            Console.WriteLine("パイポのシューリンガン");
            PrintInformationMessage("充填率 160%...");
            Console.WriteLine("シューリンガンのグーリンダイ");
            PrintInformationMessage("充填率 180%...");
            Console.WriteLine("グーリンダイのポンポコピーのポンポコナの");
            PrintInformationMessage("充填率 200%...");
            Console.WriteLine("長久命の長助");
            PrintErrorMessage("宇宙戦艦ヤ〇トは轟沈しました");
        }

        /// <summary>
        /// コンソールを初期化します。
        /// </summary>
        private static void InitializeConsole()
        {
            if (OperatingSystem.IsWindows())
            {
                // Windows の場合

                // 現在使用しているコンソールがエスケープコードを解釈しない場合、エスケープコードを解釈するように設定する。

                // コンソールのハンドルを取得する

                IntPtr consoleOutputHandle;
                if (!Console.IsOutputRedirected)
                {
                    // 標準出力がリダイレクトされていない場合は、標準出力がコンソールに紐づけられているとみなす
                    consoleOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
                    _escapeCodeWriter = Console.Out;
                }
                else if (!Console.IsErrorRedirected)
                {
                    // 標準エラー出力がリダイレクトされていない場合は、標準エラー出力がコンソールに紐づけられているとみなす
                    consoleOutputHandle = GetStdHandle(STD_ERROR_HANDLE);
                    _escapeCodeWriter = Console.Error;
                }
                else
                {
                    // その他の場合はコンソールに紐づけられているハンドル/ストリームはない。
                    consoleOutputHandle = INVALID_HANDLE_VALUE;
                    _escapeCodeWriter = null;
                }

                if (consoleOutputHandle != INVALID_HANDLE_VALUE)
                {
                    // 標準出力と標準エラー出力の少なくともどちらかがコンソールに紐づけられている場合

                    // 現在のコンソールモード(フラグ)を取得する
                    if (!GetConsoleMode(consoleOutputHandle, out var mode))
                        throw new Exception("Failed to get console mode.", Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));

                    // コンソールモードに ENABLE_VIRTUAL_TERMINAL_PROCESSING がセットされていないのならセットする
                    if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
                    {
                        if (!SetConsoleMode(consoleOutputHandle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
                            throw new Exception("Failed to set console mode.", Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
                    }
                }
            }
        }

        /// <summary>
        /// 情報メッセージを表示します。
        /// </summary>
        /// <param name="message">
        /// メッセージの <see cref="string"/> オブジェクトです。
        /// </param>
        private static void PrintInformationMessage(string message)
            => Console.Error.WriteLine(message);

        /// <summary>
        /// 警告メッセージを表示します。
        /// </summary>
        /// <param name="message">
        /// メッセージの <see cref="string"/> オブジェクトです。
        /// </param>
        /// <param name="message"></param>
        private static void PrintWarningMessage(string message)
        {
            // 前景色を黄色に変更するコードを標準エラー出力に出力
            _escapeCodeWriter?.Write(ConsoleColor.Yellow.ToForeGroundColorAnsiEscapeCode());

            Console.Error.WriteLine(message);

            // 前景色を初期状態に戻すコードを標準エラー出力に出力
            _escapeCodeWriter?.Write(((ConsoleColor)(-1)).ToForeGroundColorAnsiEscapeCode());
        }

        /// <summary>
        /// エラーメッセージを表示します。
        /// </summary>
        /// <param name="message">
        /// メッセージの <see cref="string"/> オブジェクトです。
        /// </param>
        private static void PrintErrorMessage(string message)
        {
            // 前景色を赤に変更するコードを標準エラー出力に出力
            _escapeCodeWriter?.Write(ConsoleColor.Red.ToForeGroundColorAnsiEscapeCode());

            Console.Error.WriteLine(message);

            // 前景色を初期状態に戻すコードを標準エラー出力に出力
            _escapeCodeWriter?.Write(((ConsoleColor)(-1)).ToForeGroundColorAnsiEscapeCode());

            Console.Beep();
        }
    }

    public static class ConsoleColorExtensions
    {
        /// <summary>
        /// コンソールの前景色を初期状態に戻す ANSI エスケープコードです。
        /// </summary>
        private const string _ansiEscapeCodeToResetForegroundColor = "\x1b[39m";

        /// <summary>
        /// コンソールの背景色を初期状態に戻す ANSI エスケープコードです。
        /// </summary>
        private const string _ansiEscapeCodeToResetBackgroundColor = "\x1b[49m";

        /// <summary>
        /// コンソールの前景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードを取得します。
        /// </summary>
        /// <param name="color">
        /// <see cref="ConsoleColor"/>型の値です。
        /// </param>
        /// <returns>
        /// コンソールの前景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードです。
        /// </returns>
        public static string ToForeGroundColorAnsiEscapeCode(this ConsoleColor color)
            => color switch
            {
                ConsoleColor.Black => "\x1b[30m",
                ConsoleColor.DarkBlue => "\x1b[34m",
                ConsoleColor.DarkGreen => "\x1b[32m",
                ConsoleColor.DarkCyan => "\x1b[36m",
                ConsoleColor.DarkRed => "\x1b[31m",
                ConsoleColor.DarkMagenta => "\x1b[35m",
                ConsoleColor.DarkYellow => "\x1b[33m",
                ConsoleColor.Gray => "\x1b[37m",
                ConsoleColor.DarkGray => "\x1b[90m",
                ConsoleColor.Blue => "\x1b[94m",
                ConsoleColor.Green => "\x1b[92m",
                ConsoleColor.Cyan => "\x1b[96m",
                ConsoleColor.Red => "\x1b[91m",
                ConsoleColor.Magenta => "\x1b[95m",
                ConsoleColor.Yellow => "\x1b[93m",
                ConsoleColor.White => "\x1b[97m",
                _ => _ansiEscapeCodeToResetForegroundColor,
            };

        /// <summary>
        /// コンソールの背景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードを取得します。
        /// </summary>
        /// <param name="color">
        /// <see cref="ConsoleColor"/>型の値です。
        /// </param>
        /// <returns>
        /// コンソールの背景色を <see cref="ConsoleColor"/> 型で与えられた色に変更する ANSI エスケープコードです。
        /// </returns>
        public static string ToBackGroundColorAnsiEscapeCode(this ConsoleColor color)
            => color switch
            {
                ConsoleColor.Black => "\x1b[40m",
                ConsoleColor.DarkBlue => "\x1b[44m",
                ConsoleColor.DarkGreen => "\x1b[42m",
                ConsoleColor.DarkCyan => "\x1b[46m",
                ConsoleColor.DarkRed => "\x1b[41m",
                ConsoleColor.DarkMagenta => "\x1b[45m",
                ConsoleColor.DarkYellow => "\x1b[43m",
                ConsoleColor.Gray => "\x1b[47m",
                ConsoleColor.DarkGray => "\x1b[100m",
                ConsoleColor.Blue => "\x1b[104m",
                ConsoleColor.Green => "\x1b[102m",
                ConsoleColor.Cyan => "\x1b[106m",
                ConsoleColor.Red => "\x1b[101m",
                ConsoleColor.Magenta => "\x1b[105m",
                ConsoleColor.Yellow => "\x1b[103m",
                ConsoleColor.White => "\x1b[107m",
                _ => _ansiEscapeCodeToResetBackgroundColor,
            };
    }
}

このプログラムを以下の環境で実行しましたが、リダイレクトの有無にかかわらず標準エラー出力にもちゃんと色がつきました。

  • Windows10 版コマンドプロンプト
  • Windows ターミナル版コマンドプロンプト
  • Windows ターミナル版 ubuntu (Linux on Windows)

ちなみに、Win32 API の宣言を行っていますが、ちゃんと Linux 上でも実行可能です。
DllImport属性の場合は 最初の呼び出しの時点で kernel32.dll を探しに行く ので、OperatingSystem.IsWindows() == false のときには Win32 API を呼び出さない ように気をつければ同じソースコードで Linux 上でも動作します。

2. コンソール関連 Win32 API の現在と将来について

本稿の内容には直接には関連しませんが、コンソール関連 Win32 APIに関して興味深い Microsoft のドキュメントを見つけました。

自分の読解力にあんまり自信が持てないのですが、これによると以下のようなことが書かれているようです。

  • コンソールの操作については Win32 API よりも エスケープコードの使用を推奨する。
  • コンソール関連 Win32 API の廃止の予定はないが、その内部実装はエスケープコードを使用したものに置き換わりつつある。

どちらにしろエスケープコードでのやり取りになってしまうから最初から可能な限りエスケープコードを使用してください、ってことでしょうか。

  1. 2023年3月25日修正: SetConsoleTextAttribute Win32 API に標準入力のハンドルを渡すとエラーになるようです。

  2. 詳細については Wikipedia の記事 (英文) を参照してください。

7
9
2

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
7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?