Tutorial: Introdução ao System.CommandLine

Importante

System.CommandLine está atualmente em PRÉ-VISUALIZAÇÃO e esta documentação destina-se à versão 2.0 beta 4. Algumas informações estão relacionadas com o produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado. A Microsoft não faz garantias, de forma expressa ou implícita, em relação à informação aqui apresentada.

Este tutorial mostra como criar uma aplicação de linha de comandos .NET que utiliza a System.CommandLine biblioteca. Irá começar por criar um comando de raiz simples que tem uma opção. Em seguida, irá adicionar a essa base, criando uma aplicação mais complexa que contém vários subcomandos e diferentes opções para cada comando.

Neste tutorial, ficará a saber como:

  • Criar comandos, opções e argumentos.
  • Especifique os valores predefinidos para as opções.
  • Atribua opções e argumentos a comandos.
  • Atribua uma opção recursivamente a todos os subcomandos sob um comando.
  • Trabalhe com vários níveis de subcomandos aninhados.
  • Crie aliases para comandos e opções.
  • Trabalhe com stringos tipos de opção , string[]int, , boolFileInfo e enumeração.
  • Vincular valores de opção ao código do processador de comandos.
  • Utilize código personalizado para analisar e validar opções.

Pré-requisitos

Ou

  • Visual Studio 2022 com a carga de trabalho de desenvolvimento de ambiente de trabalho .NET instalada.

Criar a aplicação

Crie um projeto de aplicação de consola .NET 6 com o nome "scl".

  1. Crie uma pasta com o nome scl para o projeto e, em seguida, abra uma linha de comandos na nova pasta.

  2. Execute o seguinte comando:

    dotnet new console --framework net6.0
    

Instalar o pacote System.CommandLine

  • Execute o seguinte comando:

    dotnet add package System.CommandLine --prerelease
    

    A --prerelease opção é necessária porque a biblioteca ainda está em versão beta.

  1. Substitua o conteúdo do ficheiro Program.cs pelo seguinte código:

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

O código anterior:

  • Cria uma opção com o nome --file do tipo FileInfo e atribui-a ao comando raiz:

    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);
    
  • Especifica que ReadFile é o método que será chamado quando o comando de raiz for invocado:

    rootCommand.SetHandler((file) => 
        { 
            ReadFile(file!); 
        },
        fileOption);
    
  • Apresenta o conteúdo do ficheiro especificado quando o comando raiz é invocado:

    static void ReadFile(FileInfo file)
    {
        File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
    }
    

Testar a aplicação

Pode utilizar qualquer uma das seguintes formas de testar ao desenvolver uma aplicação de linha de comandos:

  • Execute o dotnet build comando e, em seguida, abra uma linha de comandos na pasta scl/bin/Debug/net6.0 para executar o executável:

    dotnet build
    cd bin/Debug/net6.0
    scl --file scl.runtimeconfig.json
    
  • Utilize dotnet run e transmita valores de opção para a aplicação em vez de para o run comando ao incluí-los depois --de , como no exemplo seguinte:

    dotnet run -- --file scl.runtimeconfig.json
    

    Na Pré-visualização do SDK .NET 7.0.100, pode utilizar o commandLineArgs de um ficheiro launchSettings.json ao executar o comando dotnet run --launch-profile <profilename>.

  • Publique o projeto numa pasta, abra uma linha de comandos para essa pasta e execute o executável:

    dotnet publish -o publish
    cd ./publish
    scl --file scl.runtimeconfig.json
    
  • No Visual Studio 2022, selecione DepurarPropriedades de Depuração> no menu e introduza as opções e argumentos na caixa Argumentos da linha de comandos. Por exemplo:

    Argumentos da linha de comandos no Visual Studio 2022

    Em seguida, execute a aplicação, por exemplo, ao premir Ctrl+F5.

Este tutorial pressupõe que está a utilizar a primeira destas opções.

Quando executa a aplicação, esta apresenta o conteúdo do ficheiro especificado pela opção --file .

{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    }
  }
}

Saída da ajuda

System.CommandLine fornece automaticamente o resultado da ajuda:

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

Saída da versão

System.CommandLine fornece automaticamente o resultado da versão:

scl --version
1.0.0

Adicionar um subcomando e opções

Nesta secção, pode:

  • Crie mais opções.
  • Crie um subcomando.
  • Atribua as novas opções ao novo subcomando.

As novas opções permitir-lhe-ão configurar as cores do texto em primeiro plano e de fundo e a velocidade de leitura. Estas funcionalidades serão utilizadas para ler uma coleção de aspas provenientes do tutorial da aplicação de consola Teleprompter.

  1. Copie o ficheiro sampleQuotes.txt do repositório do GitHub para este exemplo para o diretório do projeto. Para obter informações sobre como transferir ficheiros, consulte as instruções em Exemplos e Tutoriais.

  2. Abra o ficheiro do projeto e adicione um <ItemGroup> elemento imediatamente antes da etiqueta de fecho </Project> :

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

    Adicionar esta marcação faz com que o ficheiro de texto seja copiado para a pasta bin/debug/net6.0 quando cria a aplicação. Por isso, quando executa o executável nessa pasta, pode aceder ao ficheiro por nome sem especificar um caminho de pasta.

  3. Em Program.cs, após o código que cria a opção --file , crie opções para controlar a velocidade de leitura e as cores do texto:

    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. Depois da linha que cria o comando raiz, elimine a linha que adiciona a opção --file à mesma. Está a removê-lo aqui porque irá adicioná-lo a um novo subcomando.

    var rootCommand = new RootCommand("Sample app for System.CommandLine");
    //rootCommand.AddOption(fileOption);
    
  5. Depois da linha que cria o comando raiz, crie um read subcomando. Adicione as opções a este subcomando e adicione o subcomando ao comando raiz.

    var readCommand = new Command("read", "Read and display the file.")
        {
            fileOption,
            delayOption,
            fgcolorOption,
            lightModeOption
        };
    rootCommand.AddCommand(readCommand);
    
  6. Substitua o SetHandler código pelo seguinte SetHandler código para o novo subcomando:

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

    Já não está a chamar SetHandler no comando raiz porque o comando raiz já não precisa de um processador. Quando um comando tem subcomandos, normalmente tem de especificar um dos subcomandos ao invocar uma aplicação de linha de comandos.

  7. Substitua o método do ReadFile processador pelo seguinte código:

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

A aplicação tem agora o seguinte aspeto:

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

Testar o novo subcomando

Agora, se tentar executar a aplicação sem especificar o subcomando, receberá uma mensagem de erro seguida de uma mensagem de ajuda que especifica o subcomando que está disponível.

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.

O texto de ajuda do subcomando read mostra que estão disponíveis quatro opções. Mostra valores válidos para a enumeração.

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

Execute o subcomando read especificando apenas a opção --file e obtém os valores predefinidos para as outras três opções.

scl read --file sampleQuotes.txt

O atraso predefinido de 42 milissegundos por caráter causa uma velocidade de leitura lenta. Pode acelerá-lo ao definir --delay um número mais baixo.

scl read --file sampleQuotes.txt --delay 0

Pode utilizar --fgcolor e --light-mode para definir cores de texto:

scl read --file sampleQuotes.txt --fgcolor red --light-mode

Forneça um valor inválido para --delay e receberá uma mensagem de erro:

scl read --file sampleQuotes.txt --delay forty-two
Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'.

Forneça um valor inválido para --file e obtém uma exceção:

scl read --file nofile
Unhandled exception: System.IO.FileNotFoundException:
Could not find file 'C:\bin\Debug\net6.0\nofile'.

Adicionar subcomandos e validação personalizada

Esta secção cria a versão final da aplicação. Quando terminar, a aplicação terá os seguintes comandos e opções:

  • comando raiz com uma opção global* com o nome --file
    • Comando quotes
      • read com opções denominadas --delay, --fgcolore --light-mode
      • add comando com argumentos com o nome quote e byline
      • delete comando com a opção com o nome --search-terms

* Está disponível uma opção global para o comando ao qual está atribuído e recursivamente a todos os seus subcomandos.

Eis uma entrada de linha de comandos de exemplo que invoca cada um dos comandos disponíveis com as opções e argumentos:

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. Em Program.cs, substitua o código que cria a opção --file pelo seguinte código:

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

    Este código utiliza ParseArgument<T> para fornecer análise personalizada, validação e processamento de erros.

    Sem este código, os ficheiros em falta são reportados com uma exceção e rastreio de pilha. Com este código, é apresentada apenas a mensagem de erro especificada.

    Este código também especifica um valor predefinido, razão pela qual é definido isDefault como true. Se não estiver definido isDefault como , o parseArgument delegado não será chamado quando não for fornecida nenhuma entrada para --filetrue.

  2. Depois do código que cria lightModeOption, adicione opções e argumentos para os add comandos e delete :

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

    A AllowMultipleArgumentsPerToken definição permite-lhe omitir o nome da opção --search-terms ao especificar elementos na lista após o primeiro. Torna os seguintes exemplos de entrada de linha de comandos equivalentes:

    scl quotes delete --search-terms David "You can do"
    scl quotes delete --search-terms David --search-terms "You can do"
    
  3. Substitua o código que cria o comando raiz e o read comando pelo seguinte código:

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

    Este código efetua as seguintes alterações:

    • Remove a opção --file do read comando .

    • Adiciona a opção --file como uma opção global ao comando raiz.

    • Cria um quotes comando e adiciona-o ao comando raiz.

    • Adiciona o read comando ao quotes comando em vez de ao comando raiz.

    • add Cria e delete comandos e adiciona-os ao quotes comando .

    O resultado é a seguinte hierarquia de comandos:

    • Comando raiz
      • quotes
        • read
        • add
        • delete

    A aplicação implementa agora o padrão recomendado em que o comando principal (quotes) especifica uma área ou grupo e os respetivos comandos subordinados (read, add, delete) são ações.

    As opções globais são aplicadas ao comando e recursivamente aos subcomandos. Uma --file vez que está no comando raiz, estará disponível automaticamente em todos os subcomandos da aplicação.

  4. Depois do SetHandler código, adicione um novo SetHandler código para os novos subcomandos:

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

    O subcomando quotes não tem um processador porque não é um comando de folha. Os subcomandos read, adde delete são comandos de folha em e SetHandler são chamados quotespara cada um deles.

  5. Adicione os processadores para add e delete.

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

A aplicação concluída tem o seguinte aspeto:

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

Crie o projeto e, em seguida, experimente os seguintes comandos.

Submeta um ficheiro inexistente para --file com o read comando e receberá uma mensagem de erro em vez de uma exceção e rastreio de pilha:

scl quotes read --file nofile
File does not exist

Experimente executar o subcomando quotes e recebe uma mensagem a direcioná-lo para utilizar read, addou delete:

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.

Execute o subcomando adde, em seguida, observe o fim do ficheiro de texto para ver o texto adicionado:

scl quotes add "Hello world!" "Nancy Davolio"

Execute o subcomando delete com cadeias de pesquisa a partir do início do ficheiro e, em seguida, observe o início do ficheiro de texto para ver onde o texto foi removido:

scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"

Nota

Se estiver a executar na pasta bin/debug/net6.0 , essa pasta é onde encontrará o ficheiro com as alterações dos add comandos e delete . A cópia do ficheiro na pasta do projeto permanece inalterada.

Passos seguintes

Neste tutorial, criou uma aplicação de linha de comandos simples que utiliza System.CommandLine. Para saber mais sobre a biblioteca, consulte System.CommandLine Descrição geral.