Tutorial: Introdução a System.CommandLine
Importante
Atualmente, System.CommandLine
está em VERSÃO PRÉVIA, e essa documentação é para a versão 2.0 beta 4.
Algumas informações estão relacionadas a produtos de pré-lançamento que poderão ser substancialmente modificados antes do lançamento. A Microsoft não oferece garantias, expressas ou implícitas, das informações aqui fornecidas.
Este tutorial mostra como criar um aplicativo de linha de comando .NET que usa a biblioteca System.CommandLine
. Você começará criando um comando raiz simples que tenha uma opção. Em seguida, adicionará a essa base, criando um aplicativo mais complexo que contém vários subcomandos e opções diferentes para cada comando.
Neste tutorial, você aprenderá a:
- Criar comandos, opções e argumentos ocultos.
- Especificar valores padrão para opções.
- Atribuir opções e argumentos a comandos.
- Atribuir uma opção recursivamente a todos os subcomandos sob um comando.
- Trabalhar com vários níveis de subcomandos aninhados.
- Criar aliases para comandos e opções.
- Trabalhar com
string
,string[]
,int
,bool
,FileInfo
e tipos de opção de enumeração. - Associar valores de opção ao código do manipulador de comandos.
- Usar o código personalizado para analisar e validar opções.
Pré-requisitos
- Um editor de código, como Visual Studio Code com a extensão C#.
- O SDK do .NET 6.
Ou
- Visual Studio 2022 com a carga de trabalho Desenvolvimento de área de trabalho do .NET instalada.
Criar o aplicativo
Crie um projeto de aplicativo de console do .NET 6 chamado "scl".
Crie uma pasta chamada scl para o projeto e abra um prompt de comando na nova pasta.
Execute o comando a seguir:
dotnet new console --framework net6.0
Instalar o pacote System.CommandLine
Execute o comando a seguir:
dotnet add package System.CommandLine --prerelease
A opção
--prerelease
é necessária porque a biblioteca ainda está em beta.
Substitua o conteúdo do 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 chamada
--file
do tipo FileInfo e a atribui 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 raiz for invocado:rootCommand.SetHandler((file) => { ReadFile(file!); }, fileOption);
Exibe o conteúdo do arquivo especificado quando o comando raiz é invocado:
static void ReadFile(FileInfo file) { File.ReadLines(file.FullName).ToList() .ForEach(line => Console.WriteLine(line)); }
Testar o aplicativo
Você pode usar qualquer uma das seguintes maneiras de testar ao desenvolver um aplicativo de linha de comando:
Execute o comando
dotnet build
e abra um prompt de comando na pasta scl/bin/Debug/net6.0 para executar o executável:dotnet build cd bin/Debug/net6.0 scl --file scl.runtimeconfig.json
Use
dotnet run
e passe valores de opção para o aplicativo em vez de para o comandorun
, incluindo-os após--
, como no seguinte exemplo:dotnet run -- --file scl.runtimeconfig.json
No .NET 7.0.100 SDK (versão prévia), você pode usar o
commandLineArgs
de um arquivo launchSettings.json executando o comandodotnet run --launch-profile <profilename>
.Publique o projeto em uma pasta, abra um prompt de comando para essa pasta e execute o executável:
dotnet publish -o publish cd ./publish scl --file scl.runtimeconfig.json
No Visual Studio 2022, selecione Depurar>Propriedades da Depuração no menu e insira as opções e argumentos na caixa Argumentos de linha de comando. Por exemplo:
Em seguida, execute o aplicativo, por exemplo, pressionando Ctrl+F5.
Este tutorial pressupõe que você esteja usando a primeira dessas opções.
Quando você executa o aplicativo, ele exibe o conteúdo do arquivo especificado pela opção --file
.
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
}
}
}
Saída de Ajuda
A System.CommandLine
fornece automaticamente a saída de 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 de versão
A System.CommandLine
fornece automaticamente a saída da versão:
scl --version
1.0.0
Adicionar um subcomando e opções
Nesta seção, você:
- Crie mais opções.
- Crie um subcomando.
- Atribua as novas opções ao novo subcomando.
As novas opções permitirão que você configure as cores de texto em primeiro plano e plano de fundo e a velocidade de leitura. Esses recursos serão usados para ler uma coleção de aspas provenientes do Tutorial do aplicativo de console Teleprompter.
Copie o arquivo sampleQuotes.txt do repositório do GitHub para este exemplo no diretório de seu projeto. Para obter informações sobre como baixar arquivos, confira as instruções em Exemplos e tutoriais.
Abra o arquivo de projeto e adicione um elemento
<ItemGroup>
pouco antes da marca de fechamento</Project>
:<ItemGroup> <Content Include="sampleQuotes.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
Adicionar essa marcação faz com que o arquivo de texto seja copiado para a pasta bin/debug/net6.0 ao compilar o aplicativo. Portanto, ao executar o executável nessa pasta, você pode acessar o arquivo pelo nome sem especificar um caminho de pasta.
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.");
Depois da linha que cria o comando raiz, exclua a linha que adiciona a opção
--file
a ela. Você a está removendo aqui porque vai adicioná-la a um novo subcomando.var rootCommand = new RootCommand("Sample app for System.CommandLine"); //rootCommand.AddOption(fileOption);
Depois da linha que cria o comando raiz, crie um subcomando
read
. Adicione as opções a esse 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);
Substitua o código
SetHandler
pelo seguinte códigoSetHandler
para o novo subcomando:readCommand.SetHandler(async (file, delay, fgcolor, lightMode) => { await ReadFile(file!, delay, fgcolor, lightMode); }, fileOption, delayOption, fgcolorOption, lightModeOption);
Você não está mais chamando
SetHandler
no comando raiz porque o comando raiz não precisa mais de um manipulador. Quando um comando tem subcomandos, normalmente você precisa especificar um dos subcomandos ao invocar um aplicativo de linha de comando.Substitua o método do manipulador
ReadFile
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); }; }
O aplicativo agora tem esta aparência:
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 você tentar executar o aplicativo sem especificar o subcomando, receberá uma mensagem de erro seguida de uma mensagem de ajuda que especifica o subcomando 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 para subcomando read
mostra que quatro opções estão disponíveis. Ele 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 subcomando especificando read
apenas a opção --file
e você obterá os valores padrão para as outras três opções.
scl read --file sampleQuotes.txt
O atraso padrão de 42 milissegundos por caractere causa uma velocidade de leitura lenta. Você pode acelerá-lo configurando --delay
para um número menor.
scl read --file sampleQuotes.txt --delay 0
Você pode usar --fgcolor
e --light-mode
definir cores de texto:
scl read --file sampleQuotes.txt --fgcolor red --light-mode
Forneça um valor --delay
inválido e você 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 --file
inválido e você obterá 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 seção cria a versão final do aplicativo. Ao terminar, o aplicativo terá os seguintes comandos e opções:
- comando raiz com uma opção global* chamada
--file
- Comando
quotes
- comando
read
com opções chamadas--delay
,--fgcolor
e--light-mode
- comando
add
com argumentos chamadosquote
ebyline
- comando
delete
com a opção chamada--search-terms
- comando
- Comando
* Uma opção global está disponível para o comando ao qual ela é atribuída e recursivamente a todos os seus subcomandos.
Aqui está uma entrada de linha de comando de exemplo que invoca cada um dos comandos disponíveis com suas 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"
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); } });
Esse código usa ParseArgument<T> para fornecer análise personalizada, validação e tratamento de erro.
Sem esse código, arquivos ausentes são relatados com uma exceção e rastreamento de pilha. Com esse código, apenas a mensagem de erro especificada é exibida.
Esse código também especifica um valor padrão, e é por isso que ele define
isDefault
comotrue
. Se você não definirisDefault
comotrue
, o delegadoparseArgument
não será chamado quando nenhuma entrada for fornecida para--file
.Depois do código que cria
lightModeOption
, adicione opções e argumentos para os comandosadd
edelete
: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 configuração AllowMultipleArgumentsPerToken permite omitir o nome da opção
--search-terms
ao especificar elementos na lista após a primeira. Ele torna os seguintes exemplos de entrada de linha de comando equivalentes:scl quotes delete --search-terms David "You can do" scl quotes delete --search-terms David --search-terms "You can do"
Substitua o código que cria o comando raiz e o comando
read
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 faz as seguintes alterações:
Remove a opção
--file
do comandoread
.Adiciona a opção
--file
como uma opção global ao comando raiz.Cria um comando
quotes
e o adiciona ao comando raiz.Adiciona o comando
read
ao comandoquotes
em vez do comando raiz.Cria os comandos
add
edelete
e os adiciona ao comandoquotes
.
O resultado é a seguinte hierarquia de comandos:
- Comando raiz
quotes
read
add
delete
O aplicativo agora implementa o padrão recomendado em que o comando pai (
quotes
) especifica uma área ou grupo e seus comandos filho (read
,add
,delete
) são ações.As opções globais são aplicadas ao comando e recursivamente a subcomandos. Como
--file
está no comando raiz, ele estará disponível automaticamente em todos os subcomandos do aplicativo.Após o código
SetHandler
, adicione um códigoSetHandler
aos 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 manipulador porque não é um comando folha. Os subcomandosread
,add
edelete
são comandos folha sobquotes
, eSetHandler
é chamado para cada um deles.Adicione os manipuladores para
add
edelete
.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(); }
O aplicativo concluído tem esta aparência:
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 tente os comandos a seguir.
Envie um arquivo inexistente com comando --file
o read
e você obtém uma mensagem de erro em vez de uma exceção e um rastreamento de pilha:
scl quotes read --file nofile
File does not exist
Tente executar subcomando quotes
e você recebe uma mensagem direcionando você a usar read
, add
ou 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 add
e examine o final do arquivo de texto para ver o texto adicionado:
scl quotes add "Hello world!" "Nancy Davolio"
Execute subcomando delete
com cadeias de caracteres de pesquisa desde o início do arquivo e examine o início do arquivo de texto para ver em que ponto o texto foi removido:
scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"
Observação
Se você estiver executando na pasta bin/debug/net6.0, será nessa pasta que você encontrará o arquivo com alterações dos comandos add
e delete
. A cópia do arquivo na pasta do projeto permanece inalterada.
Próximas etapas
Neste tutorial, você criou um aplicativo de linha de comando simples que usa System.CommandLine
. Para saber mais sobre a biblioteca, confira Visão geral de System.CommandLine.