Lernprogramm: Erstellen einer Befehlszeilen-App mit "System.CommandLine"

Tipp

Dieser Artikel ist Teil des Abschnitts "Grundlagen" , der für Entwickler geschrieben wurde, die mindestens eine Programmiersprache kennen und C# erlernen. Wenn Sie noch nicht mit der Programmierung vertraut sind, beginnen Sie mit "Erste Schritte". Wenn Sie eine umfassende Bibliotheksabdeckung benötigen, lesen Sie die Dokumentation zur System.CommandLine-Bibliothek.

Die System.CommandLine Bibliothek behandelt die Befehlszeilenanalyse, die Hilfetextgenerierung und die Eingabeüberprüfung, sodass Sie sich auf die Logik Ihrer App konzentrieren können. In diesem Lernprogramm erstellen Sie eine Aufgabenverfolgungs-CLI, die die Kernkonzepte veranschaulicht: Befehle, Unterbefehle, Optionen und Argumente.

In diesem Tutorial lernen Sie Folgendes:

  • Erstellen Sie eine dateibasierte App mithilfe des System.CommandLine Pakets.
  • Definieren Sie Optionen und Argumente mit eingegebenen Werten.
  • Erstellen Von Unterbefehlen und Anfügen von Optionen und Argumenten.
  • Behandeln Sie jeden Unterbefehl mit einer Aktion.
  • Testen Sie die App mit unterschiedlichen Befehlszeileneingaben.

Voraussetzungen

Erstellen der App

Erstellen Sie zunächst ein dateibasiertes C#-Programm, und fügen Sie das System.CommandLine Paket hinzu.

  1. Erstellen Sie eine Datei mit dem Namen TaskCli.cs und dem folgenden Inhalt:

    #!/usr/bin/env dotnet
    

    Mit #! der Zeile (Shebang) können Sie die Datei direkt auf Unix-Systemen ausführen. Führen Sie unter Windows die Datei mit dotnet run TaskCli.cs.

  2. Fügen Sie die System.CommandLine Paketdirektive und die erforderlichen using Anweisungen hinzu:

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

    Von Bedeutung

    Version 2.0.0 ist die neueste Version zum Zeitpunkt des Schreibens. Überprüfen Sie die NuGet-Seite des Pakets auf die neueste Version, um sicherzustellen, dass Sie über die neuesten Sicherheitsupdates verfügen.

Befehlsstruktur verstehen

Berücksichtigen Sie vor dem Schreiben von Analysecode, wie die CLI aus Sicht des Benutzers aussieht. Die Aufgabenverfolgung unterstützt vier Vorgänge:

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

Hinweis

Nach dem -- in den vorhergehenden Beispielen wird dotnet run angezeigt, dass alle verbleibenden Argumente an Ihre App übergeben werden, anstatt von der dotnet CLI selbst interpretiert zu werden.

Jede Zeile verwendet mehrere Befehlszeilenkonzepte:

  • Unterbefehle sind Verben , die der App mitteilen, was zu tun ist. Die Aufgabenverfolgung hat vier: add, list, , completeund remove. Jeder Unterbefehl kann eigene Parameter definieren.
  • Argumente sind Positionswerte, die einem Unterbefehl folgen. In add "Write documentation", die Zeichenfolge "Write documentation" ist ein Argument, das die Aufgabenbeschreibung angibt. In complete 3, die Zahl 3 ist ein Argument, das die Vorgangs-ID angibt.
  • Optionen sind benannte Werte, die mit -- vorangestellt sind. In add --priority High --due 2026-04-01 sind --priority und --due Optionen mit ihren eigenen Werten. In list --all ist die --all-Option ein boolesches Flag, das keinen Wert benötigt.
  • Globale Optionen gelten für jeden Unterbefehl. Die --verbose Option wird für den Stammbefehl mit Recursive = truedefiniert, sodass sie mit jedem Unterbefehl funktioniert. In --verbose list, die ausführliche Kennzeichnung wird vor dem Unterbefehl angezeigt, funktioniert aber list --verbose genauso gut.

In den folgenden Abschnitten erstellen Sie diese Teile von unten nach oben. Zunächst definieren Sie die einzelnen Optionen (wie --priority und --all) sowie die Argumente (z. B. die Aufgabenbeschreibung und ID). Als Nächstes erstellen Sie die vier Unterbefehle, und fügen Sie die relevanten Optionen und Argumente jeweils an. Dann verknüpfen Sie eine Aktion mit jedem Unterbefehl. Die Aktion ist der Code, der ausgeführt wird, wenn der Benutzer diesen Befehl aufruft. Schließlich erstellen Sie den Stammbefehl, analysieren die Eingabe und rufen die übereinstimmene Aktion auf.

Einen tieferen Einblick in die Konzepte der Befehlszeilensyntax finden Sie in der Übersicht über die Befehlszeilensyntax.

Definieren von Optionen und Argumenten

Optionen stellen benannte Werte dar, die Benutzer mit einem -- Präfix angeben. Argumente stellen Positionswerte dar. Beide sind stark typisiert. System.CommandLine analysiert die Eingabezeichenfolge in den angegebenen Typ.

Die System.CommandLine Bibliothek verwendet generische Typen , um die Typsicherheit zu erzwingen. Beim Schreiben Option<int> ist der int Inhalt zwischen den spitzen Klammern ein Typargument. Sie teilt der Bibliothek mit, welche Art von Wert die Option enthält. Die Klasse selbst deklariert einen TypparameterT (wie in System.CommandLine.Option<T>), und Sie geben den konkreten Typ an, wenn Sie eine Instanz erstellen. Die Bibliothek analysiert die Eingabezeichenfolge des Benutzers und konvertiert sie automatisch in diesen Typ. Wenn der Benutzer --delay abc für ein Option<int> bereitstellt, meldet System.CommandLine einen Analysefehler, anstatt fehlerhafte Daten an Ihren Code zu übergeben. Dieses Muster wird mit Option<bool>, Option<Priority>, Option<DateOnly?>, und System.CommandLine.Argument<T> in den folgenden Schritten angezeigt. Weitere Informationen zu Generika finden Sie unter Generics.

  1. Definieren Sie die Optionen. Jeder Option<T> gibt den Werttyp, den Namen und eine Beschreibung an. Die --priority Option verwendet einen enum Typ und System.CommandLine überprüft die Eingabe automatisch anhand gültiger Enumerationswerte:

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

    Die Recursive = true Einstellung stellt --verbose die Option für jeden Unterbefehl zur Verfügung. Der DefaultValueFactory auf --priority bietet einen Standardwert, sodass Benutzer die Option weglassen können.

  2. Definieren Sie die Argumente. Jeder Argument<T> gibt den Werttyp und einen Namen an:

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

Erstellen von Befehlen und Unterbefehlen

A System.CommandLine.Command stellt eine Aktion dar, die der Benutzer aufrufen kann. Fügen Sie jedem Befehl die relevanten Optionen und Argumente hinzu, damit System.CommandLine erkennt, welche Parameter wo hingehören.

  1. Erstellen Sie die vier Unterbefehle. Jeder Befehl erhält eine eigene Kombination aus Optionen und Argumenten:

    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. Den Root-Befehl zusammenstellen. Dies System.CommandLine.RootCommand ist der Einstiegspunkt für die CLI. Fügen Sie die globale --verbose Option und alle Unterbefehle hinzu:

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

    Der automatisch generierte Hilfetext zeigt die Beschreibung des Root-Befehls an, wenn der Benutzer TaskCli --help ausführt.

Behandeln von Befehlen mit Aktionen

Jeder Unterbefehl benötigt eine Aktion. Eine Aktion ist ein Delegat, der ausgeführt wird, wenn der Benutzer diesen Befehl aufruft. Ein Delegat ist ein Typ, der einen Verweis auf eine Methode darstellt. Hier übergeben Sie einen Lambda-Ausdruck (eine inline anonyme Funktion, die mit => definiert wird) als Delegat. Rufen Sie SetAction auf, um jede Aktion zuzuweisen. Der Delegat erhält ein ParseResult, das Zugriff auf analysierte Werte durch GetValue ermöglicht.

  1. Legen Sie die Aktion für den add Befehl fest. Diese Aktion führt die Zeichenfolgeninterpolation ein ($"..." Zeichenfolgen, die Ausdrücke in geschweifte Klammern einbetten), den bedingten Operator () und ein ?: (due is DateOnly dueDate), mit dem überprüft wird, ob ein nullabler Wert einen Wert aufweist und einer neuen Variablen in einem Schritt zuweist:

    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. Legen Sie die Aktion für den list Befehl fest. LINQ (Language Integrated Query) bietet Ihnen standardmäßige Abfrageoperatoren für Speicherauflistungen. Filtert in dieser Aktion die Aufgaben nur auf die Elemente, Enumerable.Where die einer Bedingung entsprechen, und Enumerable.ToList materialisiert die gefilterte Sequenz in eine Liste. Die Aktion verwendet dann eine foreach Schleife, um die Ergebnisse zu durchlaufen, und den bedingten Operator, um ein Statussymbol auszuwählen.

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

    Weitere Details finden Sie unter LINQ.

  3. Legen Sie die Aktion für den complete Befehl fest. Diese Aktion verwendet LINQs Enumerable.FirstOrDefault , um Folgendes zu finden:

    • Eine Zuordnungsaufgabe.
    • Ein is null Muster zum Überprüfen, ob die Aufgabe vorhanden ist.
    • Ein with Ausdruck zum Erstellen einer neuen Datensatzinstanz durch Kopieren der vorhandenen Werte zuerst und anschließendes Anwenden der Eigenschaften, die with Sie im Initialisierer festgelegt haben (hier, IsComplete = true). Datensätze sind standardmäßig unveränderlich, daher verwenden Sie dieses Kopier- und Aktualisierungsmuster, um einen geänderten Wert zu erzeugen.

    Da die Aktion fehlschlagen kann (z. B. ist die Aufgaben-ID nicht vorhanden), gibt die Aktion einen ganzzahligen Fehlercode zurück, der zum Beendigungscode der App wird:

    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. Legen Sie die Aktion für den remove Befehl fest. Diese Aktion folgt demselben Nachschlage- und Überprüfungsmuster wie complete. Verwenden Sie FirstOrDefault, um die Aufgabe zu finden, und is null, um den fehlenden Fall zu behandeln.

    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. Analysieren Sie die Befehlszeile, und rufen Sie die übereinstimmene Aktion auf:

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

    rootCommand.Parse(args) analysiert die Eingabe in ein ParseResult, und .Invoke() führt die Aktion für den abgeglichenen Befehl aus. Der Rückgabewert ist ein Beendigungscode (0 für Erfolg).

Hinzufügen von unterstützenden Typen und Datenhilfsprogramme

Die App benötigt einige unterstützende Teile: lokale Funktionen, ein enum, ein record, und einen Serialisierungskontext. In den folgenden Abschnitten wird jedes Konzept vorgestellt, erläutert, warum Sie es auswählen und den Code anzeigen. Die App verwendet System.Text.Json zum Speichern von Aufgaben als JSON (JavaScript Object Notation). Dateibasierte Apps erfordern Typdeklarationen, die nach allen Anweisungen auf oberster Ebene und lokalen Funktionen angezeigt werden.

  1. Fügen Sie die lokalen Funktionen hinzu, mit denen Aufgaben geladen und gespeichert werden. Eine lokale Funktion ist eine Methode, die innerhalb einer anderen Funktion deklariert wird, einschließlich innerhalb anderer lokaler Funktionen. Lokale Funktionen behalten hilfslogik in der Nähe des Codes, der ihn aufruft, wodurch die Lesbarkeit verbessert wird, da ein Leser nicht zu einer separaten Klasse oder Datei springen muss, um den Fluss zu verstehen. LoadTasks und SaveTasks kapseln die Datei-E/A, die mehrere Befehlsaktionen gemeinsam nutzen, sodass die App die Lade-/Speicherlogik einmal schreibt und wiederverwendet:

    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. Fügen Sie Priority Enum und TaskItem Record am Ende der Datei hinzu.

    public enum Priority { Low, Medium, High }
    

    Ein enum Werttyp, der einen festen Satz benannter Konstanten definiert, die von einem integralen Typ unterstützt werden. Sie können Prioritätsebenen mit einfachen ganzzahligen Zahlen (0, 1, 2) darstellen, aber ein enum ist aus mehreren Gründen die bessere Wahl: Der Compiler schränkt Zuweisungen auf die definierten Namen ein, sodass ein Tippfehler wie Hihg einen Kompilierungsfehler anstelle eines stillen Fehlers verursacht; die Namen Low, Medium und High machen den Code besser lesbar; und System.CommandLine überprüft die Benutzereingabe automatisch gegen die Enum-Mitglieder, was eine automatische Eingabeüberprüfung beinhaltet.

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

    A record ist ein Typ, den der Compiler mit wertbasierter Gleichheit und nicht destruktiver Mutation ausstattet. A record ist die richtige Lösung für TaskItem, da Vorgangsdaten ein einfacher Zustand ohne komplexes Verhalten sind – Sie vergleichen Aufgaben anhand der Werte, nicht anhand der Referenzidentität. Der Compiler generiert Equals, GetHashCode, ToStringund with Ausdrucksunterstützung aus den Konstruktorparametern, sodass Sie korrekte Gleichheitsprüfungen, einfache Debugausgabe und unveränderliche Updates ohne Schreiben von Textbausteinen erhalten. Da Datensätze standardmäßig unveränderlich sind, verwenden Sie einen with Ausdruck, um eine modifizierte Kopie zu erzeugen (wie durch die complete Aktion), anstatt das Original zu verändern, was unbeabsichtigte Nebenwirkungen verhindert, wenn verschiedene Aktionen dieselbe Liste schreiben und lesen.

  3. Fügen Sie den JSON-Serialisierungskontext für AOT-kompatible Serialisierung hinzu:

    [System.Text.Json.Serialization.JsonSourceGenerationOptions(WriteIndented = true)]
    [System.Text.Json.Serialization.JsonSerializable(typeof(List<TaskItem>))]
    internal partial class TaskJsonContext : System.Text.Json.Serialization.JsonSerializerContext;
    

    Diese Klasse verwendet zwei Attribute (Metadatenanmerkungen, die in eckigen Klammern über einer Deklaration platziert werden). Das [JsonSourceGenerationOptions(WriteIndented = true)] Attribut weist den Quellgenerator an, eingezogenen JSON zur Lesbarkeit zu senden. Das [JsonSerializable(typeof(List<TaskItem>))] Attribut teilt dem Generator mit, für welchen Typ Serialisierungscode erstellt werden soll. Gemeinsam ermöglichen diese Attribute die vom Quelle generierte Serialisierung, wodurch Laufzeitreflektionen vermieden und die AOT-Kompilierung (Ahead-of-Time) unterstützt wird.

Testen der App

Führen Sie die App mit unterschiedlichen Eingaben aus, um jeden Unterbefehl auszuführen.

  1. Anzeigen der automatisch generierten Hilfe:

    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. Aufgaben mit verschiedenen Optionen hinzufügen:

    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. Listen Sie Aufgaben auf, und verwenden --verbose Sie diese, um zusätzliche Details anzuzeigen:

    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. Führen Sie eine Aufgabe aus und überprüfen Sie dann, ob die Listenfilter standardmäßig erledigte Aufgaben anzeigen:

    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. Verwenden Sie --all, um abgeschlossene Aufgaben einzuschließen.

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

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

Bereinigen von Ressourcen

Die Aufgabenverfolgung speichert Daten in einer JSON-Datei unter Ihrem lokalen Anwendungsdatenordner. Löschen Sie den taskcli-sample Ordner, um die Beispieldaten zu entfernen:

  • Windows: Löschen %LOCALAPPDATA%\taskcli-sample.
  • macOS: Delete ~/Library/Application Support/taskcli-sample.
  • Linux: Delete ~/.local/share/taskcli-sample.