Partilhar via


Análise sintática e invocação em System.CommandLine

Importante

System.CommandLine está atualmente em pré-visualização e esta documentação destina-se à versão 2.0 beta 5. Algumas informações estão relacionadas ao produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

System.CommandLine fornece uma separação clara entre análise de linha de comando e invocação de ação. O processo de análise é responsável por analisar a entrada da linha de comando e criar um System.CommandLine.ParseResult objeto que contém os valores analisados (e erros de análise). O processo de invocação de ação é responsável por invocar a ação associada ao comando, opção ou diretiva analisada (os argumentos não podem ter ações).

No exemplo a seguir do nosso tutorial Introdução ao System.CommandLine, o ParseResult é criado analisando a entrada da linha de comandos. Nenhuma ação é definida ou invocada:

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

Uma ação é invocada quando um determinado comando (ou diretiva, ou opção) é analisado com êxito. A ação é um delegado que usa um System.CommandLine.ParseResult parâmetro e retorna um int código de saída (ações assíncronas também estão disponíveis)). O código de saída é retornado pelo System.CommandLine.Parsing.ParseResult.Invoke método e pode ser usado para indicar se o comando foi executado com êxito ou não.

No exemplo a seguir do nosso tutorial Comece com System.CommandLine, a ação é definida para o comando raiz e invocada após a análise da entrada da linha de comandos.

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

Alguns símbolos internos, como System.CommandLine.Help.HelpOption, System.CommandLine.VersionOptionou System.CommandLine.Completions.SuggestDirective, vêm com ações predefinidas. Esses símbolos são adicionados automaticamente ao comando root quando você o cria, e quando você invoca o System.CommandLine.Parsing.ParseResult, eles "simplesmente funcionam". O uso de ações permite que você se concentre na lógica do aplicativo, enquanto a biblioteca cuida da análise e da invocação de ações para símbolos internos. Se preferir, você pode se ater ao processo de análise e não definir nenhuma ação (como no primeiro exemplo acima).

Resultado da Análise

O System.CommandLine.Parsing.ParseResult tipo é uma classe que representa os resultados da análise da entrada de linha de comando. Você precisa usá-lo para obter os valores analisados para opções e argumentos (não importa se você está usando ações ou não). Você também pode verificar se houve erros de análise ou tokens incomparáveis.

GetValue

O System.CommandLine.Parsing.ParseResult.GetValue<T> método permite recuperar os valores de opções e argumentos:

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

Você também pode obter valores por nome, mas isso requer que você especifique o tipo do valor que deseja obter.

O exemplo a seguir usa inicializadores de coleção C# para criar um comando raiz:

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."
    }
};

Em seguida, ele usa o GetValue método para obter os valores por nome:

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

    DisplayIntAndString(integer, message);
});

Essa sobrecarga de GetValue obtém o valor analisado ou padrão para o nome do símbolo especificado, no contexto do comando analisado (não na árvore de símbolos inteira). Ele aceita o nome do símbolo, não um alias.

Erros de análise

A System.CommandLine.Parsing.ParseResult.Errors propriedade contém uma lista de erros de análise que ocorreram durante o processo de análise. Cada erro é representado por um System.CommandLine.Parsing.ParseError objeto, que contém informações sobre o erro, como a mensagem de erro e o token que causou o erro.

Quando você chama o System.CommandLine.Parsing.ParseResult.Invoke método, ele retorna um código de saída que indica se a análise foi bem-sucedida ou não. Se houver algum erro de análise, o código de saída será diferente de zero e todos os erros de análise serão impressos no erro padrão.

Se você não estiver invocando o System.CommandLine.Parsing.ParseResult.Invoke método, você precisa lidar com os erros por conta própria, por exemplo, imprimindo-os:

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

Tokens que não correspondem

A System.CommandLine.Parsing.ParseResult.UnmatchedTokens propriedade contém uma lista dos tokens que foram analisados, mas não corresponderam a nenhum comando, opção ou argumento configurado.

A lista de tokens não correspondentes é útil em comandos que se comportam como wrappers. Um comando wrapper pega um conjunto de tokens e os encaminha para outro comando ou aplicativo. O sudo comando no Linux é um exemplo. Requer o nome de um utilizador para personificar, seguido de um comando para executar. Por exemplo:

sudo -u admin apt update

Essa linha de comando executaria o apt update comando como o usuário admin.

Para implementar um comando wrapper como este, defina a propriedade do comando System.CommandLine.Command.TreatUnmatchedTokensAsErrors para false. Em seguida, a System.CommandLine.Parsing.ParseResult.UnmatchedTokens propriedade conterá todos os argumentos que não pertencem explicitamente ao comando. No exemplo anterior, ParseResult.UnmatchedTokens conteria os apt tokens e update .

Ações

As ações são delegados que são acionados quando um comando (ou uma opção ou diretiva) é interpretado com sucesso. Eles pegam um System.CommandLine.ParseResult parâmetro e retornam um int (ou Task<int>) código de saída. O código de saída é usado para indicar se a ação foi executada com êxito ou não.

System.CommandLine fornece uma classe System.CommandLine.CommandLineAction base abstrata e duas classes derivadas: System.CommandLine.SynchronousCommandLineAction e System.CommandLine.AsynchronousCommandLineAction. O primeiro é usado para ações síncronas que retornam um int código de saída, enquanto o segundo é usado para ações assíncronas que retornam um código de Task<int> saída.

Não é necessário criar um tipo derivado para definir uma ação. Você pode usar o System.CommandLine.Command.SetAction método para definir uma ação para um comando. A ação síncrona pode ser um delegado que recebe um System.CommandLine.ParseResult como parâmetro e retorna um código de saída int. A ação assíncrona pode ser um delegado que usa um System.CommandLine.ParseResult e um CancellationToken como parâmetros e retorna um Task<int>.

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

Ações assíncronas

Ações síncronas e assíncronas não devem ser misturadas no mesmo aplicativo. Se você quiser usar ações assíncronas, seu aplicativo precisa ser assíncrono de cima para baixo. Isso significa que todas as ações devem ser assíncronas e você deve usar o System.CommandLine.Command.SetAction método que aceita um delegado retornando um Task<int> código de saída. Além disso, o CancellationToken que é passado para o delegado de ação precisa ser passado ainda mais para todos os métodos que podem ser cancelados, como operações de E/S de arquivo ou solicitações de rede.

Além disso, você precisa garantir que o System.CommandLine.Parsing.ParseResult.InvokeAsync método seja usado em vez de System.CommandLine.Parsing.ParseResult.Invoke. Esse método é assíncrono e retorna um código de Task<int> saída. Ele também aceita um parâmetro opcional CancellationToken que pode ser usado para cancelar a ação.

O código anterior usa uma SetAction sobrecarga que obtém um ParseResult e um CancellationToken em vez de apenas 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;
    }
}

Tempo limite de rescisão do processo

System.CommandLine.CommandLineConfiguration.ProcessTerminationTimeout permite a sinalização e o tratamento do encerramento do processo (Ctrl+C, SIGINT, SIGTERM) através de um CancellationToken que é passado para cada ação assíncrona durante a invocação. Ele está ativado por padrão (2 segundos), mas você pode configurá-lo para null desativá-lo.

Quando habilitada, se a ação não for concluída dentro do tempo limite especificado, o processo será encerrado. Isso é útil para lidar com a rescisão normalmente, por exemplo, salvando o estado antes que o processo seja encerrado.

Para testar o código de exemplo do parágrafo anterior, execute o comando com uma URL que levará um momento para carregar e, antes de terminar de carregar, pressione Ctrl+C. No macOS, pressione Command+Period(.). Por exemplo:

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

Códigos de saída

O código de saída é um valor inteiro retornado por uma ação que indica seu sucesso ou falha. Por convenção, um código de saída 0 significa sucesso, enquanto qualquer valor diferente de zero indica um erro. É importante definir códigos de saída significativos em seu aplicativo para comunicar claramente o status da execução do comando.

Cada SetAction método tem uma sobrecarga que aceita um delegado retornando um int código de saída onde o código de saída precisa ser fornecido de forma explícita e uma sobrecarga que retorna 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();
}

Ver também

Como personalizar a análise e a validação em System.CommandLineSystem.CommandLine Visão geral