教程:使用 System.CommandLine 生成命令行应用

小窍门

本文是 “基础知识 ”部分的一部分,为至少了解一种编程语言且正在学习 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;
    

    重要

    版本 2.0.0 是写入时的最新版本。 检查包的 NuGet 页 以获取最新版本,以确保具有最新的安全修补程序。

了解命令结构

在编写任何分析代码之前,请从用户的角度考虑 CLI 的外观。 任务跟踪器支持四个操作:

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` 所有剩余参数都会传递给你的应用程序,而不是由 `dotnet` CLI 本身解释。

每行都使用多个命令行概念:

  • 子命令 是告知应用要执行的操作的谓词。 任务跟踪器有四个:addlistcompleteremove。 每个子命令都可以定义其自己的参数。
  • 参数 是子命令后面的位置值。 在add "Write documentation"中,"Write documentation"字符串是用于指定任务描述的参数。 在complete 3中,数字3是指定任务 ID 的参数。
  • 选项 的命名值以 --前缀 。 在add --priority High --due 2026-04-01中,--priority--due都是具有各自值的选项。 在该 list --all选项中, --all 选项是不需要值的布尔标志。
  • 全局选项 适用于每个子命令。 该 --verbose 选项在根命令 Recursive = true上定义,因此它适用于任何子命令。 在 --verbose list中,详细标志显示在子命令之前,但 list --verbose 效果同样良好。

在后面的部分中,你将从下到上构建这些部分。 首先,定义各个选项(如 --priority--all)和参数(如任务说明和 ID)。 接下来,创建四个子命令,并将相关选项和参数附加到每个子命令。 然后,为每个子命令绑定一个操作。 操作是在用户调用该命令时运行的代码。 最后,你组合根命令、分析输入并调用匹配的操作。

有关命令行语法概念的更深入介绍,请参阅 命令行语法概述

定义选项和参数

选项表示用户使用前缀指定的 -- 命名值。 参数表示位置值。 两者都是强类型的。 System.CommandLine 将输入字符串分析为指定的类型。

System.CommandLine 使用 泛型类型 来强制实施类型安全性。 编写 Option<int>时, int 尖括号之间的参数为 类型参数。 它告诉库关于选项包含的值类型的信息。 类本身声明了一个类型参数T(例如System.CommandLine.Option<T>),并在创建实例时指定具体类型。 库分析用户的输入字符串并将其自动转换为该类型。 如果用户为Option<int>提供--delay abcSystem.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. 创建四个子命令。 每个命令都获取其自己的选项和参数组合:

    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时显示根命令说明。

使用动作处理命令

每个子命令都需要一个 操作。 操作是在用户调用该命令时运行的 委托 。 委托是表示对方法的引用的类型。 在这里,你将 lambda 表达式 作为委托传递,该表达式是用 => 定义的内联匿名函数。 调用 SetAction 以分配每个操作。 委托接收一个 ParseResult ,从 GetValue 提供对已解析值的访问权限。

  1. add命令的操作设置为。 此操作引入了 字符串内插$"..." 在大括号中嵌入表达式的字符串)、 条件运算符?:)和 类型测试模式due is DateOnly dueDate),用于检查可为 null 的值是否具有值,并在一个步骤中将其分配给新变量:

    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) 将输入分析为 a ParseResult,并 .Invoke() 运行匹配命令的操作。 返回值为退出代码(0 表示成功)。

添加支持类型和数据助手

应用需要一些支持部分:本地函数、一个enum、一个record和一个序列化上下文。 以下各节介绍每个概念,说明选择它的原因,并显示代码。 应用使用 System.Text.Json 将任务存储为 JSON(JavaScript 对象表示法)。 基于文件的应用要求在所有顶级语句和本地函数之后显示类型声明。

  1. 添加加载和保存任务的本地函数。 local 函数是在另一个函数(包括其他本地函数内)内声明的方法。 本地函数使帮助程序逻辑靠近调用它的代码,这可提高可读性,因为读取器不必跳转到单独的类或文件来了解流。 在这里, LoadTasksSaveTasks 封装多个命令操作共享的文件 I/O,以便应用编写加载/保存逻辑一次并重复使用它:

    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错误而不是无提示 bug;使代码中的名称LowMediumHigh更具可读性;此外,System.CommandLine 自动验证用户输入是否符合枚举成员,从而实现免费的输入检查。

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

    编译器为一种 record 类型提供基于值的相等性和非破坏性突变支持。 record 适合 TaskItem,因为任务数据是简单状态,没有复杂的行为——你是根据任务的值而不是引用标识来比较任务的。 编译器从构造函数参数生成EqualsGetHashCodeToStringwith表达式支持,因此无需编写样本即可获得正确的相等性检查、轻松调试输出和不可变更新。 由于记录在默认情况下是不可变的,因此使用 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;
    

    此类使用两个 属性 (在声明上方的方括号中放置的元数据注释)。 该 [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