教程:System.CommandLine 入门

重要

System.CommandLine 目前为预览版,本文档适用于版本 2.0 beta 4。 一些信息与预发行产品相关,相应产品在发行之前可能会进行重大修改。 对于此处提供的信息,Microsoft 不作任何明示或暗示的担保。

本教程演示如何创建使用 System.CommandLine的 .NET 命令行应用。 首先,创建一个包含一个选项的简单根命令。 然后,在该基础上进行添加,从而创建一个更复杂的应用,其中包含多个子命令和每个命令的不同选项。

在本教程中,你将了解:

  • 创建命令、选项和参数。
  • 为选项指定默认值。
  • 将选项和参数分配给命令。
  • 将选项以递归方式分配给命令下的所有子命令。
  • 使用多个级别的嵌套子命令。
  • 为命令和选项创建别名。
  • 使用 stringstring[]intboolFileInfo 和枚举选项类型。
  • 将选项值绑定到命令处理程序代码。
  • 使用自定义代码分析和验证选项。

先决条件

创建应用

创建一个名为“scl”的 .NET 6 控制台应用项目。

  1. 为项目创建一个名为 scl 的文件夹,然后在新文件夹中打开一个命令提示符。

  2. 运行以下命令:

    dotnet new console --framework net6.0
    

安装 System.CommandLine 包

  • 运行以下命令:

    dotnet add package System.CommandLine --prerelease
    

    --prerelease 选项是必需的,因为库仍为 beta 版本。

  1. 将 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));
        }
    }
    

前面的代码:

  • 创建一个名为 --fileFileInfo 类型的选项,并将其分配给根命令

    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 中,从菜单中选择“调试”>“调试属性”,然后在“命令行参数”框中输入选项和参数。 例如:

    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 控制台应用教程中的一组引号。

  1. 将此示例的 GitHub 存储库中的 sampleQuotes.txt 文件复制到项目目录中。 有关如何下载文件的信息,请参阅示例和教程中的说明。

  2. 打开项目文件,并在结束 </Project> 标记之前添加 <ItemGroup> 元素:

    <ItemGroup>
      <Content Include="sampleQuotes.txt">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </Content>
    </ItemGroup>
    

    添加此标记将导致在生成应用时将文本文件复制到 bin/debug/net6.0 文件夹中。 因此,在该文件夹中运行可执行文件时,可以通过名称访问该文件,而无需指定文件夹路径。

  3. 在 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.");
    
  4. 在创建根命令的行之后,删除向其添加 --file 选项的行。 要将该行删除,因为要将它添加到新的子命令。

    var rootCommand = new RootCommand("Sample app for System.CommandLine");
    //rootCommand.AddOption(fileOption);
    
  5. 在创建根命令的行后,创建一个 read 子命令。 向此子命令添加选项,并向根命令添加子命令。

    var readCommand = new Command("read", "Read and display the file.")
        {
            fileOption,
            delayOption,
            fgcolorOption,
            lightModeOption
        };
    rootCommand.AddCommand(readCommand);
    
  6. SetHandler 代码替换为以下新子命令的 SetHandler 代码:

    readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
        {
            await ReadFile(file!, delay, fgcolor, lightMode);
        },
        fileOption, delayOption, fgcolorOption, lightModeOption);
    

    由于根命令不再需要处理程序,因此不再对根命令调用 SetHandler。 当命令具有子命令时,在调用命令行应用时通常必须指定其中一个子命令。

  7. 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 命令
      • 带有名为 quotebyline 的参数的 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"
  1. 在 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 委托。

  2. 在创建 lightModeOption 的代码之后,为 adddelete 命令添加选项和参数:

    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"
    
  3. 将创建根命令的代码和 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 命令,而不是添加到根命令。

    • 创建 adddelete 命令并将其添加到 quotes 命令。

    结果为以下命令层次结构:

    • 根命令
      • quotes
        • read
        • add
        • delete

    应用现在已实现推荐模式,其中的父命令 (quotes) 指定区域或组,其子命令(readadddelete)为操作。

    全局选项将应用于命令并以递归方式应用于子命令。 由于 --file 在根命令上,它将在应用的所有子命令中自动使用。

  4. SetHandler 代码后,为新的子命令添加新 SetHandler 代码:

    deleteCommand.SetHandler((file, searchTerms) =>
        {
            DeleteFromFile(file!, searchTerms);
        },
        fileOption, searchTermsOption);
    
    addCommand.SetHandler((file, quote, byline) =>
        {
            AddToFile(file!, quote, byline);
        },
        fileOption, quoteArgument, bylineArgument);
    

    子命令 quotes 没有处理程序,因为它不是叶命令。 子命令 readadddeletequotes 下方的叶命令,并为每个命令调用 SetHandler

  5. adddelete 添加处理程序。

    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,你将收到一条消息,指导你使用 readadddelete

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 文件夹中运行,则会在该文件夹中找到包含 adddelete 命令更改的文件。 项目文件夹中的文件副本保持不变。

后续步骤

在本教程中,你已创建了一个使用 System.CommandLine 的简单命令行应用。 若要详细了解库,请参见 System.CommandLine 概述