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
string
tipi 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
- Editor di codice, ad esempio Visual Studio Code con l'estensione C#.
- .NET 6 SDK.
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".
Creare una cartella denominata scl per il progetto e quindi aprire un prompt dei comandi nella nuova cartella.
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.
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é alrun
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 comandodotnet 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:
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.
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.
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.
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.");
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);
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);
Sostituire il
SetHandler
codice con il codice seguenteSetHandler
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.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
,--fgcolor
e--light-mode
add
comando con argomenti denominatiquote
ebyline
delete
comando con opzione denominata--search-terms
- Comando
* 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"
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
sutrue
. Se non si impostaisDefault
su , ilparseArgument
delegato non viene chiamato quando non viene fornito alcun input per--file
true
.Dopo il codice che crea
lightModeOption
, aggiungere opzioni e argomenti per iadd
comandi edelete
: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"
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
dalread
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 alquotes
comando anziché al comando radice.Crea
add
edelete
comandi e li aggiunge alquotes
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 (
quotes
read
,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.Dopo il
SetHandler
codice, aggiungere nuovo codice per i nuoviSetHandler
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. Sottocomandiread
,add
edelete
sono comandi foglia inquotes
eSetHandler
viene chiamato per ognuno di essi.Aggiungere i gestori per
add
edelete
.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 read
di , add
o 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 add
e 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.