Compartir a través de


Tutorial: Compilación de una aplicación de línea de comandos con System.CommandLine

Sugerencia

Este artículo forma parte de la sección Aspectos básicos , escrito para desarrolladores que conocen al menos un lenguaje de programación y están aprendiendo C#. Si no está familiarizado con la programación, comience con Introducción. Si necesita cobertura completa de la biblioteca, consulte la documentación de la biblioteca System.CommandLine.

La System.CommandLine biblioteca controla el análisis de la línea de comandos, la generación de texto de ayuda y la validación de entrada para que pueda centrarse en la lógica de la aplicación. En este tutorial, creará una CLI de seguimiento de tareas que muestra los conceptos básicos: comandos, subcomandos, opciones y argumentos.

En este tutorial, usted hará lo siguiente:

  • Cree una aplicación basada en archivos mediante el System.CommandLine paquete.
  • Defina opciones y argumentos con valores tipados.
  • Cree subcomandos y adjunte opciones y argumentos a ellos.
  • Controle cada subcomando con una acción.
  • Pruebe la aplicación con entradas de línea de comandos diferentes.

Prerrequisitos

Creación de la aplicación

Empiece por crear un programa de C# basado en archivos y agregar el System.CommandLine paquete.

  1. Cree un archivo denominado TaskCli.cs con el siguiente contenido:

    #!/usr/bin/env dotnet
    

    La #! línea (shebang) le permite ejecutar el archivo directamente en sistemas Unix. En Windows, ejecute el archivo con dotnet run TaskCli.cs.

  2. Agregue la directiva del paquete System.CommandLine y las declaraciones necesarias using:

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

    Importante

    La versión 2.0.0 es la versión más reciente en el momento de escribir. Compruebe la página NuGet del paquete para obtener la versión más reciente para asegurarse de que tiene las correcciones de seguridad más recientes.

Descripción de la estructura de comandos

Antes de escribir cualquier código de análisis, tenga en cuenta el aspecto de la CLI desde la perspectiva del usuario. El rastreador de tareas admite cuatro operaciones:

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

Nota:

Después de TaskCli.cs en los ejemplos anteriores, dotnet run indica que todos los argumentos restantes se pasan a su aplicación en lugar de ser interpretados por la propia dotnet CLI.

Cada línea usa varios conceptos de línea de comandos:

  • Los subcomandos son verbos que indican a la aplicación qué hacer. El rastreador de tareas tiene cuatro: add, list, completey remove. Cada subcomando puede definir sus propios parámetros.
  • Los argumentos son valores posicionales que siguen un subcomando. En add "Write documentation", la cadena "Write documentation" es un argumento que especifica la descripción de la tarea. En complete 3, el número 3 es un argumento que especifica el identificador de tarea.
  • Las opciones son valores con nombre que tienen el prefijo --. En add --priority High --due 2026-04-01, y --priority--due son opciones con sus propios valores. En list --all, la --all opción es una marca booleana que no necesita un valor.
  • Las opciones globales se aplican a cada subcomando. La --verbose opción se define en el comando raíz con Recursive = true, por lo que funciona con cualquier subcomando. En --verbose list, la marca detallada aparece antes del subcomando, pero list --verbose funciona igualmente bien.

En las secciones siguientes, creará estas piezas de abajo hacia arriba. En primer lugar, defina las opciones individuales (como --priority y --all) y los argumentos (como la descripción de la tarea y el identificador). A continuación, cree los cuatro subcomandos y adjunte las opciones y argumentos pertinentes a cada uno. A continuación, asigne una acción para cada subcomando. La acción es el código que se ejecuta cuando el usuario invoca ese comando. Por último, ensambla el comando raíz, analiza la entrada e invoca la acción coincidente.

Para obtener un vistazo más profundo a los conceptos de sintaxis de la línea de comandos, consulte Introducción a la sintaxis de la línea de comandos.

Definir opciones y argumentos

Las opciones representan valores con nombre que los usuarios especifican con un -- prefijo. Los argumentos representan valores posicionales. Ambos están fuertemente tipados. System.CommandLine analiza la cadena de entrada al tipo que especifiques.

La System.CommandLine biblioteca usa tipos genéricos para aplicar la seguridad de tipos. Al escribir Option<int>, el int entre corchetes angulares es un argumento de tipo. Indica a la biblioteca qué tipo de valor contiene la opción. La propia clase declara un parámetroT de tipo (como en System.CommandLine.Option<T>), y se proporciona el tipo concreto al crear una instancia. La biblioteca analiza la cadena de entrada del usuario y la convierte automáticamente en ese tipo. Si el usuario proporciona --delay abc para Option<int>, System.CommandLine notifica un error de análisis sintáctico en vez de pasar datos incorrectos a su código. Verá este patrón con Option<bool>, Option<Priority>, Option<DateOnly?>y System.CommandLine.Argument<T> en los pasos siguientes. Para más información sobre genéricos, vea Generics.

  1. Defina las opciones. Cada Option<T> especifica el tipo de valor, el nombre y una descripción. La --priority opción usa un enum tipo y System.CommandLine valida automáticamente la entrada con los valores de enumeración válidos:

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

    La Recursive = true configuración de --verbose hace que la opción esté disponible en cada subcomando. El DefaultValueFactory en --priority proporciona un valor predeterminado para que los usuarios puedan omitir la opción.

  2. Defina los argumentos. Cada Argument<T> especifica el tipo de valor y un nombre:

    var descriptionArgument = new Argument<string>("description")
    {
        Description = "Task description"
    };
    
    var taskIdArgument = new Argument<int>("id")
    {
        Description = "Task ID"
    };
    

Comandos y subcomandos de compilación

System.CommandLine.Command representa una acción que el usuario puede invocar. Agregue las opciones y argumentos pertinentes a cada comando para System.CommandLine saber qué parámetros pertenecen a dónde.

  1. Cree los cuatro subcomandos. Cada comando obtiene su propia combinación de opciones y 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. Ensamblar el comando raíz. System.CommandLine.RootCommand es el punto de entrada de la CLI. Agregue la opción global --verbose y todos los subcomandos:

    var rootCommand = new RootCommand("A simple task tracker CLI")
    {
        Options = { verboseOption },
        Subcommands = { addCommand, listCommand, completeCommand, removeCommand }
    };
    

    El texto de ayuda generado automáticamente muestra la descripción del comando raíz cuando el usuario ejecuta TaskCli --help.

Manejo de comandos con acciones

Cada subcomando necesita una acción. Una acción es un delegado que se ejecuta cuando el usuario invoca ese comando. Un delegado es un tipo que representa una referencia a un método . Aquí, se pasa una expresión lambda (una función anónima insertada definida con =>) como delegado. Llame SetAction para asignar cada acción. El delegado recibe un ParseResult que proporciona acceso a los valores procesados a través de GetValue.

  1. Establezca la acción para el add comando. Esta acción presenta la interpolación de cadenas ($"..." cadenas que insertan expresiones entre llaves), el operador condicional (?:) y un patrón de prueba de tipo (due is DateOnly dueDate) que comprueba si un valor anulable tiene un valor y lo asigna a una nueva variable de un solo paso:

    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. Establezca la acción para el list comando. LINQ (Language Integrated Query) proporciona operadores de consulta estándar para colecciones en memoria. En esta acción, Enumerable.Where filtra las tareas solo a los elementos que coinciden con una condición y Enumerable.ToList materializa la secuencia filtrada en una lista. A continuación, la acción usa un foreach bucle para recorrer en iteración los resultados y el operador condicional para elegir un símbolo de estado:

    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 obtener más información, consulte LINQ.

  3. Establezca la acción para el complete comando. Esta acción usa LINQ Enumerable.FirstOrDefault para buscar:

    • Una tarea coincidente.
    • Patrón is null para comprobar si la tarea existe.
    • Expresiónwith para crear una nueva instancia de registro copiando primero los valores existentes y aplicando las propiedades establecidas en el with inicializador (aquí, IsComplete = true). Los registros son inmutables de forma predeterminada, por lo que este patrón de copia y actualización es cómo se genera un valor modificado.

    Dado que la acción puede producir un error (por ejemplo, el identificador de tarea no existe), la acción devuelve un código de error entero que se convierte en el código de salida de la aplicación:

    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. Establezca la acción para el remove comando. Esta acción sigue el mismo patrón de búsqueda y validación que complete. Use FirstOrDefault para buscar la tarea y is null para manejar el caso faltante.

    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. Analice la línea de comandos e invoque la acción coincidente:

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

    rootCommand.Parse(args) analiza la entrada en ParseResulty .Invoke() ejecuta la acción para el comando coincidente. El valor devuelto es un código de salida (0 para éxito).

Agregar tipos auxiliares y herramientas de datos

La aplicación necesita algunas partes auxiliares: funciones locales, , enuma recordy un contexto de serialización. En las secciones siguientes se presenta cada concepto, se explica por qué se elegiría y se muestra el código. La aplicación usa System.Text.Json para almacenar tareas como JSON (notación de objetos JavaScript). Las aplicaciones basadas en archivos requieren que aparezcan declaraciones de tipo después de todas las instrucciones de nivel superior y las funciones locales.

  1. Agregue las funciones locales que cargan y guardan tareas. Una función local es un método declarado dentro de otra función, incluida dentro de otras funciones locales. Las funciones locales mantienen la lógica auxiliar cerca del código que lo llama, lo que mejora la legibilidad porque un lector no tiene que saltar a una clase o archivo independiente para comprender el flujo. Aquí, LoadTasks y SaveTasks encapsulan la E/S de archivos compartida por múltiples acciones de comando, de modo que la aplicación escribe la lógica de carga/guardado una sola vez y la reutiliza.

    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. Agregue la Priority enumeración y TaskItem el registro al final del archivo:

    public enum Priority { Low, Medium, High }
    

    Un enum es un tipo de valor que define un conjunto fijo de constantes con nombre respaldadas por un tipo integral. Puede representar niveles de prioridad con enteros simples (0, 1, 2), pero una enum es una mejor elección por varias razones: el compilador restringe las asignaciones a los nombres definidos, de modo que un error tipográfico como Hihg provoca un error en tiempo de compilación en lugar de un error silencioso; los nombres Low, Medium, y High hacen que el código sea más legible; y System.CommandLine valida automáticamente la entrada del usuario en comparación con los miembros de la enumeración, por lo que obtiene una validación automática de entrada.

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

    Un record es un tipo que el compilador equipa con igualdad basada en valores y mutación no destructiva. Un record es la opción adecuada para TaskItem porque los datos de tareas son de estado simple sin ningún comportamiento complejo; las tareas se comparan por sus valores y no por su identidad de referencia. El compilador genera compatibilidad con Equals, GetHashCode, ToString y with a partir de los parámetros del constructor, por lo que se obtienen comprobaciones de igualdad correctas, salida de depuración sencilla y actualizaciones inmutables sin escribir código repetitivo. Dado que los registros son inmutables de forma predeterminada, se usa una with expresión para generar una copia modificada (como hace la complete acción) en lugar de mutar el original, lo que evita efectos secundarios accidentales cuando diferentes acciones leen y escriben la misma lista.

  3. Agregue el contexto de serialización JSON para la serialización compatible con 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 clase usa dos atributos (anotaciones de metadatos colocadas entre corchetes encima de una declaración). El atributo [JsonSourceGenerationOptions(WriteIndented = true)] indica al generador de origen que emita JSON con sangría para mejorar la legibilidad. El [JsonSerializable(typeof(List<TaskItem>))] atributo indica al generador para qué tipo se va a crear código de serialización. Juntos, estos atributos habilitan la serialización generada por código fuente, lo que evita la reflexión en tiempo de ejecución y admite la compilación AOT (Ahead Of Time).

Prueba de la aplicación

Ejecute la aplicación con diferentes entradas para ejercer cada subcomando.

  1. Vea la ayuda generada automáticamente:

    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. Agregue tareas con diferentes opciones:

    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. Enumerar tareas y usar --verbose para mostrar detalles adicionales:

    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. Complete una tarea y compruebe que los filtros de lista han completado las tareas de forma predeterminada:

    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 tareas completadas:

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

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

Limpieza de recursos

El rastreador de tareas almacena datos en un archivo JSON en la carpeta de datos de la aplicación local. Elimine la taskcli-sample carpeta para quitar los datos de ejemplo:

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