Compartilhar via


Tutorial: Criar um aplicativo de linha de comando com System.CommandLine

Dica

Este artigo faz parte da seção Conceitos Básicos , escrita para desenvolvedores que conhecem pelo menos uma linguagem de programação e estão aprendendo C#. Se você não estiver familiarizado com a programação, comece com Introdução. Se você precisar de cobertura abrangente da biblioteca, consulte a documentação da biblioteca System.CommandLine.

A System.CommandLine biblioteca manipula a análise de linha de comando, a geração de texto de ajuda e a validação de entrada para que você possa se concentrar na lógica do aplicativo. Neste tutorial, você criará uma CLI do rastreador de tarefas que demonstra os principais conceitos: comandos, subcomandos, opções e argumentos.

Neste tutorial, você:

  • Crie um aplicativo baseado em arquivo usando o System.CommandLine pacote.
  • Defina opções e argumentos com valores tipados.
  • Crie subcomandos e anexe opções e argumentos a eles.
  • Execute cada subcomando com uma ação.
  • Teste o aplicativo com entradas de linha de comando diferentes.

Pré-requisitos

Criar o aplicativo

Comece criando um programa C# baseado em arquivo e adicionando o System.CommandLine pacote.

  1. Crie um arquivo nomeado TaskCli.cs com o seguinte conteúdo:

    #!/usr/bin/env dotnet
    

    A linha shebang #! permite executar o arquivo diretamente nos sistemas Unix. No Windows, execute o arquivo com dotnet run TaskCli.cs.

  2. Adicione a diretiva de pacote System.CommandLine e as instruções using necessárias.

    #:package System.CommandLine@2.0.0
    
    using System.CommandLine;
    using System.CommandLine.Parsing;
    using System.Text.Json;
    

    Importante

    A versão 2.0.0 é a versão mais recente no momento da gravação. Verifique a página NuGet do pacote para obter a versão mais recente para garantir que você tenha as correções de segurança mais recentes.

Entender a estrutura de comandos

Antes de escrever qualquer código de análise, considere a aparência da CLI da perspectiva do usuário. O rastreador de tarefas dá suporte a 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

O -- após TaskCli.cs nos exemplos precedentes informa dotnet run que todos os argumentos restantes são enviados para o seu aplicativo, em vez de serem interpretados pela própria dotnet CLI.

Cada linha usa vários conceitos de linha de comando:

  • Subcomandos são verbos que dizem ao aplicativo o que fazer. O rastreador de tarefas tem quatro: add, list, e completeremove. Cada subcomando pode definir seus próprios parâmetros.
  • Argumentos são valores posicionais que seguem um subcomando. Em add "Write documentation", a cadeia de caracteres "Write documentation" é um argumento que especifica a descrição da tarefa. Em complete 3, o número 3 é um argumento que especifica a ID da tarefa.
  • As opções são valores nomeados prefixados com --. Em add --priority High --due 2026-04-01, ambos --priority e --due são opções com seus próprios valores. Em list --all, a opção --all é um sinalizador booliano que não precisa de um valor.
  • As opções globais se aplicam a cada subcomando. A --verbose opção é definida no comando raiz com Recursive = true, portanto, funciona com qualquer subcomando. Em --verbose list, o flag verboso aparece antes do subcomando, mas list --verbose funciona igualmente bem.

Nas seções a seguir, você cria essas peças de baixo para cima. Primeiro, você define as opções individuais (como --priority e --all) e argumentos (como a descrição e a ID da tarefa). Em seguida, crie os quatro subcomandos e anexe as opções e argumentos relevantes a cada um deles. Em seguida, você conecta uma ação para cada subcomando. A ação é o código executado quando o usuário invoca esse comando. Por fim, você monta o comando raiz, analisa a entrada e invoca a ação correspondente.

Para obter uma visão mais detalhada dos conceitos de sintaxe de linha de comando, consulte a visão geral da sintaxe de linha de comando.

Definir opções e argumentos

As opções representam valores nomeados que os usuários especificam com um -- prefixo. Os argumentos representam valores posicionais. Ambos são fortemente tipados. System.CommandLine analisa a cadeia de caracteres de entrada no tipo especificado.

A System.CommandLine biblioteca usa tipos genéricos para garantir a segurança de tipos. Quando você escreve Option<int>, o int entre os colchetes angulares é um argumento de tipo. Ele informa à biblioteca qual tipo de valor a opção contém. A própria classe declara um parâmetroT de tipo (como em System.CommandLine.Option<T>) e você fornece o tipo concreto ao criar uma instância. A biblioteca analisa a cadeia de caracteres de entrada do usuário e a converte automaticamente nesse tipo. Se o usuário fornecer --delay abc para um Option<int>, System.CommandLine relatará um erro de análise em vez de passar dados incorretos para seu código. Você verá esse padrão com Option<bool>, Option<Priority>Option<DateOnly?>e System.CommandLine.Argument<T> nas etapas a seguir. Confira mais informações sobre genéricos em Genéricos.

  1. Defina as opções. Cada Option<T> um especifica o tipo de valor, o nome e uma descrição. A --priority opção usa um enum tipo e System.CommandLine valida automaticamente a entrada em relação aos valores válidos de enum:

    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 Recursive = true configuração em --verbose torna a opção disponível em cada subcomando. O DefaultValueFactory on --priority fornece um valor padrão para que os usuários possam omitir a opção.

  2. Defina os argumentos. Cada Argument<T> um 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"
    };
    

Criar comandos e subcomandos

System.CommandLine.Command representa uma ação que o usuário pode invocar. Adicione as opções e argumentos relevantes a cada comando para que System.CommandLine saiba quais parâmetros pertencem a cada local.

  1. Crie os quatro subcomandos. Cada comando obtém 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 }
    };
    
  2. Monte o comando root. O System.CommandLine.RootCommand é o ponto de entrada para a CLI. Adicione a opção global --verbose e 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 usuário executa TaskCli --help.

Manipular comandos com ações

Cada subcomando precisa de uma ação. Uma ação é um delegado que é executado quando o usuário invoca esse comando. Um delegado é um tipo que representa uma referência a um método. Aqui, você passa uma expressão lambda (uma função anônima embutida definida com =>) como delegado. Chame SetAction para atribuir cada ação. O delegado recebe um ParseResult que fornece acesso a valores analisados por meio de GetValue.

  1. Defina a ação para o add comando. Essa ação introduz interpolação de strings ($"..." strings que incorporam expressões entre chaves), o operador condicional (?:) e um padrão de teste de tipo (due is DateOnly dueDate) que verifica se um valor anulável possui um valor e o atribui a uma nova variável em uma única etapa:

    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}");
            }
        }
    });
    
  2. Defina a ação para o list comando. LINQ (Consulta Integrada à Linguagem) fornece operadores de consulta padrão para coleções na memória. Nesta ação, Enumerable.Where filtra as tarefas somente para os itens que correspondem a uma condição e Enumerable.ToList materializa a sequência filtrada em uma lista. Em seguida, a ação usa um foreach loop para iterar sobre os resultados e o operador condicional para definir 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 obter mais detalhes, consulte LINQ.

  3. Defina a ação para o complete comando. Esta ação usa LINQ's Enumerable.FirstOrDefault para localizar:

    • Uma tarefa correspondente.
    • Um is null padrão para verificar se a tarefa existe.
    • Uma with expressão para criar uma nova instância de registro copiando os valores existentes primeiro e, em seguida, aplicando as propriedades definidas no with inicializador (aqui, IsComplete = true). Os registros são imutáveis por padrão, portanto, esse padrão de cópia e atualização é como você produz um valor modificado.

    Como a ação pode falhar (por exemplo, a ID da tarefa não existe), a ação retorna um código de erro inteiro que se torna o código de saída do aplicativo:

    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;
    });
    
  4. Defina a ação para o remove comando. Essa ação segue o mesmo padrão de pesquisa e validação que complete. Use FirstOrDefault para localizar a tarefa e is null lidar com o caso ausente:

    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;
    });
    
  5. Analise a linha de comando e invoque a ação correspondente:

    return rootCommand.Parse(args).Invoke();
    

    rootCommand.Parse(args) analisa a entrada em um ParseResulte .Invoke() executa a ação para o comando correspondente. O valor retornado é um código de saída (0 para êxito).

Adicionar tipos de suporte e auxiliares de dados

O aplicativo precisa de algumas partes de suporte: funções locais, um enum, um recorde um contexto de serialização. As seções a seguir introduzem cada conceito, explicam por que você o escolheria e mostram o código. O aplicativo usa System.Text.Json para armazenar tarefas como JSON (JavaScript Object Notation). Aplicativos baseados em arquivo exigem que declarações de tipo apareçam após todas as instruções de nível superior e funções locais.

  1. Adicione as funções locais que carregam e salvam 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 ao código que a chama, o que melhora a legibilidade porque um leitor não precisa ir para uma classe ou arquivo separado para entender o fluxo. Aqui, LoadTasks e SaveTasks encapsulam a entrada/saída de arquivo que várias ações de comando compartilham, para que o aplicativo grave a lógica de carregamento/salvamento 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);
    }
    
  2. Adicione o Priority enum e o TaskItem registro no final do arquivo.

    public enum Priority { Low, Medium, High }
    

    Um enum é um tipo de valor que define um conjunto fixo de constantes nomeadas apoiadas por um tipo integral. Você pode representar níveis de prioridade com inteiros simples (0, 1, 2), mas uma enum opção é melhor por vários motivos: o compilador restringe as atribuições aos nomes definidos, portanto, um erro de digitação como Hihg causa um erro de tempo de compilação em vez de um bug silencioso; os nomes LowMediume High tornam o código mais legível; e System.CommandLine valida automaticamente a entrada do usuário nos membros de enumeração, para que você obtenha verificação de entrada gratuita.

    public record TaskItem(int Id, string Description, Priority Priority, DateOnly? Due, bool IsComplete);
    

    Um record é um tipo que o compilador equipa com igualdade baseada em valor e mutação não destrutiva. Um record é adequado para TaskItem porque os dados da tarefa são um estado simples sem comportamento complexo; você compara tarefas por seus valores, não pela identidade de referência. O compilador gera Equals, GetHashCode, ToString e with com suporte para expressões a partir dos parâmetros do construtor, para que você obtenha verificações de igualdade corretas, saída de depuração fácil e atualizações imutáveis sem escrever código repetitivo. Como os registros são imutáveis por padrão, você usa uma with expressão para produzir uma cópia modificada (como a ação complete faz) em vez de alterar o original, o que impede efeitos colaterais acidentais quando ações diferentes leem e gravam a mesma lista.

  3. 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;
    

    Essa classe usa dois atributos (anotações de metadados colocadas em colchetes acima de uma declaração). O atributo [JsonSourceGenerationOptions(WriteIndented = true)] informa ao gerador de código-fonte a emitir JSON formatado para legibilidade. O [JsonSerializable(typeof(List<TaskItem>))] atributo informa ao gerador para qual tipo criar código de serialização. Juntos, esses atributos permitem a serialização gerada pela fonte, o que evita a reflexão em tempo de execução e dá suporte à compilação antecipada em tempo (AOT).

Testar o aplicativo

Execute o aplicativo com entradas diferentes para exercitar cada subcomando.

  1. Exiba a ajuda gerada automaticamente:

    dotnet run TaskCli.cs -- --help
    
    Description:
      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 task
    
  2. Adicione tarefas com opções diferentes:

    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 errors
    
  3. Listar tarefas e usar --verbose para mostrar detalhes extras:

    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: Medium
    
  4. Conclua uma tarefa e verifique se a lista filtra as tarefas concluídas por padrão:

    dotnet run TaskCli.cs -- complete 2
    dotnet run TaskCli.cs -- list
    
    Completed task 2: Review pull request
      [ ] 1: Write documentation
      [ ] 3: Fix build errors
    
  5. Use --all para incluir tarefas concluídas:

    dotnet run TaskCli.cs -- list --all
    
      [ ] 1: Write documentation
      [✓] 2: Review pull request
      [ ] 3: Fix build errors
    
  6. Remova uma tarefa:

    dotnet run TaskCli.cs -- remove 3
    
    Removed task 3: Fix build errors
    

Limpar os recursos

O rastreador de tarefas armazena dados em um arquivo JSON na pasta de dados do aplicativo local. Exclua a taskcli-sample pasta para remover os dados de exemplo:

  • Windows: Excluir %LOCALAPPDATA%\taskcli-sample.
  • macOS: Excluir ~/Library/Application Support/taskcli-sample.
  • Linux: Excluir ~/.local/share/taskcli-sample.