Esercitazione: Introduzione a System.CommandLine

Importante

System.CommandLine è attualmente disponibile in ANTEPRIMA e questa documentazione è relativa alla versione 2.0 beta 4. Alcune informazioni si riferiscono al prodotto in versione preliminare che può essere modificato in modo sostanziale prima del rilascio. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Questa esercitazione illustra come creare un'app da riga di comando .NET che usa la System.CommandLine libreria. Si inizierà creando un semplice comando radice con un'unica opzione. Si aggiungerà quindi a tale base, creando un'app più complessa che contiene più sottocomandi e opzioni diverse per ogni comando.

In questa esercitazione verranno illustrate le procedure per:

  • Creare comandi, opzioni e argomenti.
  • Specificare i valori predefiniti per le opzioni.
  • Assegnare opzioni e argomenti ai comandi.
  • Assegnare un'opzione in modo ricorsivo a tutti i sottocomandi in un comando.
  • Usare più livelli di sottocomandi annidati.
  • Creare alias per comandi e opzioni.
  • Usare i stringtipi di opzione , string[]int, bool, FileInfo enum.
  • Associare i valori delle opzioni al codice del gestore dei comandi.
  • Usare codice personalizzato per l'analisi e la convalida delle opzioni.

Prerequisiti

Oppure

  • Visual Studio 2022 con il carico di lavoro Sviluppo di applicazioni desktop .NET installato.

Creare l'app

Creare un progetto di app console .NET 6 denominato "scl".

  1. Creare una cartella denominata scl per il progetto e quindi aprire un prompt dei comandi nella nuova cartella.

  2. Eseguire il comando seguente:

    dotnet new console --framework net6.0
    

Installare il pacchetto System.CommandLine

  • Eseguire il comando seguente:

    dotnet add package System.CommandLine --prerelease
    

    L'opzione --prerelease è necessaria perché la libreria è ancora in versione beta.

  1. Sostituire il contenuto di Program.cs con il codice seguente:

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

Il codice precedente:

  • Crea un'opzione denominata --file di tipo FileInfo e la assegna al comando radice:

    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);
    
  • Specifica che ReadFile è il metodo che verrà chiamato quando viene richiamato il comando radice:

    rootCommand.SetHandler((file) => 
        { 
            ReadFile(file!); 
        },
        fileOption);
    
  • Visualizza il contenuto del file specificato quando viene richiamato il comando radice:

    static void ReadFile(FileInfo file)
    {
        File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
    }
    

Testare l'app

È possibile usare uno dei modi seguenti per testare durante lo sviluppo di un'app da riga di comando:

  • Eseguire il dotnet build comando e quindi aprire un prompt dei comandi nella cartella scl/bin/Debug/net6.0 per eseguire il file eseguibile:

    dotnet build
    cd bin/Debug/net6.0
    scl --file scl.runtimeconfig.json
    
  • Usare dotnet run e passare i valori delle opzioni all'app anziché al run comando includendoli dopo --, come nell'esempio seguente:

    dotnet run -- --file scl.runtimeconfig.json
    

    In .NET 7.0.100 SDK Preview è possibile usare di commandLineArgs un file launchSettings.json eseguendo il comando dotnet run --launch-profile <profilename>.

  • Pubblicare il progetto in una cartella, aprire un prompt dei comandi in tale cartella ed eseguire il file eseguibile:

    dotnet publish -o publish
    cd ./publish
    scl --file scl.runtimeconfig.json
    
  • In Visual Studio 2022 selezionare Debug>Proprietà debug dal menu e immettere le opzioni e gli argomenti nella casella Argomenti della riga di comando . Ad esempio:

    Argomenti della riga di comando in Visual Studio 2022

    Eseguire quindi l'app, ad esempio premendo CTRL+F5.

Questa esercitazione presuppone che si usi la prima di queste opzioni.

Quando si esegue l'app, viene visualizzato il contenuto del file specificato dall'opzione --file .

{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    }
  }
}

Output della Guida

System.CommandLine fornisce automaticamente l'output della Guida:

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

Output della versione

System.CommandLine fornisce automaticamente l'output della versione:

scl --version
1.0.0

Aggiungere un sottocomando e opzioni

In questa sezione verrà illustrato come:

  • Crea altre opzioni.
  • Creare un sottocomando.
  • Assegnare le nuove opzioni al nuovo sottocomando.

Le nuove opzioni consentono di configurare i colori del testo in primo piano e di sfondo e la velocità di lettura. Queste funzionalità verranno usate per leggere una raccolta di virgolette provenienti dall'esercitazione sull'app console Teleprompter.

  1. Copiare il file sampleQuotes.txt dal repository GitHub di questo esempio alla directory del progetto. Per informazioni su come scaricare i file, vedere le istruzioni in Esempi ed esercitazioni.

  2. Aprire il file di progetto e aggiungere un <ItemGroup> elemento subito prima del tag di chiusura </Project> :

    <ItemGroup>
      <Content Include="sampleQuotes.txt">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </Content>
    </ItemGroup>
    

    L'aggiunta di questo markup fa sì che il file di testo venga copiato nella cartella bin/debug/net6.0 quando si compila l'app. Pertanto, quando si esegue il file eseguibile in tale cartella, è possibile accedere al file in base al nome senza specificare un percorso di cartella.

  3. In Program.cs, dopo il codice che crea l'opzione --file , creare le opzioni per controllare la velocità di lettura e i colori del testo:

    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.");
    
  4. Dopo la riga che crea il comando radice, eliminare la riga che aggiunge l'opzione --file . Questa operazione verrà rimossa perché verrà aggiunta a un nuovo sottocomando.

    var rootCommand = new RootCommand("Sample app for System.CommandLine");
    //rootCommand.AddOption(fileOption);
    
  5. Dopo la riga che crea il comando radice, creare un read sottocomando. Aggiungere le opzioni a questo sottocomando e aggiungere il sottocomando al comando radice.

    var readCommand = new Command("read", "Read and display the file.")
        {
            fileOption,
            delayOption,
            fgcolorOption,
            lightModeOption
        };
    rootCommand.AddCommand(readCommand);
    
  6. Sostituire il SetHandler codice con il codice seguente SetHandler per il nuovo sottocomando:

    readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
        {
            await ReadFile(file!, delay, fgcolor, lightMode);
        },
        fileOption, delayOption, fgcolorOption, lightModeOption);
    

    Non si chiama SetHandler più sul comando radice perché il comando radice non richiede più un gestore. Quando un comando include sottocomandi, in genere è necessario specificare uno dei sottocomandi quando si richiama un'app da riga di comando.

  7. Sostituire il ReadFile metodo del gestore con il codice seguente:

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

L'app è ora simile alla seguente:

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

Testare il nuovo sottocomando

Se si tenta di eseguire l'app senza specificare il sottocomando, viene visualizzato un messaggio di errore seguito da un messaggio della Guida che specifica il sottocomando disponibile.

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.

Il testo della Guida per il sottocomando read mostra che sono disponibili quattro opzioni. Mostra i valori validi per l'enumerazione.

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

Eseguire sottocomando read specificando solo l'opzione --file e si ottengono i valori predefiniti per le altre tre opzioni.

scl read --file sampleQuotes.txt

Il ritardo predefinito di 42 millisecondi per carattere causa una velocità di lettura lenta. È possibile velocizzarla impostando --delay un numero inferiore.

scl read --file sampleQuotes.txt --delay 0

È possibile usare --fgcolor e --light-mode per impostare i colori del testo:

scl read --file sampleQuotes.txt --fgcolor red --light-mode

Specificare un valore non valido per --delay e viene visualizzato un messaggio di errore:

scl read --file sampleQuotes.txt --delay forty-two
Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'.

Specificare un valore non valido per --file e si ottiene un'eccezione:

scl read --file nofile
Unhandled exception: System.IO.FileNotFoundException:
Could not find file 'C:\bin\Debug\net6.0\nofile'.

Aggiungere sottocomandi e convalida personalizzata

Questa sezione crea la versione finale dell'app. Al termine, l'app avrà i comandi e le opzioni seguenti:

  • comando root con un'opzione globale* denominata --file
    • Comando quotes
      • read comando con opzioni denominate --delay, --fgcolore --light-mode
      • add comando con argomenti denominati quote e byline
      • delete comando con opzione denominata --search-terms

* Un'opzione globale è disponibile per il comando a cui è assegnata e in modo ricorsivo a tutti i relativi sottocomandi.

Ecco l'input della riga di comando di esempio che richiama ognuno dei comandi disponibili con le relative opzioni e argomenti:

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"
  1. In Program.cs sostituire il codice che crea l'opzione --file con il codice seguente:

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

    Questo codice usa ParseArgument<T> per fornire analisi, convalida e gestione degli errori personalizzati.

    Senza questo codice, i file mancanti vengono segnalati con un'eccezione e un'analisi dello stack. Con questo codice viene visualizzato solo il messaggio di errore specificato.

    Questo codice specifica anche un valore predefinito, motivo per cui imposta isDefault su true. Se non si imposta isDefault su , il parseArgument delegato non viene chiamato quando non viene fornito alcun input per --filetrue.

  2. Dopo il codice che crea lightModeOption, aggiungere opzioni e argomenti per i add comandi e delete :

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

    L'impostazione AllowMultipleArgumentsPerToken consente di omettere il --search-terms nome dell'opzione quando si specificano elementi nell'elenco dopo il primo. Rende gli esempi seguenti di input da riga di comando equivalenti:

    scl quotes delete --search-terms David "You can do"
    scl quotes delete --search-terms David --search-terms "You can do"
    
  3. Sostituire il codice che crea il comando radice e il comando con il read codice seguente:

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

    Questo codice apporta le modifiche seguenti:

    • Rimuove l'opzione --file dal read comando.

    • Aggiunge l'opzione --file come opzione globale al comando radice.

    • Crea un quotes comando e lo aggiunge al comando radice.

    • Aggiunge il read comando al quotes comando anziché al comando radice.

    • Crea add e delete comandi e li aggiunge al quotes comando.

    Il risultato è la gerarchia di comandi seguente:

    • Comando radice
      • quotes
        • read
        • add
        • delete

    L'app implementa ora il modello consigliato in cui il comando padre () specifica un'area o un gruppo e i relativi comandi figlio (quotesread, add, delete) sono azioni.

    Le opzioni globali vengono applicate al comando e ricorsivamente ai sottocomandi. Poiché --file si trova nel comando radice, sarà disponibile automaticamente in tutti i sottocomandi dell'app.

  4. Dopo il SetHandler codice, aggiungere nuovo codice per i nuovi SetHandler sottocomandi:

    deleteCommand.SetHandler((file, searchTerms) =>
        {
            DeleteFromFile(file!, searchTerms);
        },
        fileOption, searchTermsOption);
    
    addCommand.SetHandler((file, quote, byline) =>
        {
            AddToFile(file!, quote, byline);
        },
        fileOption, quoteArgument, bylineArgument);
    

    Subcommand quotes non ha un gestore perché non è un comando foglia. Sottocomandi read, adde delete sono comandi foglia in quotese SetHandler viene chiamato per ognuno di essi.

  5. Aggiungere i gestori per add e delete.

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

L'app completata ha un aspetto simile al seguente:

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

Compilare il progetto e quindi provare i comandi seguenti.

Inviare un file inesistente a --file con il read comando e viene visualizzato un messaggio di errore anziché un'eccezione e una traccia dello stack:

scl quotes read --file nofile
File does not exist

Provare a eseguire sottocomande quotes e si riceve un messaggio che indirizza all'uso readdi , addo 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.

Eseguire sottocomand adde quindi esaminare la fine del file di testo per visualizzare il testo aggiunto:

scl quotes add "Hello world!" "Nancy Davolio"

Eseguire sottocomand delete con stringhe di ricerca dall'inizio del file e quindi esaminare l'inizio del file di testo per vedere dove è stato rimosso il testo:

scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"

Nota

Se si esegue nella cartella bin/debug/net6.0 , tale cartella è la posizione in cui si trova il file con modifiche dai add comandi e delete . La copia del file nella cartella del progetto rimane invariata.

Passaggi successivi

In questa esercitazione è stata creata un'app da riga di comando semplice che usa System.CommandLine. Per altre informazioni sulla libreria, vedere System.CommandLine panoramica.