Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Sugestão
Este artigo faz parte da secção Fundamentos , escrita para programadores que conhecem pelo menos uma linguagem de programação e estão a aprender C#. Se és novo na programação, começa por começar. Se precisar de cobertura abrangente da biblioteca, consulte a documentação da biblioteca System.CommandLine.
A System.CommandLine biblioteca trata da análise da linha de comandos, geração de texto de ajuda e validação de entradas para que possas concentrar-te na lógica da tua aplicação. Neste tutorial, constróis uma CLI de rastreador de tarefas que demonstra os conceitos centrais: comandos, subcomandos, opções e argumentos.
Neste tutorial, você:
- Crie uma aplicação baseada em ficheiros usando o
System.CommandLinepacote. - Defina opções e argumentos com valores tipados.
- Construa subcomandos e anexe opções e argumentos a eles.
- Lida com cada subcomando através de uma ação.
- Testa a aplicação com diferentes entradas de linha de comandos.
Pré-requisitos
- Instale o SDK .NET 10 ou posterior.
Criar a aplicação
Comece por criar um programa C# baseado em ficheiros e adicionar o System.CommandLine pacote.
Crie um arquivo nomeado
TaskCli.cscom o seguinte conteúdo:#!/usr/bin/env dotnetA linha shebang
#!permite executar o ficheiro diretamente em sistemas Unix. No Windows, execute o ficheiro comdotnet run TaskCli.cs.Adicione a
System.CommandLinediretiva do pacote e as declarações requeridasusing:#:package System.CommandLine@2.0.0using System.CommandLine; using System.CommandLine.Parsing; using System.Text.Json;Importante
A versão
2.0.0é a mais recente no momento em que escrevo. Consulte a página NuGet do pacote para a versão mais recente e certifique-se de que tem as correções de segurança mais recentes.
Compreender a estrutura de comando
Antes de escrever qualquer código de análise, considere como é a CLI do ponto de vista do utilizador. O rastreador de tarefas suporta quatro operações:
dotnet TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01
dotnet TaskCli.cs -- list --all
dotnet TaskCli.cs -- complete 3
dotnet TaskCli.cs -- remove 3
dotnet TaskCli.cs -- --verbose list
Observação
Nos exemplos anteriores, o -- que ocorre após TaskCli.cs indica ao dotnet run que todos os argumentos restantes são transmitidos para a sua aplicação em vez de serem interpretados pela própria interface de linha de comando dotnet.
Cada linha utiliza vários conceitos de linha de comandos:
-
Subcomandos são verbos que dizem à aplicação o que fazer. O rastreador de tarefas tem quatro:
add,list,complete, eremove. Cada subcomando pode definir os seus próprios parâmetros. -
Os argumentos são valores posicionais que seguem um subcomando. Em
add "Write documentation", a cadeia"Write documentation"é um argumento que especifica a descrição da tarefa. Emcomplete 3, o número3é um argumento que especifica o ID da tarefa. -
As opções são valores nomeados precedidos por
--. Emadd --priority High --due 2026-04-01, tanto--prioritycomo--duesão opções com os seus próprios valores. Emlist --all, a--allopção é uma flag booleana que não precisa de valor. -
As opções globais aplicam-se a todos os subcomandos. A
--verboseopção está definida no comando raiz comRecursive = true, por isso funciona com qualquer subcomando. Em--verbose list, a flag verbosa aparece antes do subcomando, maslist --verbosefunciona igualmente bem.
Nas secções seguintes, constróis estas peças de baixo para cima. Primeiro, defines as opções individuais (como --priority e --all) e os argumentos (como a descrição da tarefa e o ID). De seguida, crias os quatro subcomandos e anexas as opções e argumentos relevantes a cada um. Depois configuramos uma ação para cada subcomando. A ação é o código que é executado quando o utilizador invoca esse comando. Finalmente, montas o comando raiz, analisas a entrada e invocas a ação correspondente.
Para uma análise mais profunda dos conceitos de sintaxe da linha de comandos, veja Visão geral da sintaxe da linha de comandos.
Definir opções e argumentos
As opções representam valores nomeados que os utilizadores especificam com um -- prefixo. Os argumentos representam valores posicionais. Ambos são fortemente tipados.
System.CommandLine Analisa a cadeia de entrada no tipo que especificas.
A System.CommandLine biblioteca utiliza tipos genéricos para garantir a segurança dos tipos. Quando se escreve Option<int>, o int dentro dos colchetes angulares é um argumento de tipo. Indica à biblioteca que tipo de valor a opção tem. A própria classe declara um parâmetroT de tipo (como em System.CommandLine.Option<T>), e forneces o tipo concreto quando crias uma instância. A biblioteca analisa a cadeia de entrada do utilizador e converte-a automaticamente para esse tipo. Se o utilizador fornecer --delay abc para um Option<int>, System.CommandLine reporta um erro de análise em vez de passar dados incorretos ao seu código. Verá este padrão com Option<bool>, Option<Priority>, Option<DateOnly?>, e System.CommandLine.Argument<T> nos passos que se seguem. Para obter mais informações sobre genéricos, consulte Genéricos.
Defina as opções. Cada um
Option<T>especifica o tipo de valor, o nome e uma descrição. A--priorityopção utiliza umenumtipo eSystem.CommandLinevalida automaticamente a entrada contra valores válidos do tipo enumerado:var verboseOption = new Option<bool>("--verbose") { Description = "Show detailed output", Recursive = true }; var priorityOption = new Option<Priority>("--priority") { Description = "Task priority level", DefaultValueFactory = _ => Priority.Medium }; var dueOption = new Option<DateOnly?>("--due") { Description = "Due date (uses current culture date format)" }; var allOption = new Option<bool>("--all") { Description = "Include completed tasks" };A definição em
Recursive = true--verbosetorna a opção disponível em todos os subcomandos. ODefaultValueFactoryligado--priorityfornece um valor padrão para que os utilizadores possam omitir essa opção.Defina os argumentos. Cada um
Argument<T>especifica o tipo de valor e um nome:var descriptionArgument = new Argument<string>("description") { Description = "Task description" }; var taskIdArgument = new Argument<int>("id") { Description = "Task ID" };
Comandos e subcomandos de build
A System.CommandLine.Command representa uma ação que o utilizador pode invocar. Adicione as opções e argumentos relevantes a cada comando para System.CommandLine saber quais os parâmetros que pertencem onde.
Crie os quatro subcomandos. Cada comando tem a sua própria combinação de opções e argumentos:
var addCommand = new Command("add", "Add a new task") { Arguments = { descriptionArgument }, Options = { priorityOption, dueOption } };var listCommand = new Command("list", "List all tasks") { Options = { allOption } };var completeCommand = new Command("complete", "Mark a task as complete") { Arguments = { taskIdArgument } };var removeCommand = new Command("remove", "Remove a task") { Arguments = { taskIdArgument } };Monta o comando raiz. Este System.CommandLine.RootCommand é o ponto de entrada para a CLI. Adicione a opção global
--verbosee todos os subcomandos:var rootCommand = new RootCommand("A simple task tracker CLI") { Options = { verboseOption }, Subcommands = { addCommand, listCommand, completeCommand, removeCommand } };O texto de ajuda gerado automaticamente mostra a descrição do comando raiz quando o utilizador executa
TaskCli --help.
Lidar com comandos por meio de ações
Cada subcomando precisa de uma ação. Uma ação é um delegado que se executa quando o utilizador invoca esse comando. Um delegado é um tipo que representa uma referência a um método. Aqui, passa uma expressão lambda (uma função anónima em linha definida com =>) como uma função de delegação. Chamar SetAction para atribuir cada ação. O delegado recebe um ParseResult que fornece acesso a valores analisados através de GetValue.
Define a ação para o
addcomando. Esta ação introduz a interpolação de cadeias ($"..."cadeias que incorporam expressões em chaves), o operador condicional (?:), e um padrão de teste de tipo (due is DateOnly dueDate) que verifica se um valor anulável tem um valor e o atribui a uma nova variável num passo:addCommand.SetAction(parseResult => { var description = parseResult.GetValue(descriptionArgument)!; var priority = parseResult.GetValue(priorityOption); var due = parseResult.GetValue(dueOption); var verbose = parseResult.GetValue(verboseOption); var tasks = LoadTasks(); var id = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1; var task = new TaskItem(id, description, priority, due, false); tasks.Add(task); SaveTasks(tasks); Console.WriteLine($"Added task {id}: {description}"); if (verbose) { Console.WriteLine($" Priority: {priority}"); if (due is DateOnly dueDate) { Console.WriteLine($" Due: {dueDate}"); } } });Define a ação para o
listcomando. O LINQ (Language Integrated Query) fornece operadores de consulta padrão para coleções em memória. Nesta ação, Enumerable.Where filtra as tarefas apenas para os itens que correspondem a uma condição e Enumerable.ToList materializa a sequência filtrada numa lista. A ação então usa umforeachciclo para iterar sobre os resultados, e o operador condicional para selecionar um símbolo de status:listCommand.SetAction(parseResult => { var showAll = parseResult.GetValue(allOption); var verbose = parseResult.GetValue(verboseOption); var tasks = LoadTasks(); var filtered = showAll ? tasks : tasks.Where(t => !t.IsComplete).ToList(); if (filtered.Count == 0) { Console.WriteLine("No tasks found."); return; } foreach (var task in filtered) { var status = task.IsComplete ? "✓" : " "; Console.WriteLine($" [{status}] {task.Id}: {task.Description}"); if (verbose) { Console.WriteLine($" Priority: {task.Priority}"); if (task.Due is DateOnly dueDate) { Console.WriteLine($" Due: {dueDate}"); } } } });Para mais detalhes, consulte LINQ.
Define a ação para o
completecomando. Esta ação utiliza LINQs Enumerable.FirstOrDefault para encontrar:- Uma tarefa de correspondência.
- Um
is nullpadrão para verificar se a tarefa existe. - Uma
withexpressão para criar uma nova instância de registo copiando primeiro os valores existentes e depois aplicando as propriedades que definiste nowithinicializador (aqui,IsComplete = true). Os registos são imutáveis por defeito, portanto, este padrão de copiar e atualizar é o método para produzir um valor modificado.
Como a ação pode falhar (por exemplo, o ID da tarefa não existe), a ação devolve um código de erro inteiro que se torna o código de saída da aplicação:
completeCommand.SetAction(parseResult => { var id = parseResult.GetValue(taskIdArgument); var verbose = parseResult.GetValue(verboseOption); var tasks = LoadTasks(); var task = tasks.FirstOrDefault(t => t.Id == id); if (task is null) { Console.Error.WriteLine($"Task {id} not found."); return -1; } tasks[tasks.IndexOf(task)] = task with { IsComplete = true }; SaveTasks(tasks); Console.WriteLine($"Completed task {id}: {task.Description}"); if (verbose) { Console.WriteLine($" Priority: {task.Priority}"); } return 0; });Define a ação para o
removecomando. Esta ação segue o mesmo padrão de procura e validação quecomplete. UseFirstOrDefaultpara encontrar a tarefa eis nulltratar do caso pendente:removeCommand.SetAction(parseResult => { var id = parseResult.GetValue(taskIdArgument); var verbose = parseResult.GetValue(verboseOption); var tasks = LoadTasks(); var task = tasks.FirstOrDefault(t => t.Id == id); if (task is null) { Console.Error.WriteLine($"Task {id} not found."); return -1; } tasks.Remove(task); SaveTasks(tasks); Console.WriteLine($"Removed task {id}: {task.Description}"); if (verbose) { Console.WriteLine($" Priority: {task.Priority}"); } return 0; });Analise a linha de comandos e invoque a ação correspondente:
return rootCommand.Parse(args).Invoke();rootCommand.Parse(args)analisa a entrada num ParseResult, e.Invoke()executa a ação para o comando emparelhado. O valor de retorno é um código de saída (0 para sucesso).
Adicionar tipos de suporte e auxiliares de dados
A aplicação precisa de alguns elementos de apoio: funções locais, um enum, a record, e um contexto de serialização. As secções seguintes apresentam cada conceito, explicam porque o escolheriam e mostram o código. A aplicação utiliza o System.Text.Json para armazenar tarefas como JSON (JavaScript Object Notation). As aplicações baseadas em ficheiros exigem que as declarações de tipo apareçam após todas as instruções de topo e funções locais.
Adiciona as funções locais que carregam e guardam tarefas. Uma função local é um método declarado dentro de outra função, incluindo dentro de outras funções locais. As funções locais mantêm a lógica auxiliar próxima do código que a chama, o que melhora a legibilidade porque um leitor não precisa de saltar para uma classe ou ficheiro separado para entender o fluxo. Aqui,
LoadTaskseSaveTasksencapsulam a entrada/saída de ficheiro que múltiplas ações de comando partilham, para que a aplicação escreva a lógica de carregar/guardar uma vez e a reutilize.static string GetTaskFilePath() => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "taskcli-sample", "tasks.json"); static List<TaskItem> LoadTasks() { var path = GetTaskFilePath(); if (!File.Exists(path)) { return []; } var json = File.ReadAllText(path); return JsonSerializer.Deserialize(json, TaskJsonContext.Default.ListTaskItem) ?? []; } static void SaveTasks(List<TaskItem> tasks) { var path = GetTaskFilePath(); Directory.CreateDirectory(Path.GetDirectoryName(path)!); var json = JsonSerializer.Serialize(tasks, TaskJsonContext.Default.ListTaskItem); File.WriteAllText(path, json); }Adicione o
Priorityenum e oTaskItemregisto no final do ficheiro.public enum Priority { Low, Medium, High }An
enumé um tipo de valor que define um conjunto fixo de constantes nomeadas suportadas por um tipo inteiro. Poderia representar os níveis de prioridade com inteiros simples (0, 1, 2), mas umenumé uma escolha melhor por várias razões: o compilador restringe as atribuições aos nomes definidos, de modo que um erro tipográfico comoHihgresulta num erro de compilação em vez de um bug silencioso; os nomesLow,Medium, eHightornam o código mais legível; eSystem.CommandLinevalida automaticamente a entrada do utilizador em relação aos membros do enum, proporcionando assim verificação de entrada gratuita.public record TaskItem(int Id, string Description, Priority Priority, DateOnly? Due, bool IsComplete);A
recordé um tipo que o compilador equipa com igualdade baseada em valores e mutação não destrutiva. Arecordé o ajuste correto paraTaskItemporque os dados da tarefa são estados simples sem comportamento complexo — compara-se as tarefas por seus valores, não pela identidade de referência. O compilador geraEquals,GetHashCode,ToString, ewithsuporte de expressões a partir dos parâmetros do construtor, isto assegura verificações corretas de igualdade, fácil geração de saída para depuração e atualizações imutáveis sem escrever código padrão. Como os registos são imutáveis por defeito, usas umawithexpressão para produzir uma cópia modificada (como acompleteação faz) em vez de mutar o original, o que previne efeitos secundários acidentais quando diferentes ações leem e escrevem a mesma lista.Adicione o contexto de serialização JSON para serialização compatível com AOT:
[System.Text.Json.Serialization.JsonSourceGenerationOptions(WriteIndented = true)] [System.Text.Json.Serialization.JsonSerializable(typeof(List<TaskItem>))] internal partial class TaskJsonContext : System.Text.Json.Serialization.JsonSerializerContext;Esta classe utiliza dois atributos (anotações de metadados colocadas entre parênteses acima de uma declaração). O
[JsonSourceGenerationOptions(WriteIndented = true)]atributo indica ao gerador de origem para emitir JSON indentado para maior legibilidade. O[JsonSerializable(typeof(List<TaskItem>))]atributo indica ao gerador para que tipo deve criar código de serialização. Em conjunto, estes atributos permitem a serialização gerada pela fonte, que evita reflexões em tempo de execução e suporta a compilação antecipada (AOT).
Testar a aplicação
Executa a aplicação com diferentes comandos para exercitar cada subcomando.
Veja a ajuda gerada automaticamente:
dotnet run TaskCli.cs -- --helpDescription: A simple task tracker CLI Usage: TaskCli [command] [options] Options: --verbose Show detailed output -?, -h, --help Show help and usage information --version Show version information Commands: add <description> Add a new task list List all tasks complete <id> Mark a task as complete remove <id> Remove a taskAdicione tarefas com diferentes opções:
dotnet run TaskCli.cs -- add "Write documentation" --priority High --due 2026-04-01 dotnet run TaskCli.cs -- add "Review pull request" dotnet run TaskCli.cs -- add "Fix build errors"Added task 1: Write documentation Added task 2: Review pull request Added task 3: Fix build errorsListe tarefas e use
--verbosepara mostrar detalhes extra:dotnet run TaskCli.cs -- --verbose list[ ] 1: Write documentation Priority: High Due: 4/1/2026 [ ] 2: Review pull request Priority: Medium [ ] 3: Fix build errors Priority: MediumCompletar uma tarefa e, em seguida, verificar se a lista filtra automaticamente as tarefas concluídas:
dotnet run TaskCli.cs -- complete 2 dotnet run TaskCli.cs -- listCompleted task 2: Review pull request [ ] 1: Write documentation [ ] 3: Fix build errorsUse
--allpara incluir tarefas concluídas:dotnet run TaskCli.cs -- list --all[ ] 1: Write documentation [✓] 2: Review pull request [ ] 3: Fix build errorsRemover uma tarefa:
dotnet run TaskCli.cs -- remove 3Removed task 3: Fix build errors
Limpeza de recursos
O rastreador de tarefas armazena os dados num ficheiro JSON na pasta local de dados da aplicação. Apague a taskcli-sample pasta para remover os dados de exemplo:
-
Windows: Apagar
%LOCALAPPDATA%\taskcli-sample. -
macOS: Apagar
~/Library/Application Support/taskcli-sample. -
Linux: Apagar
~/.local/share/taskcli-sample.