【C#】コンソールアプリで入力された文字を非表示にしてReadLineする

# 動機：パスワードをユーザーに入力させたい
パスワードとかをユーザーに入力させたいときに、`System.Console.ReadLine`メソッドだと画面にそのまま表示されてしまいます。
画面の目の前にいる人には見せても別にオッケーだよ！って考え方もあるようですが、個人的には隠したいのです。
標準の`System.Console`クラスにはどうもそういう機能は無いようです。無いんだ…そっかぁ…
`Console.ReadPassword`メソッドとかがあればいいのに…
誰かが気の利いたライブラリを作ってて、私が知らないだけで実はデファクトスタンダードなんですなんてことがあるかもしれませんが…
ともかく、大したコードにはならなさそうなので、参考文献をコピペしながらちょっと自分好みに手を加えてみることにします。
# 参考文献
- [エコーバックせずに文字列を入力する](https://smdn.jp/programming/netfx/tips/console_input_without_echo_back/)
- [C#でコンソールにパスワード入力する際のマスキング処理の実装例のご紹介](https://rainbow-engine.com/csharp-writeline-password/)
- [コンソールでのパスワード入力](https://blog.recyclebin.jp/archives/373)
# ソースの概要
- 基本的には参考文献のとおりのコードだよ
- エコーバック無しでReadLineするメソッド`NonvisibleReadline()`を作ってみたよ
- 好きな文字でシーリングできるオーバーロード`NonvisibleReadline(char SealingChar)`も作ってみたよ
  - シーリング文字に全角文字を指定すると、バックスペースを入力したときに文字の半分だけ消えるバグがあるけど、面倒くさいから放置したよ
- 非同期で実行して、外部からキャンセルできるようにもしてみたよ
- 大したことはやっていないので、適当に煮るなり焼くなりしてね
# ソース
```cs
using System;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleUtilsLib
{
    public static class ConsoleUtils
    {
        /// <summary>
        /// エコーバック無しのReadLine
        /// </summary>
        /// <returns>入力された文字列を返す。ESCが押された場合はその時点でnullを返す。</returns>
        /// <exception cref="InvalidOperationException"/>
        /// <exception cref="IOException"/>
        public static string? NonvisibleReadline()
        {
            return NonvisibleReadline(new CancellationToken());
        }
        /// <summary>
        /// エコーバック無しのReadLine
        /// </summary>
        /// <returns>入力された文字列を返す。ESCが押された場合はその時点でnullを返す。</returns>
        /// <param name="SealingChar">隠すときの文字</param>
        /// <exception cref="InvalidOperationException"/>
        /// <exception cref="IOException"/>
        public static string? NonvisibleReadline(char SealingChar)
        {
            return NonvisibleReadline(new CancellationToken(), SealingChar);
        }
        private static string? NonvisibleReadline(CancellationToken cancellationToken, char? SealingChar = null)
        {
            // 入力された文字を格納するためのバッファを用意
            var password = new StringBuilder();

            while (!cancellationToken.IsCancellationRequested)
            {
                // キー入力が来るまでなにもしない
                if (!Console.KeyAvailable)
                    continue;
                // 入力されたキー情報を読む(エコーバックはしない)
                var keyinfo = Console.ReadKey(true);

                switch (keyinfo.Key)
                {
                    case ConsoleKey.Escape:
                        // ESCキーが押された場合は、入力がキャンセルされたものとしてnullを返す
                        Console.WriteLine();
                        return null;

                    case ConsoleKey.Enter:
                        // Enterキーが押された場合は、入力が確定されたものとしてバッファに格納した文字列を返す
                        Console.WriteLine();
                        return password.ToString();

                    case ConsoleKey.Backspace:
                        // BackSpaceキーが押された場合は、バッファから最後の一文字を削除する
                        if (0 < password.Length)
                        {
                            password.Length -= 1;
                            if (SealingChar is not null) Console.Write("\b \b"); // SealingCharがあるときは最後の1文字をスペースで消す
                        }
                        else
                            // 削除できる文字がない場合は、ビープ音を鳴らす
                            Console.Beep();
                        break;

                    default:
                        if (Char.IsControl(keyinfo.KeyChar)) // コントロールキーやファンクションキーが入力された場合は、ビープ音を鳴らす
                            Console.Beep();
                        else
                        {
                            password.Append(keyinfo.KeyChar);
                            Console.Write(SealingChar);
                        }
                        break;
                }
            }
            return null; // トークンでキャンセルされたときもnullを返す
        }
        public static Task<string?> NonvisibleReadlineAsync()
        {
            return NonvisibleReadlineAsync(new CancellationToken(), null);
        }
        public static Task<string?> NonvisibleReadlineAsync(char SealingChar)
        {
            return NonvisibleReadlineAsync(new CancellationToken(), SealingChar);
        }
        public static Task<string?> NonvisibleReadlineAsync(CancellationToken cancellationToken)
        {
            return NonvisibleReadlineAsync(cancellationToken, null);
        }
        public static Task<string?> NonvisibleReadlineAsync(CancellationToken cancellationToken, char SealingChar)
        {
            return NonvisibleReadlineAsync(cancellationToken, (char?)SealingChar);
        }
        private static Task<string?> NonvisibleReadlineAsync(CancellationToken cancellationToken, char? SealingChar)
        {
            return Task.Run(() => NonvisibleReadline(cancellationToken, SealingChar));
        }
    }
}
```
# 感想
- 参考文献の時代とは違って、`keyinfo.KeyChar`は`Shift`や`Caps Lock`を考慮されて入力すべき文字が入っているようで、便利になったなあ…とか思いました。
- BeepのON,OFFも指定できるようにしてもいいかもなあとか。
- オーバーロードで書いちゃったけど、省略可能引数でやっても良かったなあ