次の方法で共有


チュートリアル: System.CommandLine を使用してコマンド ライン アプリを構築する

ヒント

この記事は、「 基礎 」セクションの一部であり、少なくとも 1 つのプログラミング言語を知り、C# を学習している開発者向けに作成されています。 プログラミングを初めて使用する場合は、「はじめに」から始 めてください。 包括的なライブラリ カバレッジが必要な場合は、 System.CommandLine ライブラリのドキュメントを参照してください

System.CommandLine ライブラリは、コマンド ライン解析、ヘルプ テキスト生成、入力検証を処理するため、アプリのロジックに集中できます。 このチュートリアルでは、コマンド、サブコマンド、オプション、引数という主要な概念を示すタスク トラッカー CLI を構築します。

このチュートリアルでは、次の操作を行います。

  • System.CommandLine パッケージを使用して、ファイルベースのアプリを作成します。
  • 型指定された値を使用してオプションと引数を定義します。
  • サブコマンドをビルドし、オプションと引数をそれらにアタッチします。
  • 各サブコマンドをアクションで処理します。
  • さまざまなコマンド ライン入力でアプリをテストします。

前提条件

アプリを作成する

まず、ファイル ベースの C# プログラムを作成し、 System.CommandLine パッケージを追加します。

  1. 次の内容を含む TaskCli.cs という名前のファイルを作成します。

    #!/usr/bin/env dotnet
    

    #! (shebang) 行を使用すると、Unix システムで直接ファイルを実行できます。 Windows では、 dotnet run TaskCli.csを使用してファイルを実行します。

  2. System.CommandLine パッケージ ディレクティブと必要なusing ステートメントを追加します。

    #:package System.CommandLine@2.0.0
    
    using System.CommandLine;
    using System.CommandLine.Parsing;
    using System.Text.Json;
    

    Important

    バージョン 2.0.0 は、執筆時点の最新バージョンです。 パッケージの NuGet ページ で最新バージョンを確認し、最新のセキュリティ修正プログラムがあることを確認します。

コマンド構造を理解する

解析コードを記述する前に、ユーザーの観点から CLI がどのように表示されるかを検討してください。 タスク トラッカーは、次の 4 つの操作をサポートします。

dotnet TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01
dotnet TaskCli.cs -- list --all
dotnet TaskCli.cs -- complete 3
dotnet TaskCli.cs -- remove 3
dotnet TaskCli.cs -- --verbose list

前の例の--後のTaskCli.csは、残りの引数はすべて、dotnet run CLI 自体によって解釈されるのではなく、アプリに渡されることをdotnetに示しています。

各行では、いくつかのコマンド ラインの概念を使用します。

  • サブコマンド は、アプリに何をすべきかを伝える動詞です。 タスク トラッカーには、 addlistcompleteremoveの 4 つがあります。 各サブコマンドは、独自のパラメーターを定義できます。
  • 引数は、 サブコマンドの後に続く位置指定値です。 add "Write documentation"では、文字列"Write documentation"はタスクの説明を指定する引数です。 complete 3では、3数はタスク ID を指定する引数です。
  • オプション は、 --プレフィックスが付いた名前付き値です。 add --priority High --due 2026-04-01では、--priority--dueの両方が独自の値を持つオプションです。 list --allでは、--all オプションは値を必要としないブール型のフラグです。
  • グローバル・オプション は、すべてのサブコマンドに適用されます。 --verbose オプションは、root コマンドで Recursive = true で定義されているため、任意のサブコマンドで動作します。 --verbose listでは、冗長オプションがサブコマンドの前に来ますが、list --verboseでも同様に機能します。

以下のセクションでは、これらの部分を下から上に構築します。 最初に、個々のオプション ( --priority--allなど) と引数 (タスクの説明や ID など) を定義します。 次に、4 つのサブコマンドを作成し、関連するオプションと引数をそれぞれにアタッチします。 次に、サブコマンドごとにアクションを接続します。 アクションは、ユーザーがそのコマンドを呼び出したときに実行されるコードです。 最後に、ルート コマンドをアセンブルし、入力を解析して、一致したアクションを呼び出します。

コマンド ライン構文の概念の詳細については、「 コマンド ライン構文の概要」を参照してください。

オプションと引数を定義する

オプションは、ユーザーが -- プレフィックスで指定する名前付き値を表します。 引数は位置値を表します。 どちらも厳密に型指定されています。 System.CommandLine は、指定した型に入力文字列を解析します。

System.CommandLine ライブラリではジェネリック型を使用して型セーフティを保証します。 Option<int>を記述する場合、山かっこ間のint型引数です。 オプションが保持する値の種類がライブラリに通知されます。 クラス自体は 型パラメーターT ( System.CommandLine.Option<T>と同様) を宣言し、インスタンスを作成するときに具象型を指定します。 ライブラリは、ユーザーの入力文字列を解析し、その型に自動的に変換します。 ユーザーが--delay abcOption<int>として提供すると、コードに不適切なデータを渡す代わりにSystem.CommandLineが解析エラーを報告します。 このパターンは、次の手順で Option<bool>Option<Priority>Option<DateOnly?>、および System.CommandLine.Argument<T> と共に表示されます。 ジェネリックの詳細については、「 Generics」を参照してください。

  1. オプションを定義します。 各 Option<T> は、値の種類、名前、および説明を指定します。 --priority オプションはenum型を使用し、有効な列挙値に対して入力がSystem.CommandLineで自動的に検証されます。

    var verboseOption = new Option<bool>("--verbose")
    {
        Description = "Show detailed output",
        Recursive = true
    };
    
    var priorityOption = new Option<Priority>("--priority")
    {
        Description = "Task priority level",
        DefaultValueFactory = _ => Priority.Medium
    };
    
    var dueOption = new Option<DateOnly?>("--due")
    {
        Description = "Due date (uses current culture date format)"
    };
    
    var allOption = new Option<bool>("--all")
    {
        Description = "Include completed tasks"
    };
    

    Recursive = true--verbose設定では、すべてのサブコマンドでオプションを使用できます。 DefaultValueFactory--priorityでは既定値が提供されるため、ユーザーはオプションを省略できます。

  2. 引数を定義します。 各 Argument<T> では、値の種類と名前を指定します。

    var descriptionArgument = new Argument<string>("description")
    {
        Description = "Task description"
    };
    
    var taskIdArgument = new Argument<int>("id")
    {
        Description = "Task ID"
    };
    

コマンドとサブコマンドをビルドする

System.CommandLine.Commandは、ユーザーが呼び出すことができるアクションを表します。 各コマンドに関連するオプションと引数を追加 System.CommandLine 、どのパラメーターがどこに属するかを把握します。

  1. 4 つのサブコマンドを作成します。 各コマンドは、オプションと引数の独自の組み合わせを取得します。

    var addCommand = new Command("add", "Add a new task")
    {
        Arguments = { descriptionArgument },
        Options = { priorityOption, dueOption }
    };
    
    var listCommand = new Command("list", "List all tasks")
    {
        Options = { allOption }
    };
    
    var completeCommand = new Command("complete", "Mark a task as complete")
    {
        Arguments = { taskIdArgument }
    };
    
    var removeCommand = new Command("remove", "Remove a task")
    {
        Arguments = { taskIdArgument }
    };
    
  2. ルート コマンドをアセンブルします。 System.CommandLine.RootCommandは CLI のエントリ ポイントです。 グローバル --verbose オプションとすべてのサブコマンドを追加します。

    var rootCommand = new RootCommand("A simple task tracker CLI")
    {
        Options = { verboseOption },
        Subcommands = { addCommand, listCommand, completeCommand, removeCommand }
    };
    

    自動生成されたヘルプ テキストには、ユーザーが TaskCli --helpを実行したときのルート コマンドの説明が表示されます。

アクションを使用してコマンドを処理する

各サブコマンドには アクションが必要です。 アクションは、ユーザーがそのコマンドを呼び出したときに実行される デリゲート です。 デリゲートは、メソッドへの参照を表す型です。 ここでは、 ラムダ式 ( => で定義されたインライン匿名関数) をデリゲートとして渡します。 SetActionを呼び出して、各アクションを割り当てます。 デリゲートは、ParseResultを介して解析された値へのアクセスを提供するGetValueを受け取ります。

  1. add コマンドのアクションを設定します。 このアクションでは、 文字列補間 (中かっこに式を埋め込む文字列$"..." )、 条件付き演算子 (?:)、および null 許容値に値があるかどうかをチェックし、それを 1 つのステップで新しい変数に割り当てる 型テスト パターン (due is DateOnly dueDate) が導入されます。

    addCommand.SetAction(parseResult =>
    {
        var description = parseResult.GetValue(descriptionArgument)!;
        var priority = parseResult.GetValue(priorityOption);
        var due = parseResult.GetValue(dueOption);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var id = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
        var task = new TaskItem(id, description, priority, due, false);
        tasks.Add(task);
        SaveTasks(tasks);
    
        Console.WriteLine($"Added task {id}: {description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {priority}");
            if (due is DateOnly dueDate)
            {
                Console.WriteLine($"  Due: {dueDate}");
            }
        }
    });
    
  2. list コマンドのアクションを設定します。 LINQ (統合言語クエリ) では、メモリ内コレクションの標準クエリ演算子が提供されます。 このアクションでは、 Enumerable.Where 条件に一致する項目のみにタスクをフィルター処理し、フィルター処理されたシーケンスをリストに具体化 Enumerable.ToList 。 その後、アクションは foreach ループ を使用して結果を反復処理し、条件付き演算子は状態シンボルを選択します。

    listCommand.SetAction(parseResult =>
    {
        var showAll = parseResult.GetValue(allOption);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var filtered = showAll ? tasks : tasks.Where(t => !t.IsComplete).ToList();
    
        if (filtered.Count == 0)
        {
            Console.WriteLine("No tasks found.");
            return;
        }
    
        foreach (var task in filtered)
        {
            var status = task.IsComplete ? "✓" : " ";
            Console.WriteLine($"  [{status}] {task.Id}: {task.Description}");
            if (verbose)
            {
                Console.WriteLine($"       Priority: {task.Priority}");
                if (task.Due is DateOnly dueDate)
                {
                    Console.WriteLine($"       Due: {dueDate}");
                }
            }
        }
    });
    

    詳細については、 LINQ を参照してください。

  3. complete コマンドのアクションを設定します。 このアクションでは、LINQ の Enumerable.FirstOrDefault を使用して次の情報を検索します。

    • マッチングタスク。
    • タスクが存在するかどうかを確認する is null パターン
    • 既存の値を最初にコピーしてから、with初期化子で設定したプロパティを適用して新しいレコード インスタンスを作成するwith (ここでは、IsComplete = true)。 レコードは既定では不変であるため、このコピーと更新のパターンは変更された値を生成する方法です。

    アクションは失敗する可能性があるため (たとえば、タスク ID が存在しません)、アクションは、アプリの終了コードになる整数のエラー コードを返します。

    completeCommand.SetAction(parseResult =>
    {
        var id = parseResult.GetValue(taskIdArgument);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var task = tasks.FirstOrDefault(t => t.Id == id);
    
        if (task is null)
        {
            Console.Error.WriteLine($"Task {id} not found.");
            return -1;
        }
    
        tasks[tasks.IndexOf(task)] = task with { IsComplete = true };
        SaveTasks(tasks);
    
        Console.WriteLine($"Completed task {id}: {task.Description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {task.Priority}");
        }
        return 0;
    });
    
  4. remove コマンドのアクションを設定します。 このアクションは、 completeと同じ検索および検証パターンに従います。 FirstOrDefaultを使用してタスクを検索し、不足しているケースを処理するis nullを確認します。

    removeCommand.SetAction(parseResult =>
    {
        var id = parseResult.GetValue(taskIdArgument);
        var verbose = parseResult.GetValue(verboseOption);
    
        var tasks = LoadTasks();
        var task = tasks.FirstOrDefault(t => t.Id == id);
    
        if (task is null)
        {
            Console.Error.WriteLine($"Task {id} not found.");
            return -1;
        }
    
        tasks.Remove(task);
        SaveTasks(tasks);
    
        Console.WriteLine($"Removed task {id}: {task.Description}");
        if (verbose)
        {
            Console.WriteLine($"  Priority: {task.Priority}");
        }
        return 0;
    });
    
  5. コマンド ラインを解析し、一致したアクションを呼び出します。

    return rootCommand.Parse(args).Invoke();
    

    rootCommand.Parse(args) は入力を ParseResultに解析し、一致 .Invoke() コマンドのアクションを実行します。 戻り値は終了コード (成功の場合は 0) です。

サポートする型とデータ ヘルパーを追加する

アプリには、 ローカル関数enumrecord、シリアル化コンテキストなど、いくつかのサポート要素が必要です。 次のセクションでは、各概念を紹介し、その理由を説明し、コードを示します。 このアプリでは 、System.Text.Json を使用してタスクを JSON (JavaScript Object Notation) として格納します。 ファイル ベースのアプリでは、すべての最上位レベルのステートメントとローカル関数の後に型宣言を表示する必要があります。

  1. タスクを読み込んで保存するローカル関数を追加します。 ローカル関数は、他の ローカル関数 内を含め、別の関数内で宣言されたメソッドです。 ローカル関数はヘルパー ロジックを呼び出すコードの近くに保持するため、リーダーがフローを理解するために別のクラスまたはファイルにジャンプする必要がないため、読みやすさが向上します。 ここでは、複数のコマンド アクションが共有するファイル I/O を LoadTasks および SaveTasks カプセル化するため、アプリは読み込み/保存ロジックを 1 回書き込んで再利用します。

    static string GetTaskFilePath() => Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "taskcli-sample",
        "tasks.json");
    
    static List<TaskItem> LoadTasks()
    {
        var path = GetTaskFilePath();
        if (!File.Exists(path))
        {
            return [];
        }
    
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize(json, TaskJsonContext.Default.ListTaskItem) ?? [];
    }
    
    static void SaveTasks(List<TaskItem> tasks)
    {
        var path = GetTaskFilePath();
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        var json = JsonSerializer.Serialize(tasks, TaskJsonContext.Default.ListTaskItem);
        File.WriteAllText(path, json);
    }
    
  2. ファイルの末尾に Priority 列挙型と TaskItem レコードを追加します。

    public enum Priority { Low, Medium, High }
    

    enumは、整数型によってサポートされる名前付き定数の固定セットを定義する値型です。 優先度レベルはプレーンな整数 (0、1、2) で表すことができますが、いくつかの理由から、 enum が適しています。コンパイラは定義された名前に割り当てを制限するため、 Hihg のような入力ミスは、サイレント バグではなくコンパイル時エラーを引き起こします。名前 LowMediumHigh はコードをより読みやすくし、列挙型メンバーに対するユーザー入力を自動的に検証 System.CommandLine 。 ので、無料の入力チェックを受け取ります。

    public record TaskItem(int Id, string Description, Priority Priority, DateOnly? Due, bool IsComplete);
    

    recordは、コンパイラが値に基づく等価性と非破壊的変異を備える型です。 タスク データは複雑な動作を伴わないプレーンな状態であるため、 recordTaskItem に適しています。参照 ID ではなく、値によってタスクを比較します。 コンパイラは、コンストラクター パラメーターから EqualsGetHashCodeToString、および with 式のサポートを生成するため、定型句を記述せずに、正しい等値チェック、デバッグ出力の簡単なデバッグ、変更できない更新を取得できます。 レコードは既定では不変であるため、 with 式を使用して、元のコピーを変更するのではなく ( complete アクションのように) 変更されたコピーを生成します。これにより、異なるアクションが同じリストを読み書きするときに偶発的な副作用を防ぐことができます。

  3. AOT と互換性のあるシリアル化の JSON シリアル化コンテキストを追加します。

    [System.Text.Json.Serialization.JsonSourceGenerationOptions(WriteIndented = true)]
    [System.Text.Json.Serialization.JsonSerializable(typeof(List<TaskItem>))]
    internal partial class TaskJsonContext : System.Text.Json.Serialization.JsonSerializerContext;
    

    このクラスでは、2 つの 属性 (宣言の上に角かっこで囲まれたメタデータ注釈) を使用します。 [JsonSourceGenerationOptions(WriteIndented = true)]属性は、読みやすくするためにインデントされた JSON を出力するようにソース ジェネレーターに指示します。 [JsonSerializable(typeof(List<TaskItem>))]属性は、シリアル化コードを作成する型をジェネレーターに指示します。 これらの属性を組み合わせることで、 ソース生成のシリアル化が可能になります。これにより、実行時のリフレクションが回避され、事前コンパイル (AOT) がサポートされます。

アプリケーションをテストする

異なる入力を使用してアプリを実行し、各サブコマンドを実行します。

  1. 自動生成されたヘルプを表示します。

    dotnet run TaskCli.cs -- --help
    
    Description:
      A simple task tracker CLI
    
    Usage:
      TaskCli [command] [options]
    
    Options:
      --verbose       Show detailed output
      -?, -h, --help  Show help and usage information
      --version       Show version information
    
    Commands:
      add <description>  Add a new task
      list               List all tasks
      complete <id>      Mark a task as complete
      remove <id>        Remove a task
    
  2. さまざまなオプションを使用してタスクを追加します。

    dotnet run TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01
    dotnet run TaskCli.cs -- add "Review pull request"
    dotnet run TaskCli.cs -- add "Fix build errors"
    
    Added task 1: Write documentation
    Added task 2: Review pull request
    Added task 3: Fix build errors
    
  3. タスクを一覧表示し、 --verbose を使用して詳細を表示します。

    dotnet run TaskCli.cs -- --verbose list
    
      [ ] 1: Write documentation
           Priority: High
           Due: 4/1/2026
      [ ] 2: Review pull request
           Priority: Medium
      [ ] 3: Fix build errors
           Priority: Medium
    
  4. タスクを完了し、既定で完了したタスクをフィルター処理するリストを確認します。

    dotnet run TaskCli.cs -- complete 2
    dotnet run TaskCli.cs -- list
    
    Completed task 2: Review pull request
      [ ] 1: Write documentation
      [ ] 3: Fix build errors
    
  5. --allを使用して、完了したタスクを含めます。

    dotnet run TaskCli.cs -- list --all
    
      [ ] 1: Write documentation
      [✓] 2: Review pull request
      [ ] 3: Fix build errors
    
  6. タスクを削除します。

    dotnet run TaskCli.cs -- remove 3
    
    Removed task 3: Fix build errors
    

リソースをクリーンアップする

タスク トラッカーは、ローカル アプリケーション データ フォルダーの下の JSON ファイルにデータを格納します。 taskcli-sample フォルダーを削除してサンプル データを削除します。

  • Windows: %LOCALAPPDATA%\taskcli-sampleを削除します。
  • macOS: ~/Library/Application Support/taskcli-sampleを削除します。
  • Linux: ~/.local/share/taskcli-sampleを削除します。