小窍门
本文是 “基础知识 ”部分的一部分,为至少了解一种编程语言且正在学习 C# 的开发人员编写。 如果你不熟悉编程,请 从入门开始。 如果需要全面的库覆盖范围,请参阅 System.CommandLine 库文档。
该 System.CommandLine 库处理命令行分析、帮助文本生成和输入验证,以便你可以专注于应用的逻辑。 在本教程中,你将生成一个任务跟踪器 CLI,用于演示核心概念:命令、子命令、选项和参数。
在本教程中,你将了解:
- 使用
System.CommandLine包创建基于文件的应用。 - 使用类型化值定义选项和参数。
- 生成子命令并向其附加选项和参数。
- 通过操作处理每个子命令。
- 使用不同的命令行输入测试应用。
先决条件
- 安装 .NET 10 SDK 或更高版本。
创建应用
首先创建基于文件的 C# 程序并添加包 System.CommandLine 。
创建包含以下内容的文件
TaskCli.cs:#!/usr/bin/env dotnet通过
#!(shebang) 行,可以直接在 Unix 系统上运行文件。 在 Windows 上,运行带有dotnet run TaskCli.cs的文件。请加入
System.CommandLine包指令和所需的using语句:#:package System.CommandLine@2.0.0using 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 本身解释。
每行都使用多个命令行概念:
-
子命令 是告知应用要执行的操作的谓词。 任务跟踪器有四个:
add、list和completeremove。 每个子命令都可以定义其自己的参数。 -
参数 是子命令后面的位置值。 在
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 abc,System.CommandLine会报告解析错误,而不是将不良数据传递给代码。 你将在接下来的步骤中看到此模式与Option<bool>Option<Priority>Option<DateOnly?>System.CommandLine.Argument<T>。 有关泛型的详细信息,请参阅 Generics。
定义选项。 每个
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上提供默认值,使用户可以省略该选项。定义参数。 每个
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 知道哪些参数属于何处。
创建四个子命令。 每个命令都获取其自己的选项和参数组合:
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 } };组装根命令。 这个 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 提供对已解析值的访问权限。
将
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}"); } } });设置
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。
设置
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; });设置
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; });分析命令行并调用匹配的操作:
return rootCommand.Parse(args).Invoke();rootCommand.Parse(args)将输入分析为 a ParseResult,并.Invoke()运行匹配命令的操作。 返回值为退出代码(0 表示成功)。
添加支持类型和数据助手
应用需要一些支持部分:本地函数、一个enum、一个record和一个序列化上下文。 以下各节介绍每个概念,说明选择它的原因,并显示代码。 应用使用 System.Text.Json 将任务存储为 JSON(JavaScript 对象表示法)。 基于文件的应用要求在所有顶级语句和本地函数之后显示类型声明。
添加加载和保存任务的本地函数。 local 函数是在另一个函数(包括其他本地函数内)内声明的方法。 本地函数使帮助程序逻辑靠近调用它的代码,这可提高可读性,因为读取器不必跳转到单独的类或文件来了解流。 在这里,
LoadTasks并SaveTasks封装多个命令操作共享的文件 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); }在文件末尾添加
Priority枚举和TaskItem记录:public enum Priority { Low, Medium, High }一
enum个值类型,用于定义由整型类型支持的固定命名常量集。 可以使用纯整数(0、1、2)来表示优先级级别,但enum是更好的选择有多个原因:编译器将赋值限制为已定义名称,因此拼写错误会导致编译时发生Hihg错误而不是无提示 bug;使代码中的名称Low、Medium和High更具可读性;此外,System.CommandLine自动验证用户输入是否符合枚举成员,从而实现免费的输入检查。public record TaskItem(int Id, string Description, Priority Priority, DateOnly? Due, bool IsComplete);编译器为一种
record类型提供基于值的相等性和非破坏性突变支持。record适合TaskItem,因为任务数据是简单状态,没有复杂的行为——你是根据任务的值而不是引用标识来比较任务的。 编译器从构造函数参数生成Equals、GetHashCode和ToStringwith表达式支持,因此无需编写样本即可获得正确的相等性检查、轻松调试输出和不可变更新。 由于记录在默认情况下是不可变的,因此使用with表达式生成修改的副本(正如complete操作所做的那样),而不是改变原始副本,从而防止不同操作读取和写入同一列表时出现意外副作用。为 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) 编译。
测试应用程序
使用不同的输入运行应用,以练习每个子命令。
查看自动生成的帮助:
dotnet run TaskCli.cs -- --helpDescription: 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添加具有不同选项的任务:
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列出任务并用于
--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完成任务,然后默认验证列表筛选器已完成的任务:
dotnet run TaskCli.cs -- complete 2 dotnet run TaskCli.cs -- listCompleted task 2: Review pull request [ ] 1: Write documentation [ ] 3: Fix build errors使用
--all来包括已完成的任务。dotnet run TaskCli.cs -- list --all[ ] 1: Write documentation [✓] 2: Review pull request [ ] 3: Fix build errors删除任务:
dotnet run TaskCli.cs -- remove 3Removed task 3: Fix build errors
清理资源
任务跟踪器将数据存储在本地应用程序数据文件夹下的 JSON 文件中。
taskcli-sample删除文件夹以删除示例数据:
-
Windows:删除
%LOCALAPPDATA%\taskcli-sample。 -
macOS:删除
~/Library/Application Support/taskcli-sample。 -
Linux:删除
~/.local/share/taskcli-sample。