教程:入门 System.CommandLine

重要

System.CommandLine 目前为预览版,本文档适用于版本 2.0 beta 5。 某些信息与预发布产品有关,该产品在发布前可能会进行大幅修改。 Microsoft对此处提供的信息不作任何明示或暗示的保证。

本教程演示如何创建使用库的 System.CommandLine.NET 命令行应用。 首先创建一个具有一个选项的简单根命令。 然后,你将基于该基础构建,创建一个更复杂的应用,其中包含多个子命令和每个命令的不同选项。

本教程中,您将学习如何:

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

先决条件

创建应用

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

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

  2. 运行下面的命令:

    dotnet new console --framework net9.0
    

安装 System.CommandLine 包

  • 运行下面的命令:

    dotnet add package System.CommandLine --prerelease
    

    或者,在 .NET 10+ 中:

    dotnet package add System.CommandLine --prerelease
    

    --prerelease 选项是必需的,因为该库仍处于 beta 阶段。

分析参数

  1. 将 Program.cs 的内容替换为以下代码:

    using System.CommandLine;
    using System.CommandLine.Parsing;
    
    namespace scl;
    
    class Program
    {
        static int Main(string[] args)
        {
            Option<FileInfo> fileOption = new("--file")
            {
                Description = "The file to read and display on the console."
            };
    
            RootCommand rootCommand = new("Sample app for System.CommandLine");
            rootCommand.Options.Add(fileOption);
    
            ParseResult parseResult = rootCommand.Parse(args);
            if (parseResult.GetValue(fileOption) is FileInfo parsedFile)
            {
                ReadFile(parsedFile);
                return 0;
            }
            foreach (ParseError parseError in parseResult.Errors)
            {
                Console.Error.WriteLine(parseError.Message);
            }
            return 1;
        }
    
        static void ReadFile(FileInfo file)
        {
            foreach (string line in File.ReadLines(file.FullName))
            {
                Console.WriteLine(line);
            }
        }
    }
    

前面的代码:

Option<FileInfo> fileOption = new("--file")
{
    Description = "The file to read and display on the console."
};

RootCommand rootCommand = new("Sample app for System.CommandLine");
rootCommand.Options.Add(fileOption);
  • 分析 args 并检查是否为 --file 选项提供了任何值。 如果是这样,它将使用分析的值调用 ReadFile 该方法并返回 0 退出代码:
ParseResult parseResult = rootCommand.Parse(args);
if (parseResult.GetValue(fileOption) is FileInfo parsedFile)
{
    ReadFile(parsedFile);
    return 0;
}
  • 如果未提供 --file任何值,它将输出可用的分析错误并返回 1 退出代码:
foreach (ParseError parseError in parseResult.Errors)
{
    Console.Error.WriteLine(parseError.Message);
}
return 1;
  • 该方法 ReadFile 读取指定的文件并在控制台上显示其内容:
static void ReadFile(FileInfo file)
{
    foreach (string line in File.ReadLines(file.FullName))
    {
        Console.WriteLine(line);
    }
}

测试应用程序

在开发命令行应用时,可以使用以下任一方法来进行测试:

  • 运行该 dotnet build 命令,然后在 scl/bin/Debug/net9.0 文件夹中打开命令提示符以运行可执行文件:

    dotnet build
    cd bin/Debug/net9.0
    scl --file scl.runtimeconfig.json
    
  • 使用 dotnet run 选项值并将其传递给应用,而不是 run 在命令后面 --包括这些值,如以下示例所示:

    dotnet run -- --file bin/Debug/net9.0/scl.runtimeconfig.json
    

本教程假设你使用的是这些选项中的第一个。

运行应用时,它会显示由该选项指定的 --file 文件的内容。

{
  "runtimeOptions": {
    "tfm": "net9.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "9.0.0"
    }
  }
}

但是,如果你要求它通过提供 --help来显示帮助,会发生什么情况? 不会向控制台打印任何内容,因为应用尚未处理未提供且没有分析错误的情况 --file

分析参数并调用 ParseResult

System.CommandLine 允许指定在成功分析给定符号(命令、指令或选项)时调用的作。 该作是一个委托,它采用参数 System.CommandLine.ParseResult 并返回 int 退出代码(异步作也 可用)。 退出代码由 System.CommandLine.Parsing.ParseResult.Invoke 该方法返回,可用于指示命令是否已成功执行。

  1. 将 Program.cs 的内容替换为以下代码:

    using System.CommandLine;
    
    namespace scl;
    
    class Program
    {
        static int Main(string[] args)
        {
            Option<FileInfo> fileOption = new("--file")
            {
                Description = "The file to read and display on the console."
            };
    
            RootCommand rootCommand = new("Sample app for System.CommandLine");
            rootCommand.Options.Add(fileOption);
    
            rootCommand.SetAction(parseResult =>
            {
                FileInfo parsedFile = parseResult.GetValue(fileOption);
                ReadFile(parsedFile);
                return 0;
            });
    
            ParseResult parseResult = rootCommand.Parse(args);
            return parseResult.Invoke();
        }
    
        static void ReadFile(FileInfo file)
        {
            foreach (string line in File.ReadLines(file.FullName))
            {
                Console.WriteLine(line);
            }
        }
    }
    

前面的代码:

  • 指定 ReadFile调用根命令时将调用的方法:

    rootCommand.SetAction(parseResult =>
    {
        FileInfo parsedFile = parseResult.GetValue(fileOption);
        ReadFile(parsedFile);
        return 0;
    });
    
  • args分析并调用结果:

    ParseResult parseResult = rootCommand.Parse(args);
    return parseResult.Invoke();
    

运行应用时,它会显示由该选项指定的 --file 文件的内容。

如果你要求它通过提供 --help来显示帮助,会发生什么情况?

scl --help

打印以下输出:

Description:
  Sample app for System.CommandLine

Usage:
  scl [options]

Options:
  -?, -h, --help  Show help and usage information
  --version       Show version information
  --file          The file to read and display on the conso

System.CommandLine.RootCommand 默认情况下,提供 “帮助”选项“版本”选项“建议”指令ParseResult.Invoke 方法负责调用已分析符号的作。 可以是为命令显式定义的作,也可以是为其System.CommandLineSystem.CommandLine.Help.HelpOption定义的帮助作。 此外,当它检测到任何分析错误时,它会将它们输出到标准错误,输出有助于标准输出并返回 1 退出代码:

scl --invalid bla
Unrecognized command or argument '--invalid'.
Unrecognized command or argument 'bla'.

添加子命令和选项

本部分的操作:

  • 创建更多选项。
  • 创建子命令。
  • 将新选项分配给新的子命令。

通过新选项,可以配置前景和背景文本颜色以及读出速度。 这些功能将用于读取来自 Teleprompter 控制台应用教程的报价集合。

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

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

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

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

  3. Program.cs,在创建 --file 选项的代码之后,创建用于控制读取速度和文本颜色的选项:

    Option<int> delayOption = new("--delay")
    {
        Description = "Delay between lines, specified as milliseconds per character in a line.",
        DefaultValueFactory = parseResult => 42
    };
    Option<ConsoleColor> fgcolorOption = new("--fgcolor")
    {
        Description = "Foreground color of text displayed on the console.",
        DefaultValueFactory = parseResult => ConsoleColor.White
    };
    Option<bool> lightModeOption = new("--light-mode")
    {
        Description = "Background color of text displayed on the console: default is black, light mode is white."
    };
    
  4. 创建根命令的行后,删除添加 --file 选项的代码。 你在这里删除它,因为你会将其添加到新的子命令。

  5. 创建根命令的行后,创建 read 子命令。 将选项添加到此子命令(通过使用集合初始值设定项语法而不是 Options 属性),并将子命令添加到根命令。

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

    readCommand.SetAction(parseResult => ReadFile(
        parseResult.GetValue(fileOption),
        parseResult.GetValue(delayOption),
        parseResult.GetValue(fgcolorOption),
        parseResult.GetValue(lightModeOption)));
    

    不再调用 SetAction 根命令,因为根命令不再需要作。 当命令具有子命令时,通常必须在调用命令行应用时指定其中一个子命令。

  7. ReadFile 作方法替换为以下代码:

    internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        foreach (string line in File.ReadLines(file.FullName))
        {
            Console.WriteLine(line);
            Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length));
        }
    }
    

应用现在如下所示:

using System.CommandLine;

namespace scl;

class Program
{
    static int Main(string[] args)
    {
        Option<FileInfo> fileOption = new("--file")
        {
            Description = "The file to read and display on the console."
        };

        Option<int> delayOption = new("--delay")
        {
            Description = "Delay between lines, specified as milliseconds per character in a line.",
            DefaultValueFactory = parseResult => 42
        };
        Option<ConsoleColor> fgcolorOption = new("--fgcolor")
        {
            Description = "Foreground color of text displayed on the console.",
            DefaultValueFactory = parseResult => ConsoleColor.White
        };
        Option<bool> lightModeOption = new("--light-mode")
        {
            Description = "Background color of text displayed on the console: default is black, light mode is white."
        };

        RootCommand rootCommand = new("Sample app for System.CommandLine");

        Command readCommand = new("read", "Read and display the file.")
        {
            fileOption,
            delayOption,
            fgcolorOption,
            lightModeOption
        };
        rootCommand.Subcommands.Add(readCommand);

        readCommand.SetAction(parseResult => ReadFile(
            parseResult.GetValue(fileOption),
            parseResult.GetValue(delayOption),
            parseResult.GetValue(fgcolorOption),
            parseResult.GetValue(lightModeOption)));

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

    internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        foreach (string line in File.ReadLines(file.FullName))
        {
            Console.WriteLine(line);
            Thread.Sleep(TimeSpan.FromMilliseconds(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:
  -?, -h, --help  Show help and usage information
  --version       Show version 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\net9.0\nofile''.
File name: 'C:\bin\Debug\net9.0\nofile''

添加子命令和自定义验证

本部分将创建应用的最终版本。 完成后,应用将具有以下命令和选项:

  • 具有名为 recursive* 选项的 root 命令 --file
    • quotes 命令
      • read 具有命名 --delay选项、 --fgcolor--light-mode
      • add 具有命名 quote 参数和 byline
      • delete 带有命名选项的命令 --search-terms

* 递归选项可用于分配给该选项的所有子命令,并以递归方式提供给其所有子命令。

下面是使用其选项和参数调用每个可用命令的示例命令行输入:

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 选项的代码替换为以下代码:

    Option<FileInfo> fileOption = new("--file")
    {
        Description = "An option whose argument is parsed as a FileInfo",
        Required = true,
        DefaultValueFactory = result =>
        {
            if (result.Tokens.Count == 0)
            {
                return new FileInfo("sampleQuotes.txt");
    
            }
            string filePath = result.Tokens.Single().Value;
            if (!File.Exists(filePath))
            {
                result.AddError("File does not exist");
                return null;
            }
            else
            {
                return new FileInfo(filePath);
            }
        }
    };
    

    此代码用于 System.CommandLine.Parsing.ArgumentResult 提供自定义分析、验证和错误处理。

    如果没有此代码,将报告缺少的文件,并带有异常和堆栈跟踪。 在此代码中,只显示指定的错误消息。

    此代码还指定默认值,这就是为什么它设置为 DefaultValueFactory 自定义分析方法的原因。

  2. 创建lightModeOption代码后,为和命令添加选项和delete参数add

    Option<string[]> searchTermsOption = new("--search-terms")
    {
        Description = "Strings to search for when deleting entries.",
        Required = true,
        AllowMultipleArgumentsPerToken = true
    };
    Argument<string> quoteArgument = new("quote")
    {
        Description = "Text of quote."
    };
    Argument<string> bylineArgument = new("byline")
    {
        Description = "Byline of quote."
    };
    

    通过 xref:System.CommandLine.Option.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 命令替换为以下代码:

    RootCommand rootCommand = new("Sample app for System.CommandLine");
    fileOption.Recursive = true;
    rootCommand.Options.Add(fileOption);
    
    Command quotesCommand = new("quotes", "Work with a file that contains quotes.");
    rootCommand.Subcommands.Add(quotesCommand);
    
    Command readCommand = new("read", "Read and display the file.")
    {
        delayOption,
        fgcolorOption,
        lightModeOption
    };
    quotesCommand.Subcommands.Add(readCommand);
    
    Command deleteCommand = new("delete", "Delete lines from the file.");
    deleteCommand.Options.Add(searchTermsOption);
    quotesCommand.Subcommands.Add(deleteCommand);
    
    Command addCommand = new("add", "Add an entry to the file.");
    addCommand.Arguments.Add(quoteArgument);
    addCommand.Arguments.Add(bylineArgument);
    addCommand.Aliases.Add("insert");
    quotesCommand.Subcommands.Add(addCommand);
    

    此代码进行以下更改:

    • --fileread命令中删除该选项。

    • 将选项 --file 作为递归选项添加到根命令。

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

    • read 命令添加到 quotes 命令而不是根命令。

    • add创建和delete命令并将其添加到quotes命令。

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

    • Root 命令
      • quotes
        • read
        • add
        • delete

    应用现在实现 建议 的模式,其中父命令 (quotes) 指定区域或组,其子命令 (readadd, ) delete是作。

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

  4. SetAction 代码之后,为新 SetAction 子命令添加新代码:

    deleteCommand.SetAction(parseResult => DeleteFromFile(
        parseResult.GetValue(fileOption),
        parseResult.GetValue(searchTermsOption)));
    
    addCommand.SetAction(parseResult => AddToFile(
        parseResult.GetValue(fileOption),
        parseResult.GetValue(quoteArgument),
        parseResult.GetValue(bylineArgument))
        );
    

    子命令 quotes 没有作,因为它不是叶命令。 子命令 readadd并且 delete 是叶命令, quotesSetAction 为其中的每个命令调用。

  5. 添加作和 adddelete

    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
    
        var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s)));
        File.WriteAllLines(file.FullName, lines);
    }
    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}");
    }
    

完成的应用如下所示:

using System.CommandLine;

namespace scl;

class Program
{
    static int Main(string[] args)
    {
        Option<FileInfo> fileOption = new("--file")
        {
            Description = "An option whose argument is parsed as a FileInfo",
            Required = true,
            DefaultValueFactory = result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("sampleQuotes.txt");

                }
                string filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.AddError("File does not exist");
                    return null;
                }
                else
                {
                    return new FileInfo(filePath);
                }
            }
        };

        Option<int> delayOption = new("--delay")
        {
            Description = "Delay between lines, specified as milliseconds per character in a line.",
            DefaultValueFactory = parseResult => 42
        };
        Option<ConsoleColor> fgcolorOption = new("--fgcolor")
        {
            Description = "Foreground color of text displayed on the console.",
            DefaultValueFactory = parseResult => ConsoleColor.White
        };
        Option<bool> lightModeOption = new("--light-mode")
        {
            Description = "Background color of text displayed on the console: default is black, light mode is white."
        };

        Option<string[]> searchTermsOption = new("--search-terms")
        {
            Description = "Strings to search for when deleting entries.",
            Required = true,
            AllowMultipleArgumentsPerToken = true
        };
        Argument<string> quoteArgument = new("quote")
        {
            Description = "Text of quote."
        };
        Argument<string> bylineArgument = new("byline")
        {
            Description = "Byline of quote."
        };

        RootCommand rootCommand = new("Sample app for System.CommandLine");
        fileOption.Recursive = true;
        rootCommand.Options.Add(fileOption);

        Command quotesCommand = new("quotes", "Work with a file that contains quotes.");
        rootCommand.Subcommands.Add(quotesCommand);

        Command readCommand = new("read", "Read and display the file.")
        {
            delayOption,
            fgcolorOption,
            lightModeOption
        };
        quotesCommand.Subcommands.Add(readCommand);

        Command deleteCommand = new("delete", "Delete lines from the file.");
        deleteCommand.Options.Add(searchTermsOption);
        quotesCommand.Subcommands.Add(deleteCommand);

        Command addCommand = new("add", "Add an entry to the file.");
        addCommand.Arguments.Add(quoteArgument);
        addCommand.Arguments.Add(bylineArgument);
        addCommand.Aliases.Add("insert");
        quotesCommand.Subcommands.Add(addCommand);

        readCommand.SetAction(parseResult => ReadFile(
            parseResult.GetValue(fileOption),
            parseResult.GetValue(delayOption),
            parseResult.GetValue(fgcolorOption),
            parseResult.GetValue(lightModeOption)));

        deleteCommand.SetAction(parseResult => DeleteFromFile(
            parseResult.GetValue(fileOption),
            parseResult.GetValue(searchTermsOption)));

        addCommand.SetAction(parseResult => AddToFile(
            parseResult.GetValue(fileOption),
            parseResult.GetValue(quoteArgument),
            parseResult.GetValue(bylineArgument))
            );

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

    internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        foreach (string line in File.ReadLines(file.FullName))
        {
            Console.WriteLine(line);
            Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length));
        }
    }
    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");

        var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s)));
        File.WriteAllLines(file.FullName, lines);
    }
    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}");
    }
}

生成项目,然后尝试以下命令。

使用read命令将不存在的文件提交到--file其中,你会收到错误消息,而不是异常和堆栈跟踪:

scl quotes read --file nofile
File does not exist

尝试运行子命令 quotes ,并收到指示你使用 read的消息, adddelete

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/net9.0 文件夹中运行,该文件夹就是在其中找到包含更改 add 的文件和 delete 命令。 项目文件夹中文件的副本保持不变。

后续步骤

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