Tutorial: Get started with System.CommandLine
Important
System.CommandLine
is currently in PREVIEW, and this documentation is for version 2.0 beta 4.
Some information relates to prerelease product that may be substantially modified before it's released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
This tutorial shows how to create a .NET command-line app that uses the System.CommandLine
library. You'll begin by creating a simple root command that has one option. Then you'll add to that base, creating a more complex app that contains multiple subcommands and different options for each command.
In this tutorial, you learn how to:
- Create commands, options, and arguments.
- Specify default values for options.
- Assign options and arguments to commands.
- Assign an option recursively to all subcommands under a command.
- Work with multiple levels of nested subcommands.
- Create aliases for commands and options.
- Work with
string
,string[]
,int
,bool
,FileInfo
and enum option types. - Bind option values to command handler code.
- Use custom code for parsing and validating options.
Prerequisites
- A code editor, such as Visual Studio Code with the C# extension.
- The .NET 6 SDK.
Or
- Visual Studio 2022 with the .NET desktop development workload installed.
Create the app
Create a .NET 6 console app project named "scl".
Create a folder named scl for the project, and then open a command prompt in the new folder.
Run the following command:
dotnet new console --framework net6.0
Install the System.CommandLine package
Run the following command:
dotnet add package System.CommandLine --prerelease
The
--prerelease
option is necessary because the library is still in beta.
Replace the contents of Program.cs with the following code:
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)); } }
The preceding code:
Creates an option named
--file
of type FileInfo and assigns it to the root command: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);
Specifies that
ReadFile
is the method that will be called when the root command is invoked:rootCommand.SetHandler((file) => { ReadFile(file!); }, fileOption);
Displays the contents of the specified file when the root command is invoked:
static void ReadFile(FileInfo file) { File.ReadLines(file.FullName).ToList() .ForEach(line => Console.WriteLine(line)); }
Test the app
You can use any of the following ways to test while developing a command-line app:
Run the
dotnet build
command, and then open a command prompt in the scl/bin/Debug/net6.0 folder to run the executable:dotnet build cd bin/Debug/net6.0 scl --file scl.runtimeconfig.json
Use
dotnet run
and pass option values to the app instead of to therun
command by including them after--
, as in the following example:dotnet run -- --file bin/Debug/net6.0/scl.runtimeconfig.json
The working directory is the project folder (the folder that has the .csproj file), so the relative path to scl.runtimeconfig.json
is from the project folder.
In .NET 7.0.100 SDK Preview, you can use the commandLineArgs
of a launchSettings.json file by running the command dotnet run --launch-profile <profilename>
.
Publish the project to a folder, open a command prompt to that folder, and run the executable:
dotnet publish -o publish cd ./publish scl --file scl.runtimeconfig.json
In Visual Studio 2022, select Debug > Debug Properties from the menu, and enter the options and arguments in the Command line arguments box. For example:
Then run the app, for example by pressing Ctrl+F5.
This tutorial assumes you're using the first of these options.
When you run the app, it displays the contents of the file specified by the --file
option.
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
}
}
}
Help output
System.CommandLine
automatically provides help output:
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
Version output
System.CommandLine
automatically provides version output:
scl --version
1.0.0
Add a subcommand and options
In this section, you:
- Create more options.
- Create a subcommand.
- Assign the new options to the new subcommand.
The new options will let you configure the foreground and background text colors and the readout speed. These features will be used to read a collection of quotes that comes from the Teleprompter console app tutorial.
Copy the sampleQuotes.txt file from the GitHub repository for this sample into your project directory. For information on how to download files, see the instructions in Samples and Tutorials.
Open the project file and add an
<ItemGroup>
element just before the closing</Project>
tag:<ItemGroup> <Content Include="sampleQuotes.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
Adding this markup causes the text file to be copied to the bin/debug/net6.0 folder when you build the app. So when you run the executable in that folder, you can access the file by name without specifying a folder path.
In Program.cs, after the code that creates the
--file
option, create options to control the readout speed and text colors: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.");
After the line that creates the root command, delete the line that adds the
--file
option to it. You're removing it here because you'll add it to a new subcommand.var rootCommand = new RootCommand("Sample app for System.CommandLine"); //rootCommand.AddOption(fileOption);
After the line that creates the root command, create a
read
subcommand. Add the options to this subcommand, and add the subcommand to the root command.var readCommand = new Command("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.AddCommand(readCommand);
Replace the
SetHandler
code with the followingSetHandler
code for the new subcommand:readCommand.SetHandler(async (file, delay, fgcolor, lightMode) => { await ReadFile(file!, delay, fgcolor, lightMode); }, fileOption, delayOption, fgcolorOption, lightModeOption);
You're no longer calling
SetHandler
on the root command because the root command no longer needs a handler. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app.Replace the
ReadFile
handler method with the following code: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); }; }
The app now looks like this:
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);
};
}
}
Test the new subcommand
Now if you try to run the app without specifying the subcommand, you get an error message followed by a help message that specifies the subcommand that is available.
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.
The help text for subcommand read
shows that four options are available. It shows valid values for the enum.
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
Run subcommand read
specifying only the --file
option, and you get the default values for the other three options.
scl read --file sampleQuotes.txt
The 42 milliseconds per character default delay causes a slow readout speed. You can speed it up by setting --delay
to a lower number.
scl read --file sampleQuotes.txt --delay 0
You can use --fgcolor
and --light-mode
to set text colors:
scl read --file sampleQuotes.txt --fgcolor red --light-mode
Provide an invalid value for --delay
and you get an error message:
scl read --file sampleQuotes.txt --delay forty-two
Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'.
Provide an invalid value for --file
and you get an exception:
scl read --file nofile
Unhandled exception: System.IO.FileNotFoundException:
Could not find file 'C:\bin\Debug\net6.0\nofile'.
Add subcommands and custom validation
This section creates the final version of the app. When finished, the app will have the following commands and options:
- root command with a global* option named
--file
quotes
commandread
command with options named--delay
,--fgcolor
, and--light-mode
add
command with arguments namedquote
andbyline
delete
command with option named--search-terms
* A global option is available to the command it's assigned to and recursively to all its subcommands.
Here's sample command line input that invokes each of the available commands with its options and arguments:
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, replace the code that creates the
--file
option with the following code: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); } });
This code uses ParseArgument<T> to provide custom parsing, validation, and error handling.
Without this code, missing files are reported with an exception and stack trace. With this code just the specified error message is displayed.
This code also specifies a default value, which is why it sets
isDefault
totrue
. If you don't setisDefault
totrue
, theparseArgument
delegate doesn't get called when no input is provided for--file
.After the code that creates
lightModeOption
, add options and arguments for theadd
anddelete
commands: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.");
The AllowMultipleArgumentsPerToken setting lets you omit the
--search-terms
option name when specifying elements in the list after the first one. It makes the following examples of command-line input equivalent:scl quotes delete --search-terms David "You can do" scl quotes delete --search-terms David --search-terms "You can do"
Replace the code that creates the root command and the
read
command with the following code: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);
This code makes the following changes:
Removes the
--file
option from theread
command.Adds the
--file
option as a global option to the root command.Creates a
quotes
command and adds it to the root command.Adds the
read
command to thequotes
command instead of to the root command.Creates
add
anddelete
commands and adds them to thequotes
command.
The result is the following command hierarchy:
- Root command
quotes
read
add
delete
The app now implements the recommended pattern where the parent command (
quotes
) specifies an area or group, and its children commands (read
,add
,delete
) are actions.Global options are applied to the command and recursively to subcommands. Since
--file
is on the root command, it will be available automatically in all subcommands of the app.After the
SetHandler
code, add newSetHandler
code for the new subcommands:deleteCommand.SetHandler((file, searchTerms) => { DeleteFromFile(file!, searchTerms); }, fileOption, searchTermsOption); addCommand.SetHandler((file, quote, byline) => { AddToFile(file!, quote, byline); }, fileOption, quoteArgument, bylineArgument);
Subcommand
quotes
doesn't have a handler because it isn't a leaf command. Subcommandsread
,add
, anddelete
are leaf commands underquotes
, andSetHandler
is called for each of them.Add the handlers for
add
anddelete
.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(); }
The finished app looks like this:
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();
}
}
Build the project, and then try the following commands.
Submit a nonexistent file to --file
with the read
command, and you get an error message instead of an exception and stack trace:
scl quotes read --file nofile
File does not exist
Try to run subcommand quotes
and you get a message directing you to use read
, add
, or 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.
Run subcommand add
, and then look at the end of the text file to see the added text:
scl quotes add "Hello world!" "Nancy Davolio"
Run subcommand delete
with search strings from the beginning of the file, and then look at the beginning of the text file to see where text was removed:
scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"
Note
If you're running in the bin/debug/net6.0 folder, that folder is where you'll find the file with changes from the add
and delete
commands. The copy of the file in the project folder remains unchanged.
Next steps
In this tutorial, you created a simple command-line app that uses System.CommandLine
. To learn more about the library, see System.CommandLine overview.