MS_BuildingYourOwnCLI - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

自作CUI(CLI)の話

概要

昨今、以下の理由で、CUI(CLI)への回帰現象がある。

  • クロスプラットフォーム対応(と言うかLinux対応
  • Visual Studio Code などのエディタ系を IDE 化する際、
    最も簡潔な連携インターフェイスとして CLI が選択されている。
  • LLM 系のチャット UI(GUI)の隆盛により、
    昨今の操作性の上がった CUI の方が操作性が優れるという逆転現象が発生。

補足(「回帰」の背景): 原文の見立ては的確で、
その後さらに裏付けが増えている。CLI が選ばれる理由を整理する。

理由 内容
自動化できる GUI は人が押すしかないが、CLI はスクリプトから叩ける(CI/CD の前提)
移植しやすい 画面の描画系を持たないため、Windows / Linux / macOS で同じ実装が動く
合成できる パイプで繋げる。小さな道具を組み合わせるという UNIX の発想
リモートで使える SSH 越しに使える。コンテナの中でも使える
差分が取れる 入出力がテキストなので、diff / grep / ログが効く
LLM と相性が良い AI が「叩ける」インターフェイス。GUI は自動操作が難しい

最後の点が、原文の 3 番目の理由をさらに押し進めたものである。
エージェント型の AI ツールが普及した結果、
「AI に操作させられるか」がインターフェイス設計の評価軸に入った

GUI しかない製品は、この観点で不利になる。

なお、CUI と CLI は厳密には別である。

用語 意味
CUI(Character User Interface) 文字ベースの UI 全般(対話的な TUI も含む)
CLI(Command Line Interface) コマンドと引数で操作するインターフェイス
TUI(Text User Interface) 端末上に画面を描くvimhtop 等)

原文が「CUI(CLI)」と併記しているのは、
両方の性格を持つツールを想定しているためと読める
(後述の Sharprompt は対話的= TUI 寄り、
System.CommandLine は引数解析= CLI そのもの)。

詳細

ライブラリ、フレームワーク

何気に、何個か開発&リリースされている。

ConsoleAppFramework

https://github.com/Cysharp/ConsoleAppFramework

補足(現在の選択肢と使い分け): 3 つが挙げられているが、
役割が違うため、併用するのが実際のところである。

ライブラリ 担当する範囲
System.CommandLine 引数・オプションの解析、ヘルプ生成、補完スクリプト生成
Sharprompt 対話的な入力(選択、確認、パスワード、複数選択)
ConsoleAppFramework メソッドをそのままコマンドにする(ホスティング・DI 統合)
Spectre.Console 描画(表、ツリー、プログレス、色)+引数解析
CommandLineParser 引数解析(歴史が長い。新規なら上記を優先
【典型的な組み合わせ】
   引数解析          → System.CommandLine(または ConsoleAppFramework)
   対話的な入力      → Sharprompt
   表示(表・進捗)  → Spectre.Console

System.CommandLine の現況に注意する。
長らくプレビュー(beta)のままで、
API が破壊的に変わってきた経緯がある
(2.0 系で大きく整理された)。
バージョンを固定して使うのが無難である。

ConsoleAppFramework の v5
Source Generator による実装に変わり、
リフレクションを使わないため AOT・起動速度で有利である。

// ConsoleAppFramework v5:メソッドがそのままコマンドになる
var app = ConsoleApp.Create();
app.Add("hello", (string name, int count = 1) =>
{
    for (var i = 0; i < count; i++) Console.WriteLine($"Hello, {name}!");
});
app.Run(args);

実装

ログイン

補足(CLI のログインの定石): Azure CLI を参考にする
という原文の指摘は妥当で、ブラウザに認証を委ねるのが要点である。

【CLI のログイン(OAuth 2.0 / OIDC)】

 ① CLI が既定のブラウザを開く
      ↓
 ② ユーザーはブラウザ上で認証(MFA・SSO もここで完結)
      ↓
 ③ 認可コードが CLI に返る
      ・ローカルに立てた HTTP リスナーへリダイレクト(Loopback)
      ・またはユーザーがコードを貼り付け(Device Code Flow)
      ↓
 ④ CLI がトークンを取得し、安全な場所に保存
方式 用途
Authorization Code + PKCE(Loopback) ブラウザが開ける環境(開発者の手元)
Device Code Flow ブラウザが開けない環境(SSH 越し、コンテナ内、CI)
クライアント資格情報 人が介在しない(CI/CD、バッチ)

CLI に ID とパスワードを直接入力させてはならない
MFA・条件付きアクセス・SSO が効かなくなり、
かつパスワードがシェル履歴やプロセス一覧に残る

トークンの保存先も設計項目である。

OS 保存先
Windows DPAPIProtectedData)で暗号化してファイルに
macOS キーチェーン
Linux libsecret(GNOME Keyring 等)、なければ暗号化ファイル

平文の JSON をホーム ディレクトリに置く実装は少なくないが、
最低限ファイル パーミッションを絞る600 相当)。

進捗表示

以下のように実装できる。

補足(進捗表示の注意点): Console.SetCursorPosition
自力で描く方法は今も動くが、落とし穴がある

・出力がリダイレクトされている場合(> file、| grep)
   → カーソル操作が意味を成さない。制御文字がファイルに混入する
・CI のログ
   → 端末ではないため、\r による上書きが効かず、行が大量に増える
・幅の狭い端末
   → 折り返して表示が崩れる

対策:

// 端末かどうかを判定してから描く
if (!Console.IsOutputRedirected)
{
    Console.Write($"\r処理中... {percent,3}%");
}
else
{
    // ログ向けには、区切りの良い所で 1 行ずつ出す
    if (percent % 10 == 0) Console.WriteLine($"処理中... {percent}%");
}

Spectre.Console を使えばこの辺りは吸収される
(リダイレクト時は自動的に静かな出力に切り替わる)。

await AnsiConsole.Progress().StartAsync(async ctx =>
{
    var task = ctx.AddTask("[green]処理中[/]", maxValue: total);
    while (!task.IsFinished) { /* ... */ task.Increment(1); }
});

トピック

CMDのパス短縮

以下のコマンドで短縮可能。

PROMPT $N$G

CMD出力のクリア

以下のコマンドで CMD の出力をクリア可能。

cls

補足(PowerShell での対応): 原文の 2 つは CMD の話である。
PowerShell では以下になる。

やりたいこと CMD PowerShell
プロンプトを短くする PROMPT $N$G function prompt { "PS> " }
画面をクリア cls Clear-Host(別名 clsclear
# プロファイルに書けば恒久化される
function prompt { "$(Split-Path -Leaf $PWD)> " }

Windows Terminal を使っているなら、
oh-my-posh 等でプロンプトを整えるのが現在の主流である。

補足(CLI を自作する際の設計指針): 原文の各論を補う形で、
作法をまとめておく。

① 終了コードを正しく返す

return success ? 0 : 1;   // 0 = 成功、非 0 = 失敗

スクリプトから使われる以上、終了コードが唯一の機械可読な結果である。
ここを常に 0 にしていると、CI で失敗を検知できない。

② 標準出力と標準エラーを分ける

標準出力(stdout)… 処理の「結果」。パイプで次に渡すもの
標準エラー(stderr)… 進捗・警告・エラー。人間向けのメッセージ

進捗表示を stdout に出すと、パイプした先が壊れる

③ 機械可読な出力形式を用意する

mytool list              → 人間向けの整形された表
mytool list --output json → JSON(スクリプトから使う)

Azure CLI の --output json / --query が良い例である。

④ 対話を必須にしない

対話的な入力(Sharprompt 等)は便利だが、
必ず「引数で全部指定できる」道を残す
でないと CI やバッチから使えない。
--yes / --non-interactive のようなフラグを用意する。

⑤ 色と装飾は環境を見て切る

・出力がリダイレクトされている → 色を出さない
・環境変数 NO_COLOR が設定されている → 色を出さない

⑥ 配布形態を決める

方式 内容
.NET グローバル ツール dotnet tool install -g で入る。.NET SDK が要る
単一ファイル発行 PublishSingleFileランタイム不要(自己完結)
Native AOT 起動が速く小さい。CLI に最適だが制約あり

CLI は起動速度が体感を支配するため、
Native AOT の効果が大きい(数十 ms 対数百 ms)。

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

ただし InvariantGlobalization
カルチャ依存の処理を壊すため、
日本語を扱うなら有効化しない

参考

OSSコンソーシアム

サンプル

https://github.com/OpenTouryoProject/SampleProgram/tree/master/Other/PipesFamilyHouse

開発基盤部会 Blog

Microsoft Learn


Tags: 移行, シェル, インフラストラクチャ, Windows, .NET開発

⚠️ **GitHub.com Fallback** ⚠️