教程:System.CommandLine 入门
重要
System.CommandLine
目前为预览版,本文档适用于版本 2.0 beta 4。
一些信息与预发行产品相关,相应产品在发行之前可能会进行重大修改。 对于此处提供的信息,Microsoft 不作任何明示或暗示的担保。
本教程演示如何创建使用 System.CommandLine
库的 .NET 命令行应用。 首先,创建一个包含一个选项的简单根命令。 然后,在该基础上进行添加,从而创建一个更复杂的应用,其中包含多个子命令和每个命令的不同选项。
在本教程中,你将了解:
- 创建命令、选项和参数。
- 为选项指定默认值。
- 将选项和参数分配给命令。
- 将选项以递归方式分配给命令下的所有子命令。
- 使用多个级别的嵌套子命令。
- 为命令和选项创建别名。
- 使用
string
、string[]
、int
、bool
、FileInfo
和枚举选项类型。 - 将选项值绑定到命令处理程序代码。
- 使用自定义代码分析和验证选项。
先决条件
- 代码编辑器,如使用 C# 扩展的 Visual Studio Code。
- .NET 6 SDK。
或
- 已安装“.NET 桌面开发”工作负载的 Visual Studio 2022。
创建应用
创建一个名为“scl”的 .NET 6 控制台应用项目。
为项目创建一个名为 scl 的文件夹,然后在新文件夹中打开一个命令提示符。
运行以下命令:
dotnet new console --framework net6.0
安装 System.CommandLine 包
运行以下命令:
dotnet add package System.CommandLine --prerelease
--prerelease
选项是必需的,因为库仍为 beta 版本。
将 Program.cs 的内容替换为以下代码:
using System.CommandLine; namespace scl; class Program { static async Task<int> Main(string[] args) { var fileOption = new Option<FileInfo?>( name: "--file", description: "The file to read and display on the console."); var rootCommand = new RootCommand("Sample app for System.CommandLine"); rootCommand.AddOption(fileOption); rootCommand.SetHandler((file) => { ReadFile(file!); }, fileOption); return await rootCommand.InvokeAsync(args); } static void ReadFile(FileInfo file) { File.ReadLines(file.FullName).ToList() .ForEach(line => Console.WriteLine(line)); } }
前面的代码:
创建一个名为
--file
的 FileInfo 类型的选项,并将其分配给根命令:var fileOption = new Option<FileInfo?>( name: "--file", description: "The file to read and display on the console."); var rootCommand = new RootCommand("Sample app for System.CommandLine"); rootCommand.AddOption(fileOption);
指定
ReadFile
是在调用根命令时调用的方法:rootCommand.SetHandler((file) => { ReadFile(file!); }, fileOption);
调用根命令时显示指定文件的内容:
static void ReadFile(FileInfo file) { File.ReadLines(file.FullName).ToList() .ForEach(line => Console.WriteLine(line)); }
测试应用程序
可以使用以下任一方法在开发命令行应用时进行测试:
运行
dotnet build
命令,然后在 scl/bin/Debug/net6.0 文件夹中打开命令提示符以运行可执行文件:dotnet build cd bin/Debug/net6.0 scl --file scl.runtimeconfig.json
使用
dotnet run
并向应用(而不是向run
命令)传递选项值,具体做法是将值包含在--
后面,如以下示例所示:dotnet run -- --file scl.runtimeconfig.json
在 .NET 7.0.100 SDK 预览中,可以通过运行命令
dotnet run --launch-profile <profilename>
来使用 launchSettings.json 文件的commandLineArgs
。将项目发布到文件夹,在该文件夹中打开命令提示符,并运行可执行文件:
dotnet publish -o publish cd ./publish scl --file scl.runtimeconfig.json
在 Visual Studio 2022 中,从菜单中选择“调试”>“调试属性”,然后在“命令行参数”框中输入选项和参数。 例如:
然后运行该应用,例如通过按 Ctrl+F5 运行。
本教程假定你使用的这些选项中的第一个。
运行应用时,它将显示 --file
选项指定的文件的内容。
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
}
}
}
帮助输出
System.CommandLine
自动提供帮助输出:
scl --help
Description:
Sample app for System.CommandLine
Usage:
scl [options]
Options:
--file <file> The file to read and display on the console.
--version Show version information
-?, -h, --help Show help and usage information
版本输出
System.CommandLine
自动提供版本输出:
scl --version
1.0.0
添加子命令和选项
本部分的操作:
- 创建更多选项。
- 创建子命令。
- 将新选项分配给新的子命令。
可以通过新选项配置前景文本色和背景文本色以及读数速度。 这些功能将用于读取 Teleprompter 控制台应用教程中的一组引号。
将此示例的 GitHub 存储库中的 sampleQuotes.txt 文件复制到项目目录中。 有关如何下载文件的信息,请参阅示例和教程中的说明。
打开项目文件,并在结束
</Project>
标记之前添加<ItemGroup>
元素:<ItemGroup> <Content Include="sampleQuotes.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
添加此标记将导致在生成应用时将文本文件复制到 bin/debug/net6.0 文件夹中。 因此,在该文件夹中运行可执行文件时,可以通过名称访问该文件,而无需指定文件夹路径。
在 Program.cs 中,在创建该
--file
选项的代码之后,创建用于控制读数速度和文本颜色的选项:var delayOption = new Option<int>( name: "--delay", description: "Delay between lines, specified as milliseconds per character in a line.", getDefaultValue: () => 42); var fgcolorOption = new Option<ConsoleColor>( name: "--fgcolor", description: "Foreground color of text displayed on the console.", getDefaultValue: () => ConsoleColor.White); var lightModeOption = new Option<bool>( name: "--light-mode", description: "Background color of text displayed on the console: default is black, light mode is white.");
在创建根命令的行之后,删除向其添加
--file
选项的行。 要将该行删除,因为要将它添加到新的子命令。var rootCommand = new RootCommand("Sample app for System.CommandLine"); //rootCommand.AddOption(fileOption);
在创建根命令的行后,创建一个
read
子命令。 向此子命令添加选项,并向根命令添加子命令。var readCommand = new Command("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.AddCommand(readCommand);
将
SetHandler
代码替换为以下新子命令的SetHandler
代码:readCommand.SetHandler(async (file, delay, fgcolor, lightMode) => { await ReadFile(file!, delay, fgcolor, lightMode); }, fileOption, delayOption, fgcolorOption, lightModeOption);
由于根命令不再需要处理程序,因此不再对根命令调用
SetHandler
。 当命令具有子命令时,在调用命令行应用时通常必须指定其中一个子命令。将
ReadFile
处理程序方法替换为以下代码:internal static async Task ReadFile( FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; List<string> lines = File.ReadLines(file.FullName).ToList(); foreach (string line in lines) { Console.WriteLine(line); await Task.Delay(delay * line.Length); }; }
现在,应用如下所示:
using System.CommandLine;
namespace scl;
class Program
{
static async Task<int> Main(string[] args)
{
var fileOption = new Option<FileInfo?>(
name: "--file",
description: "The file to read and display on the console.");
var delayOption = new Option<int>(
name: "--delay",
description: "Delay between lines, specified as milliseconds per character in a line.",
getDefaultValue: () => 42);
var fgcolorOption = new Option<ConsoleColor>(
name: "--fgcolor",
description: "Foreground color of text displayed on the console.",
getDefaultValue: () => ConsoleColor.White);
var lightModeOption = new Option<bool>(
name: "--light-mode",
description: "Background color of text displayed on the console: default is black, light mode is white.");
var rootCommand = new RootCommand("Sample app for System.CommandLine");
//rootCommand.AddOption(fileOption);
var readCommand = new Command("read", "Read and display the file.")
{
fileOption,
delayOption,
fgcolorOption,
lightModeOption
};
rootCommand.AddCommand(readCommand);
readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
{
await ReadFile(file!, delay, fgcolor, lightMode);
},
fileOption, delayOption, fgcolorOption, lightModeOption);
return rootCommand.InvokeAsync(args).Result;
}
internal static async Task ReadFile(
FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
{
Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
Console.ForegroundColor = fgColor;
List<string> lines = File.ReadLines(file.FullName).ToList();
foreach (string line in lines)
{
Console.WriteLine(line);
await Task.Delay(delay * line.Length);
};
}
}
测试新的子命令
现在,如果尝试在不指定子命令的情况下运行应用,将收到一条错误消息,后跟一个帮助消息,其中指定了可用的子命令。
scl --file sampleQuotes.txt
'--file' was not matched. Did you mean one of the following?
--help
Required command was not provided.
Unrecognized command or argument '--file'.
Unrecognized command or argument 'sampleQuotes.txt'.
Description:
Sample app for System.CommandLine
Usage:
scl [command] [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
Commands:
read Read and display the file.
子命令 read
的帮助文本显示四个选项可用。 它显示枚举的有效值。
scl read -h
Description:
Read and display the file.
Usage:
scl read [options]
Options:
--file <file> The file to read and display on the console.
--delay <delay> Delay between lines, specified as milliseconds per
character in a line. [default: 42]
--fgcolor Foreground color of text displayed on the console.
<Black|Blue|Cyan|DarkBlue|DarkCyan|DarkGray|DarkGreen|Dark [default: White]
Magenta|DarkRed|DarkYellow|Gray|Green|Magenta|Red|White|Ye
llow>
--light-mode Background color of text displayed on the console:
default is black, light mode is white.
-?, -h, --help Show help and usage information
运行仅指定 --file
选项的子命令 read
,将获得其他三个选项的默认值。
scl read --file sampleQuotes.txt
每个字符的默认延迟为 42 毫秒,导致读数速度缓慢。 可以通过将 --delay
设置为较小的值来加快速度。
scl read --file sampleQuotes.txt --delay 0
可以使用 --fgcolor
和 --light-mode
来设置文本颜色:
scl read --file sampleQuotes.txt --fgcolor red --light-mode
为 --delay
提供一个无效值无效,将收到错误消息:
scl read --file sampleQuotes.txt --delay forty-two
Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'.
为 --file
提供一个无效值,将出现异常:
scl read --file nofile
Unhandled exception: System.IO.FileNotFoundException:
Could not find file 'C:\bin\Debug\net6.0\nofile'.
添加子命令和自定义验证
本部分创建应用的最终版本。 完成后,应用将有以下命令和选项:
- 带有名为
--file
的全局*选项的根命令quotes
命令- 带有名为
--delay
、--fgcolor
和--light-mode
的选项的read
命令 - 带有名为
quote
和byline
的参数的add
命令 - 带有名为
--search-terms
的选项的delete
命令
- 带有名为
*全局选项可用于它分配到的命令,并以递归方式应用于所有子命令。
下面是使用其选项和参数调用每个可用命令的示例命令行输入:
scl quotes read --file sampleQuotes.txt --delay 40 --fgcolor red --light-mode
scl quotes add "Hello world!" "Nancy Davolio"
scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"
在 Program.cs 中,将创建
--file
选项的代码替换为以下代码:var fileOption = new Option<FileInfo?>( name: "--file", description: "An option whose argument is parsed as a FileInfo", isDefault: true, parseArgument: result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } string? filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.ErrorMessage = "File does not exist"; return null; } else { return new FileInfo(filePath); } });
此代码使用 ParseArgument<T> 来提供自定义解析、验证和错误处理。
如果不使用此代码,则会报告缺少的文件,其中包含异常和堆栈跟踪。 在此代码中,会显示指定的错误消息。
此代码还指定默认值,这就是它将
isDefault
设置为true
的原因。 如果未将isDefault
设置为true
,则在没有为--file
提供任何输入的情况下,将不会调用parseArgument
委托。在创建
lightModeOption
的代码之后,为add
和delete
命令添加选项和参数:var searchTermsOption = new Option<string[]>( name: "--search-terms", description: "Strings to search for when deleting entries.") { IsRequired = true, AllowMultipleArgumentsPerToken = true }; var quoteArgument = new Argument<string>( name: "quote", description: "Text of quote."); var bylineArgument = new Argument<string>( name: "byline", description: "Byline of quote.");
如果在列表中的第一个元素之后指定元素,则可以通过 AllowMultipleArgumentsPerToken 设置省略
--search-terms
选项名称。 它将使以下命令行输入示例等效:scl quotes delete --search-terms David "You can do" scl quotes delete --search-terms David --search-terms "You can do"
将创建根命令的代码和
read
命令替换为以下代码:var rootCommand = new RootCommand("Sample app for System.CommandLine"); rootCommand.AddGlobalOption(fileOption); var quotesCommand = new Command("quotes", "Work with a file that contains quotes."); rootCommand.AddCommand(quotesCommand); var readCommand = new Command("read", "Read and display the file.") { delayOption, fgcolorOption, lightModeOption }; quotesCommand.AddCommand(readCommand); var deleteCommand = new Command("delete", "Delete lines from the file."); deleteCommand.AddOption(searchTermsOption); quotesCommand.AddCommand(deleteCommand); var addCommand = new Command("add", "Add an entry to the file."); addCommand.AddArgument(quoteArgument); addCommand.AddArgument(bylineArgument); addCommand.AddAlias("insert"); quotesCommand.AddCommand(addCommand);
此代码会更改以下内容:
从
read
命令中删除--file
选项。将
--file
选项作为全局选项添加到根命令。创建
quotes
命令并将其添加到根命令。将
read
命令添加到quotes
命令,而不是添加到根命令。创建
add
和delete
命令并将其添加到quotes
命令。
结果为以下命令层次结构:
- 根命令
quotes
read
add
delete
应用现在已实现推荐模式,其中的父命令 (
quotes
) 指定区域或组,其子命令(read
、add
、delete
)为操作。全局选项将应用于命令并以递归方式应用于子命令。 由于
--file
在根命令上,它将在应用的所有子命令中自动使用。在
SetHandler
代码后,为新的子命令添加新SetHandler
代码:deleteCommand.SetHandler((file, searchTerms) => { DeleteFromFile(file!, searchTerms); }, fileOption, searchTermsOption); addCommand.SetHandler((file, quote, byline) => { AddToFile(file!, quote, byline); }, fileOption, quoteArgument, bylineArgument);
子命令
quotes
没有处理程序,因为它不是叶命令。 子命令read
、add
和delete
是quotes
下方的叶命令,并为每个命令调用SetHandler
。为
add
和delete
添加处理程序。internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); File.WriteAllLines( file.FullName, File.ReadLines(file.FullName) .Where(line => searchTerms.All(s => !line.Contains(s))).ToList()); } internal static void AddToFile(FileInfo file, string quote, string byline) { Console.WriteLine("Adding to file"); using StreamWriter? writer = file.AppendText(); writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}"); writer.WriteLine($"{Environment.NewLine}-{byline}"); writer.Flush(); }
完成的应用如下所示:
using System.CommandLine;
namespace scl;
class Program
{
static async Task<int> Main(string[] args)
{
var fileOption = new Option<FileInfo?>(
name: "--file",
description: "An option whose argument is parsed as a FileInfo",
isDefault: true,
parseArgument: result =>
{
if (result.Tokens.Count == 0)
{
return new FileInfo("sampleQuotes.txt");
}
string? filePath = result.Tokens.Single().Value;
if (!File.Exists(filePath))
{
result.ErrorMessage = "File does not exist";
return null;
}
else
{
return new FileInfo(filePath);
}
});
var delayOption = new Option<int>(
name: "--delay",
description: "Delay between lines, specified as milliseconds per character in a line.",
getDefaultValue: () => 42);
var fgcolorOption = new Option<ConsoleColor>(
name: "--fgcolor",
description: "Foreground color of text displayed on the console.",
getDefaultValue: () => ConsoleColor.White);
var lightModeOption = new Option<bool>(
name: "--light-mode",
description: "Background color of text displayed on the console: default is black, light mode is white.");
var searchTermsOption = new Option<string[]>(
name: "--search-terms",
description: "Strings to search for when deleting entries.")
{ IsRequired = true, AllowMultipleArgumentsPerToken = true };
var quoteArgument = new Argument<string>(
name: "quote",
description: "Text of quote.");
var bylineArgument = new Argument<string>(
name: "byline",
description: "Byline of quote.");
var rootCommand = new RootCommand("Sample app for System.CommandLine");
rootCommand.AddGlobalOption(fileOption);
var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
rootCommand.AddCommand(quotesCommand);
var readCommand = new Command("read", "Read and display the file.")
{
delayOption,
fgcolorOption,
lightModeOption
};
quotesCommand.AddCommand(readCommand);
var deleteCommand = new Command("delete", "Delete lines from the file.");
deleteCommand.AddOption(searchTermsOption);
quotesCommand.AddCommand(deleteCommand);
var addCommand = new Command("add", "Add an entry to the file.");
addCommand.AddArgument(quoteArgument);
addCommand.AddArgument(bylineArgument);
addCommand.AddAlias("insert");
quotesCommand.AddCommand(addCommand);
readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
{
await ReadFile(file!, delay, fgcolor, lightMode);
},
fileOption, delayOption, fgcolorOption, lightModeOption);
deleteCommand.SetHandler((file, searchTerms) =>
{
DeleteFromFile(file!, searchTerms);
},
fileOption, searchTermsOption);
addCommand.SetHandler((file, quote, byline) =>
{
AddToFile(file!, quote, byline);
},
fileOption, quoteArgument, bylineArgument);
return await rootCommand.InvokeAsync(args);
}
internal static async Task ReadFile(
FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
{
Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
Console.ForegroundColor = fgColor;
var lines = File.ReadLines(file.FullName).ToList();
foreach (string line in lines)
{
Console.WriteLine(line);
await Task.Delay(delay * line.Length);
};
}
internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
{
Console.WriteLine("Deleting from file");
File.WriteAllLines(
file.FullName, File.ReadLines(file.FullName)
.Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
}
internal static void AddToFile(FileInfo file, string quote, string byline)
{
Console.WriteLine("Adding to file");
using StreamWriter? writer = file.AppendText();
writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
writer.WriteLine($"{Environment.NewLine}-{byline}");
writer.Flush();
}
}
生成项目,然后尝试以下命令。
使用 read
命令将不存在的文件提交到 --file
,将收到错误消息,而不是异常和堆栈跟踪:
scl quotes read --file nofile
File does not exist
尝试运行子命令 quotes
,你将收到一条消息,指导你使用 read
、add
或 delete
:
scl quotes
Required command was not provided.
Description:
Work with a file that contains quotes.
Usage:
scl quotes [command] [options]
Options:
--file <file> An option whose argument is parsed as a FileInfo [default: sampleQuotes.txt]
-?, -h, --help Show help and usage information
Commands:
read Read and display the file.
delete Delete lines from the file.
add, insert <quote> <byline> Add an entry to the file.
运行子命令 add
,然后查看文本文件末尾以查看添加的文本:
scl quotes add "Hello world!" "Nancy Davolio"
从文件开头运行包含搜索字符串的子命令 delete
,然后查看文本文件的开头,以查看删除文本的位置:
scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"
注意
如果在 bin/debug/net6.0 文件夹中运行,则会在该文件夹中找到包含 add
和 delete
命令更改的文件。 项目文件夹中的文件副本保持不变。
后续步骤
在本教程中,你已创建了一个使用 System.CommandLine
的简单命令行应用。 若要详细了解库,请参见 System.CommandLine 概述。