System.CommandLine 中的分析与调用

重要

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

System.CommandLine 提供命令行分析和作调用之间的明确分隔。 分析过程负责分析命令行输入,并创建ParseResult包含已分析值的对象(和分析错误)。 动作调用过程负责调用与已解析命令、选项或指令相关联的动作(参数不能有动作)。

“入门指南”System.CommandLine教程的下面示例中,ParseResult是通过解析命令行输入创建的。 未定义或调用任何动作。

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);
        }
    }
}

成功解析给定的命令(或指令、选项)后,将触发一个动作。 该操作是一个委托,它接受 ParseResult 参数并返回 int 退出代码(也提供 异步操作)。 退出代码由 System.CommandLine.Parsing.ParseResult.Invoke 该方法返回,可用于指示命令是否已成功执行。

入门 System.CommandLine 教程的以下示例中,首先为根命令定义操作,并在解析命令行输入后调用:

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);
        }
    }
}

一些内置符号(例如 System.CommandLine.Help.HelpOptionSystem.CommandLine.VersionOptionSystem.CommandLine.Completions.SuggestDirective)附带预定义的动作。 这些符号会在您创建根命令以及调用System.CommandLine.Parsing.ParseResult时自动添加,这样它们就能轻松运行。通过使用操作,您可以专注于应用程序逻辑,而库则负责解析和调用内置符号的操作。 如果您愿意,可以坚持进行解析过程,而不定义任何动作(如上面的第一个示例所示)。

ParseResult

ParseResult 类表示分析命令行输入的结果。 你需要使用它来获取选项和参数的解析值(无论是否使用操作)。 还可以检查是否存在任何解析错误或不匹配的令牌

GetValue

此方法 ParseResult.GetValue 允许检索选项和参数的值:

int integer = parseResult.GetValue(delayOption);
string? message = parseResult.GetValue(messageOption);

还可以按名称获取值,但这要求指定要获取的值的类型。

以下示例使用 C# 集合初始值设定项创建根命令:

RootCommand rootCommand = new("Parameter binding example")
{
    new Option<int>("--delay")
    {
        Description = "An option whose argument is parsed as an int."
    },
    new Option<string>("--message")
    {
        Description = "An option whose argument is parsed as a string."
    }
};

然后它使用 GetValue 方法通过名称获取值:

rootCommand.SetAction(parseResult =>
{
    int integer = parseResult.GetValue<int>("--delay");
    string? message = parseResult.GetValue<string>("--message");

    DisplayIntAndString(integer, message);
});

GetValue 的此重载在已解析命令(而不是整个符号树)的上下文中获取指定符号名称的解析值或默认值。 它接受符号名称,而不是 别名

分析错误

ParseResult.Errors 属性包含分析过程中发生的分析错误列表。 每个错误都由一个 ParseError 对象表示,该对象包含有关错误的信息,例如错误消息和导致错误的令牌。

调用 ParseResult.Invoke() 该方法时,它将返回一个退出代码,该代码指示分析是否成功。 如果存在任何分析错误,则退出代码为非零,所有分析错误都打印为标准错误。

如果不调用 ParseResult.Invoke 方法,则需要自己处理错误,例如通过打印它们:

foreach (ParseError parseError in parseResult.Errors)
{
    Console.Error.WriteLine(parseError.Message);
}
return 1;

不匹配的令牌

UnmatchedTokens 属性包含已分析但与任何配置的命令、选项或参数不匹配的令牌列表。

不匹配的令牌列表在行为类似于包装器的命令中非常有用。 包装命令采用一组令牌,并将其转发到另一个命令或应用程序。 sudo Linux 中的命令是一个示例。 它采用要模拟的用户的名称,后接要运行的命令。 例如,以下命令以用户apt update身份运行admin命令:

sudo -u admin apt update

若要实现如下所示的包装器命令,请将命令属性 System.CommandLine.Command.TreatUnmatchedTokensAsErrors 设置为 false。 然后,该 System.CommandLine.Parsing.ParseResult.UnmatchedTokens 属性将包含不显式属于该命令的所有参数。 在前面的示例中,ParseResult.UnmatchedTokens将包含aptupdate标记。

行动

操作是在成功解析命令(或选项或指令)时调用的委派操作。 它们采用参数 ParseResult 并返回 int (或 Task<int>) 退出代码。 退出代码用于指示作是否已成功执行。

System.CommandLine 提供抽象基类 CommandLineAction 和两个派生类: SynchronousCommandLineActionAsynchronousCommandLineAction。 前者用于返回 int 退出代码的同步操作,而后者用于返回 Task<int> 退出代码的异步操作。

无需创建派生类型来定义动作。 可以使用SetAction方法为命令设置动作。 同步操作可以是一个采用 ParseResult 参数,并返回 int 退出代码的委托。 异步操作可以是一个委托,该委托接受 ParseResultCancellationToken 参数,并返回一个 Task<int>

rootCommand.SetAction(parseResult =>
{
    FileInfo parsedFile = parseResult.GetValue(fileOption);
    ReadFile(parsedFile);
    return 0;
});

异步操作

同步和异步作不应在同一应用程序中混合。 如果要使用异步操作,应用程序需要始终是异步的。 这意味着所有操作都应是异步操作,你应使用接受委托并返回 System.CommandLine.Command.SetAction 退出代码的 Task<int> 方法。 此外,传递给操作委托的CancellationToken还需要进一步传递给所有可以取消的方法,例如文件 I/O 操作或网络请求。

还需要确保使用 ParseResult.InvokeAsync(CancellationToken) 方法而不是 Invoke 方法。 此方法是异步的,并返回 Task<int> 退出代码。 它还接受一个可选的 CancellationToken 参数,该参数可用于取消动作。

以下代码使用 SetAction 重载来获取 ParseResultCancellationToken,而不仅仅是 ParseResult

static Task<int> Main(string[] args)
{
    Option<string> urlOption = new("--url", "A URL.");
    RootCommand rootCommand = new("Handle termination example") { urlOption };

    rootCommand.SetAction((ParseResult parseResult, CancellationToken cancellationToken) =>
    {
        string? urlOptionValue = parseResult.GetValue(urlOption);
        return DoRootCommand(urlOptionValue, cancellationToken);
    });

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

public static async Task<int> DoRootCommand(
    string? urlOptionValue, CancellationToken cancellationToken)
{
    using HttpClient httpClient = new();

    try
    {
        await httpClient.GetAsync(urlOptionValue, cancellationToken);
        return 0;
    }
    catch (OperationCanceledException)
    {
        await Console.Error.WriteLineAsync("The operation was aborted");
        return 1;
    }
}

进程终止超时

ProcessTerminationTimeout通过在调用期间传递给每个异步操作的CancellationToken来启用信号和处理进程终止(Ctrl+CSIGINTSIGTERM)。 它默认处于启用状态(2 秒),但你可以将其设置为 null 禁用它。

启用后,如果动作未在指定的超时时间内完成,则进程将被终止。 这可用于正常处理终止,例如,通过在进程终止之前保存状态。

若要测试上一段的示例代码,请使用 URL 运行命令,该 URL 需要一些时间才能加载完毕,然后在加载完成后按 Ctrl+C。 在 macOS 上,按 Command+Period(.)。 例如:

testapp --url https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis
The operation was aborted

退出代码

退出代码是一个整数值,由一个动作返回以指示其成功或失败。 按照约定,退出代码 0 表示成功,而任何非零值都表示错误。 请务必在应用程序中定义有意义的退出代码,以便清楚地传达命令执行的状态。

每个 SetAction 方法都有一个接受返回 int 退出代码的委派的重载,以及一个返回 0 的重载。

static int Main(string[] args)
{
    Option<int> delayOption = new("--delay");
    Option<string> messageOption = new("--message");

    RootCommand rootCommand = new("Parameter binding example")
    {
        delayOption,
        messageOption
    };

    rootCommand.SetAction(parseResult =>
    {
        Console.WriteLine($"--delay = {parseResult.GetValue(delayOption)}");
        Console.WriteLine($"--message = {parseResult.GetValue(messageOption)}");
        // Value returned from the action delegate is the exit code.
        return 100;
    });

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

另请参阅