Partilhar via


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

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 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 tutorial Introdução ao System.CommandLine tutorial, o ParseResult é criado analisando a entrada de linha de comando. 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.Errors.Count == 0 && 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 ParseResult argumento 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 ParseResult.Invoke(InvocationConfiguration) método e pode ser usado para indicar se o comando foi executado com êxito ou não.

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

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 HelpOption, VersionOptione 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 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 preferires, podes seguir o processo de análise sintática e não definir nenhuma ação (como no primeiro exemplo deste artigo).

ParseResult

A ParseResult classe 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 ParseResult.GetValue 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 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 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 ParseResult.Invoke(InvocationConfiguration) 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 chamar o 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 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, o comando a seguir executa o apt update comando como o usuário admin:

sudo -u admin apt update

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 ParseResult argumento 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 CommandLineAction base abstrata e duas classes derivadas: SynchronousCommandLineAction e 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 SetAction método para definir uma ação para um comando. A ação síncrona pode ser um delegado que usa um ParseResult argumento e retorna um int código de saída. A ação assíncrona pode ser um delegado que usa ParseResult e CancellationToken argumenta e retorna um Task<int>arquivo .

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 precisará ser assíncrono durante todo o processo. Isso significa que todas as ações devem ser assíncronas e você deve usar o 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 para todos os métodos que podem ser cancelados, como operações de E/S de arquivo ou solicitações de rede.

Você também precisa garantir que o ParseResult.InvokeAsync(InvocationConfiguration, CancellationToken) método seja usado em vez de 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 a seguir 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")
    {
        Description = "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

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