πŸ•·οΈ Crawler Inspector

URL Lookup

Direct Parameter Lookup

Raw Queries and Responses

1. Shard Calculation

Query:
Response:
Calculated Shard: 168 (from laksa001)

2. Crawled Status Check

Query:
Response:

3. Robots.txt Check

Query:
Response:

4. Spam/Ban Check

Query:
Response:

5. Seen Status Check

ℹ️ Skipped - page is already crawled

πŸ“„
INDEXABLE
βœ…
CRAWLED
10 days ago
πŸ€–
ROBOTS ALLOWED

Page Info Filters

FilterStatusConditionDetails
HTTP statusPASSdownload_http_code = 200HTTP 200
Age cutoffPASSdownload_stamp > now() - 6 MONTH0.3 months ago (distributed domain, exempt)
History dropPASSisNull(history_drop_reason)No drop reason
Spam/banPASSfh_dont_index != 1 AND ml_spam_score = 0ml_spam_score=0
CanonicalPASSmeta_canonical IS NULL OR = '' OR = src_unparsedNot set

Page Details

PropertyValue
URLhttps://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial
Last Crawled2026-04-02 14:46:38 (10 days ago)
First Indexed2022-09-25 06:28:25 (3 years ago)
HTTP Status Code200
Meta TitleTutorial: Get started with System.CommandLine - .NET | Microsoft Learn
Meta DescriptionLearn how to use the System.CommandLine library for command-line apps.
Meta Canonicalnull
Boilerpipe Text
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 build on 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. Read option values in command action code. Use custom code for parsing and validating options. The latest .NET SDK Visual Studio Code editor The C# DevKit Or Visual Studio with the .NET desktop development workload installed. Create a .NET 9 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 net9. 0 Run the following command: dotnet add package System.CommandLine Or, in .NET 10+: dotnet package add System.CommandLine Replace the contents of Program.cs with the following code: using System.CommandLine; using System.CommandLine.Parsing; namespace scl ; class Program { static int Main ( string [] args ) { Option<FileInfo> fileOption = new ( "--file" ) { Description = "The file to read and display on the console" }; RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); rootCommand.Options.Add(fileOption); ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0 ; } foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1 ; } static void ReadFile ( FileInfo file ) { foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } The preceding code: Creates an option named --file of type FileInfo and adds it to the root command : Option<FileInfo> fileOption = new ( "--file" ) { Description = "The file to read and display on the console" }; RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); rootCommand.Options.Add(fileOption); Parses args and checks whether any value was provided for the --file option. If so, it calls the ReadFile method using the parsed value and returns exit code 0 : ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0 ; } If no value was provided for --file , it prints available parse errors and returns exit code 1 : foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1 ; The ReadFile method reads the specified file and displays its contents on the console: static void ReadFile ( FileInfo file ) { foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } 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 build output folder to run the executable: dotnet build cd bin/Debug/net9.0 scl --file scl.runtimeconfig.json Use dotnet run and pass option values to the app instead of to the run command by including them after -- , as in the following example: dotnet run -- --file bin/Debug/net9. 0 /scl.runtimeconfig.json (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": "net9.0", "framework": { "name": "Microsoft.NETCore.App", "version": "9.0.0" } } } But if you ask it to display the help by providing --help , nothing gets printed to the console. That's because the app doesn't yet handle the scenario where --file isn't provided and there are no parse errors. System.CommandLine allows you to specify an action that's invoked when a given symbol (command, directive, or option) is parsed successfully. The action is a delegate that takes a ParseResult parameter and returns an int exit code. ( Async actions are also available). The exit code is returned by the ParseResult.Invoke(InvocationConfiguration) method and can be used to indicate whether the command was executed successfully or not. Replace the contents of Program.cs with the following code: using System.CommandLine; namespace scl ; class Program { static int Main ( string [] args ) { Option<FileInfo> fileOption = new ( "--file" ) { Description = "The file to read and display on the console" }; RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); rootCommand.Options.Add(fileOption); rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0 ; }); ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); } static void ReadFile ( FileInfo file ) { foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } The preceding code: Specifies that ReadFile is the method that will be called when the root command is invoked : rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0 ; }); Parses args and invokes the result: ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); When you run the app, it displays the contents of the file specified by the --file option. If you ask it to display the help by specifying scl --help , the following output gets printed: Description: Sample app for System.CommandLine Usage: scl [options] Options: -?, -h, --help Show help and usage information --version Show version information --file The file to read and display on the console RootCommand by default provides Help option , Version option , and Suggest directive . The ParseResult.Invoke(InvocationConfiguration) method is responsible for invoking the action of the parsed symbol. It could be the action explicitly defined for the command or the help action defined by System.CommandLine for System.CommandLine.Help.HelpOption . Moreover, when the Invoke method detects any parse errors, it prints them to the standard error, prints help to standard output, and returns 1 as the exit code: scl --invalid bla Unrecognized command or argument '--invalid'. Unrecognized command or argument 'bla'. In this section, you: Create more options. Create a subcommand. Assign the new options to the new subcommand. The new options let the end user 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 dotnet samples repository 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 output 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: Option< int > delayOption = new ( "--delay" ) { Description = "Delay between lines, specified as milliseconds per character in a line" , DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new ( "--fgcolor" ) { Description = "Foreground color of text displayed on the console" , DefaultValueFactory = parseResult => ConsoleColor.White }; Option< bool > lightModeOption = new ( "--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 code that adds the --file option to it. You're removing it here because you'll add it to a new subcommand. After the line that creates the root command, create a read subcommand. Add the options to this subcommand (by using collection initializer syntax rather than Options property), and add the subcommand to the root command. Command readCommand = new ( "read" , "Read and display the file." ) { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); Replace the SetAction code with the following SetAction code for the new subcommand: readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); You're no longer calling SetAction on the root command because the root command no longer needs an action. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app. Replace the ReadFile action method with the following code: internal static void ReadFile ( FileInfo file, int delay, ConsoleColor fgColor, bool lightMode ) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } The app now looks like this: using System.CommandLine; namespace scl ; class Program { static int Main ( string [] args ) { Option<FileInfo> fileOption = new ( "--file" ) { Description = "The file to read and display on the console" }; Option< int > delayOption = new ( "--delay" ) { Description = "Delay between lines, specified as milliseconds per character in a line" , DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new ( "--fgcolor" ) { Description = "Foreground color of text displayed on the console" , DefaultValueFactory = parseResult => ConsoleColor.White }; Option< bool > lightModeOption = new ( "--light-mode" ) { Description = "Background color of text displayed on the console: default is black, light mode is white" }; RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); Command readCommand = new ( "read" , "Read and display the file." ) { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile ( FileInfo file, int delay, ConsoleColor fgColor, bool lightMode ) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } } 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: -?, -h, --help Show help and usage information --version Show version 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 If you provide an invalid value for --delay , 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'. If you provide an invalid value for --file , you get an exception: scl read --file nofile Unhandled exception: System.IO.FileNotFoundException: Could not find file 'C:\bin\Debug\net9.0\nofile''. File name: 'C:\bin\Debug\net9.0\nofile'' This section creates the final version of the app. When finished, the app will have the following commands and options: Command Options Arguments Root command --file ( recursive ) quotes read --delay , --fgcolor , --light-mode add quote and byline delete --search-terms (A recursive 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: Option<FileInfo> fileOption = new ( "--file" ) { Description = "An option whose argument is parsed as a FileInfo" , Required = true , DefaultValueFactory = result => { if (result.Tokens.Count == 0 ) { return new FileInfo( "sampleQuotes.txt" ); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError( "File does not exist" ); return null ; } else { return new FileInfo(filePath); } } }; This code uses ArgumentResult 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 Option<T>.DefaultValueFactory to custom parsing method. After the code that creates lightModeOption , add options and arguments for the add and delete commands: Option< string []> searchTermsOption = new ( "--search-terms" ) { Description = "Strings to search for when deleting entries" , Required = true , AllowMultipleArgumentsPerToken = true }; Argument< string > quoteArgument = new ( "quote" ) { Description = "Text of quote." }; Argument< string > bylineArgument = new ( "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: RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); fileOption.Recursive = true ; rootCommand.Options.Add(fileOption); Command quotesCommand = new ( "quotes" , "Work with a file that contains quotes." ); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new ( "read" , "Read and display the file." ) { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new ( "delete" , "Delete lines from the file." ); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new ( "add" , "Add an entry to the file." ); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add( "insert" ); quotesCommand.Subcommands.Add(addCommand); This code makes the following changes: Removes the --file option from the read command. Adds the --file option as a recursive option to the root command. Creates a quotes command and adds it to the root command. Adds the read command to the quotes command instead of to the root command. Creates add and delete commands and adds them to the quotes 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. Recursive 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 SetAction code, add new SetAction code for the new subcommands: deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); Subcommand quotes doesn't have an action because it isn't a leaf command. Subcommands read , add , and delete are leaf commands under quotes , and SetAction is called for each of them. Add the actions for add and delete . internal static void DeleteFromFile ( FileInfo file, string [] searchTerms ) { Console.WriteLine( "Deleting from file" ); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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} " ); } The finished app looks like this: using System.CommandLine; namespace scl ; class Program { static int Main ( string [] args ) { Option<FileInfo> fileOption = new ( "--file" ) { Description = "An option whose argument is parsed as a FileInfo" , Required = true , DefaultValueFactory = result => { if (result.Tokens.Count == 0 ) { return new FileInfo( "sampleQuotes.txt" ); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError( "File does not exist" ); return null ; } else { return new FileInfo(filePath); } } }; Option< int > delayOption = new ( "--delay" ) { Description = "Delay between lines, specified as milliseconds per character in a line" , DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new ( "--fgcolor" ) { Description = "Foreground color of text displayed on the console" , DefaultValueFactory = parseResult => ConsoleColor.White }; Option< bool > lightModeOption = new ( "--light-mode" ) { Description = "Background color of text displayed on the console: default is black, light mode is white" }; Option< string []> searchTermsOption = new ( "--search-terms" ) { Description = "Strings to search for when deleting entries" , Required = true , AllowMultipleArgumentsPerToken = true }; Argument< string > quoteArgument = new ( "quote" ) { Description = "Text of quote." }; Argument< string > bylineArgument = new ( "byline" ) { Description = "Byline of quote." }; RootCommand rootCommand = new ( "Sample app for System.CommandLine" ); fileOption.Recursive = true ; rootCommand.Options.Add(fileOption); Command quotesCommand = new ( "quotes" , "Work with a file that contains quotes." ); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new ( "read" , "Read and display the file." ) { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new ( "delete" , "Delete lines from the file." ); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new ( "add" , "Add an entry to the file." ); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add( "insert" ); quotesCommand.Subcommands.Add(addCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile ( FileInfo file, int delay, ConsoleColor fgColor, bool lightMode ) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach ( string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } internal static void DeleteFromFile ( FileInfo file, string [] searchTerms ) { Console.WriteLine( "Deleting from file" ); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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} " ); } } 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 If you try to run subcommand quotes , 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 The file in the output folder reflects the changes from the add and delete commands. The copy of the file in the project folder remains unchanged. In this tutorial, you created a simple command-line app that uses System.CommandLine . To learn more about the library, see System.CommandLine overview . To enable tab completion capabilities, see Tab completion for System.CommandLine .
Markdown
[Skip to main content](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#main) [Skip to Ask Learn chat experience](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) ## Microsoft Build 2026 June 2-3, 2026 Go deep on real code and real systems in San Francisco and online [Learn more](https://aka.ms/MSBuild_FY26_BN_MSLearn_Hero) Dismiss alert This browser is no longer supported. Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support. [Download Microsoft Edge](https://go.microsoft.com/fwlink/p/?LinkID=2092881%20) [More info about Internet Explorer and Microsoft Edge](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge) [Learn](https://learn.microsoft.com/en-us/) [Sign in](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) ![]() ![]() - [Profile](https://learn.microsoft.com/en-us/users/me/activity/) - [Settings](https://learn.microsoft.com/en-us/users/me/settings/) [Sign out](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) [Learn](https://learn.microsoft.com/en-us/) - Documentation - [All product documentation](https://learn.microsoft.com/en-us/docs/) - [Azure documentation](https://learn.microsoft.com/en-us/azure/?product=popular) - [Dynamics 365 documentation](https://learn.microsoft.com/en-us/dynamics365/) - [Microsoft Copilot documentation](https://learn.microsoft.com/en-us/copilot/) - [Microsoft 365 documentation](https://learn.microsoft.com/en-us/microsoft-365/) - [Power Platform documentation](https://learn.microsoft.com/en-us/power-platform/) - [Code samples](https://learn.microsoft.com/en-us/samples/) - [Troubleshooting documentation](https://learn.microsoft.com/en-us/troubleshoot/) Free to join. Request to attend. [Microsoft AI Tour](https://aitour.microsoft.com/?wt.mc_id=itour26_learnmarketingspot_wwl) Take your business to the AI frontier. - Training & Labs - [All training](https://learn.microsoft.com/en-us/training/) - [Azure training](https://learn.microsoft.com/en-us/training/browse/?products=azure) - [Dynamics 365 training](https://learn.microsoft.com/en-us/training/browse/?products=dynamics-365) - [Microsoft Copilot training](https://learn.microsoft.com/en-us/training/browse/?products=ms-copilot) - [Microsoft 365 training](https://learn.microsoft.com/en-us/training/browse/?products=m365) - [Microsoft Power Platform training](https://learn.microsoft.com/en-us/training/browse/?products=power-platform) - [Labs](https://learn.microsoft.com/en-us/labs/) - [Credentials](https://learn.microsoft.com/en-us/credentials/) - [Career paths](https://learn.microsoft.com/en-us/training/career-paths/) Free to join. Request to attend. [Microsoft AI Tour](https://aitour.microsoft.com/?wt.mc_id=itour26_learnmarketingspot_wwl) Take your business to the AI frontier. - Q\&A - [Ask a question](https://learn.microsoft.com/en-us/answers/questions/ask/) - [Azure questions](https://learn.microsoft.com/en-us/answers/tags/133/azure/) - [Windows questions](https://learn.microsoft.com/en-us/answers/tags/60/windows/) - [Microsoft 365 questions](https://learn.microsoft.com/en-us/answers/tags/9/m365/) - [Microsoft Outlook questions](https://learn.microsoft.com/en-us/answers/tags/131/office-outlook/) - [Microsoft Teams questions](https://learn.microsoft.com/en-us/answers/tags/108/office-teams/) - [Popular tags](https://learn.microsoft.com/en-us/answers/tags/) - [All questions](https://learn.microsoft.com/en-us/answers/questions/) Free to join. Request to attend. [Microsoft AI Tour](https://aitour.microsoft.com/?wt.mc_id=itour26_learnmarketingspot_wwl) Take your business to the AI frontier. - Topics - [Artificial intelligence](https://learn.microsoft.com/en-us/ai/) Learning hub to build AI skills - [Compliance](https://learn.microsoft.com/en-us/compliance/) Compliance resources you need to get started with your business - [DevOps](https://learn.microsoft.com/en-us/devops/) DevOps practices, Git version control and Agile methods - [Learn for Organizations](https://learn.microsoft.com/en-us/training/organizations/) Curated offerings from Microsoft to boost your team’s technical skills - [Platform engineering](https://learn.microsoft.com/en-us/platform-engineering/) Tools from Microsoft and others to build personalized developer experiences - [Security](https://learn.microsoft.com/en-us/security/) Guidance to help you tackle security challenges - [Assessments](https://learn.microsoft.com/en-us/assessments/) Interactive guidance with custom recommendations - [Student hub](https://learn.microsoft.com/en-us/training/student-hub/) Self-paced and interactive training for students - [Educator center](https://learn.microsoft.com/en-us/training/educator-center/) Resources for educators to bring technical innovation in their classroom Free to join. Request to attend. [Microsoft AI Tour](https://aitour.microsoft.com/?wt.mc_id=itour26_learnmarketingspot_wwl) Take your business to the AI frontier. [Sign in](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) ![]() ![]() - [Profile](https://learn.microsoft.com/en-us/users/me/activity/) - [Settings](https://learn.microsoft.com/en-us/users/me/settings/) [Sign out](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) [.NET](https://learn.microsoft.com/en-us/dotnet/) - Languages - [C\#](https://learn.microsoft.com/en-us/dotnet/csharp/) - [F\#](https://learn.microsoft.com/en-us/dotnet/fsharp/) - [Visual Basic](https://learn.microsoft.com/en-us/dotnet/visual-basic/) - Features - [Fundamentals](https://learn.microsoft.com/en-us/dotnet/fundamentals/) - [Tools and diagnostics](https://learn.microsoft.com/en-us/dotnet/navigate/tools-diagnostics/) - AI - [Generative AI](https://learn.microsoft.com/en-us/dotnet/ai/) - [ML.NET](https://learn.microsoft.com/en-us/dotnet/machine-learning/) - [Migrate from .NET Framework](https://learn.microsoft.com/en-us/dotnet/navigate/migration-guide/) - [Compatibility](https://learn.microsoft.com/en-us/dotnet/core/compatibility/breaking-changes/) - [Advanced programming](https://learn.microsoft.com/en-us/dotnet/navigate/advanced-programming/) - [DevOps and testing](https://learn.microsoft.com/en-us/dotnet/navigate/devops-testing/) - [Security](https://learn.microsoft.com/en-us/dotnet/navigate/security/) - [Data access](https://learn.microsoft.com/en-us/dotnet/navigate/data-access/) - Workloads - [Web](https://learn.microsoft.com/en-us/aspnet/core/) - [Cloud](https://learn.microsoft.com/en-us/dotnet/azure/) - Cloud-native - [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) - [Microsoft Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/) - Desktop and mobile - [.NET Multi-platform App UI (.NET MAUI)](https://learn.microsoft.com/en-us/dotnet/maui/) - [Windows Presentation Foundation](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/) - [Windows Forms](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/) - [WinUI apps](https://learn.microsoft.com/en-us/windows/apps/winui/) - APIs - [.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=net-10.0) - [Aspire](https://learn.microsoft.com/en-us/dotnet/api/?view=dotnet-aspire-9.0) - [.NET Framework](https://learn.microsoft.com/en-us/dotnet/api/?view=netframework-4.8&preserve-view=true) - [.NET Multi-platform App UI (.NET MAUI)](https://learn.microsoft.com/en-us/dotnet/api/?view=net-maui-9.0) - [ASP.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=aspnetcore-10.0) - [ML.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=ml-dotnet&preserve-view=true) - [Troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/developer/dotnet/welcome-dotnet-landing) - Resources - [Library guidance](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/) - [What is .NET?](https://dotnet.microsoft.com/learn/dotnet/what-is-dotnet) - [.NET architecture guides](https://dotnet.microsoft.com/learn/dotnet/architecture-guides) - [Learning materials](https://dotnet.microsoft.com/learn) - [Downloads](https://dotnet.microsoft.com/download) - [Community](https://dotnet.microsoft.com/platform/community) - [Support](https://dotnet.microsoft.com/platform/support) - [Blog](https://devblogs.microsoft.com/dotnet/) - More - Languages - [C\#](https://learn.microsoft.com/en-us/dotnet/csharp/) - [F\#](https://learn.microsoft.com/en-us/dotnet/fsharp/) - [Visual Basic](https://learn.microsoft.com/en-us/dotnet/visual-basic/) - Features - [Fundamentals](https://learn.microsoft.com/en-us/dotnet/fundamentals/) - [Tools and diagnostics](https://learn.microsoft.com/en-us/dotnet/navigate/tools-diagnostics/) - AI - [Generative AI](https://learn.microsoft.com/en-us/dotnet/ai/) - [ML.NET](https://learn.microsoft.com/en-us/dotnet/machine-learning/) - [Migrate from .NET Framework](https://learn.microsoft.com/en-us/dotnet/navigate/migration-guide/) - [Compatibility](https://learn.microsoft.com/en-us/dotnet/core/compatibility/breaking-changes/) - [Advanced programming](https://learn.microsoft.com/en-us/dotnet/navigate/advanced-programming/) - [DevOps and testing](https://learn.microsoft.com/en-us/dotnet/navigate/devops-testing/) - [Security](https://learn.microsoft.com/en-us/dotnet/navigate/security/) - [Data access](https://learn.microsoft.com/en-us/dotnet/navigate/data-access/) - Workloads - [Web](https://learn.microsoft.com/en-us/aspnet/core/) - [Cloud](https://learn.microsoft.com/en-us/dotnet/azure/) - Cloud-native - [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) - [Microsoft Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/) - Desktop and mobile - [.NET Multi-platform App UI (.NET MAUI)](https://learn.microsoft.com/en-us/dotnet/maui/) - [Windows Presentation Foundation](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/) - [Windows Forms](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/) - [WinUI apps](https://learn.microsoft.com/en-us/windows/apps/winui/) - APIs - [.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=net-10.0) - [Aspire](https://learn.microsoft.com/en-us/dotnet/api/?view=dotnet-aspire-9.0) - [.NET Framework](https://learn.microsoft.com/en-us/dotnet/api/?view=netframework-4.8&preserve-view=true) - [.NET Multi-platform App UI (.NET MAUI)](https://learn.microsoft.com/en-us/dotnet/api/?view=net-maui-9.0) - [ASP.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=aspnetcore-10.0) - [ML.NET](https://learn.microsoft.com/en-us/dotnet/api/?view=ml-dotnet&preserve-view=true) - [Troubleshooting](https://learn.microsoft.com/en-us/troubleshoot/developer/dotnet/welcome-dotnet-landing) - Resources - [Library guidance](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/) - [What is .NET?](https://dotnet.microsoft.com/learn/dotnet/what-is-dotnet) - [.NET architecture guides](https://dotnet.microsoft.com/learn/dotnet/architecture-guides) - [Learning materials](https://dotnet.microsoft.com/learn) - [Downloads](https://dotnet.microsoft.com/download) - [Community](https://dotnet.microsoft.com/platform/community) - [Support](https://dotnet.microsoft.com/platform/support) - [Blog](https://devblogs.microsoft.com/dotnet/) [Download .NET](https://dotnet.microsoft.com/download) - [.NET fundamentals documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/) - Get started - Overview - What's new in .NET - Fundamental coding components - Runtime libraries - [Overview](https://learn.microsoft.com/en-us/dotnet/standard/runtime-libraries-overview) - [Preview APIs](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/preview-apis) - Format numbers, dates, other types - Work with strings - Compliance - Regular expressions - Serialization - System. CommandLine - [Overview](https://learn.microsoft.com/en-us/dotnet/standard/commandline/) - [Get started tutorial](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) - [Command-line syntax overview](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax) - How to - [Parse and invoke the result](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-parse-and-invoke) - [Customize parsing and validation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-parsing-and-validation) - [Configure the parser](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-configure-the-parser) - [Enable and customize tab completion](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-enable-tab-completion) - [Customize help](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-help) - [Design guidance](https://learn.microsoft.com/en-us/dotnet/standard/commandline/design-guidance) - [Migration guide to 2.0.0-beta5](https://learn.microsoft.com/en-us/dotnet/standard/commandline/migration-guide-2.0.0-beta5) - File and stream I/O - [The System.AppContext class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appcontext) - The System. Console class - [The System.Random class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-random) - [Artificial intelligence (AI)](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai?toc=/dotnet/fundamentals/toc.json&bc=/dotnet/breadcrumb/toc.json) - Dependency injection - Configuration - Logging - [HostBuilder (generic host)](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host) - [Asynchronous state management](https://learn.microsoft.com/en-us/dotnet/core/extensions/async-state) - [Testing with FakeTimeProvider](https://learn.microsoft.com/en-us/dotnet/core/extensions/timeprovider-testing) - [Audit reports for privacy and compliance](https://learn.microsoft.com/en-us/dotnet/core/extensions/audit-reports) - [Resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience/) - Networking - [File globbing](https://learn.microsoft.com/en-us/dotnet/core/extensions/file-globbing) - [Primitives library](https://learn.microsoft.com/en-us/dotnet/core/extensions/primitives) - Globalization and localization - Resources in .NET apps - Worker services - [Caching](https://learn.microsoft.com/en-us/dotnet/core/extensions/caching) - [Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) - Math operations - [The Win32.Registry class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/microsoft-win32-registry) - [The Uri class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-uri) - Reflection - Graphics - [The InternalsVisibleToAttribute class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-runtime-compilerservices-internalsvisibletoattribute) - The RuntimeHelpers class - [The ComponentGuaranteesAttribute class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-runtime-versioning-componentguaranteesattribute) - [The AssemblyLoadContext class](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-runtime-loader-assemblyloadcontext) - The ProcessStartInfo class - The Environment class - Execution model Download PDF Table of contents Exit editor mode 1. [Learn](https://learn.microsoft.com/en-us/) 2. [.NET](https://learn.microsoft.com/en-us/dotnet/) 1. [Learn](https://learn.microsoft.com/en-us/) 2. [.NET](https://learn.microsoft.com/en-us/dotnet/) Ask Learn Ask Learn Focus mode Table of contents [Read in English](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) Add to Collections Add to plan [Edit](https://github.com/dotnet/docs/blob/main/docs/standard/commandline/get-started-tutorial.md) *** #### Share via [Facebook](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial%3FWT.mc_id%3Dfacebook) [x.com](https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial%3FWT.mc_id%3Dtwitter&tw_p=tweetbutton&url=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial%3FWT.mc_id%3Dtwitter) [LinkedIn](https://www.linkedin.com/feed/?shareActive=true&text=%0A%0D%0Ahttps%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial%3FWT.mc_id%3Dlinkedin) [Email](mailto:?subject=%5BShared%20Article%5D%20Tutorial%3A%20Get%20started%20with%20System.CommandLine%20-%20.NET%20%7C%20Microsoft%20Learn&body=%0A%0D%0Ahttps%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial%3FWT.mc_id%3Demail) *** Copy Markdown Print *** Note Access to this page requires authorization. You can try [signing in](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) or [changing directories](). Access to this page requires authorization. You can try [changing directories](). # Tutorial: Get started with System.CommandLine Feedback Summarize this article for me ## In this article 1. [Prerequisites](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#prerequisites) 2. [Create the app](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#create-the-app) 3. [Test the app](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#test-the-app) 4. [Parse the arguments and invoke the ParseResult](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#parse-the-arguments-and-invoke-the-parseresult) 5. [Add a subcommand and options](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#add-a-subcommand-and-options) 6. [Add subcommands and custom validation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#add-subcommands-and-custom-validation) 7. [Next steps](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#next-steps) Show 3 more This tutorial shows how to create a .NET command-line app that uses the [`System.CommandLine` library](https://learn.microsoft.com/en-us/dotnet/standard/commandline/). You'll begin by creating a simple root command that has one option. Then you'll build on 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. - Read option values in command action code. - Use custom code for parsing and validating options. ## Prerequisites - The latest [.NET SDK](https://dotnet.microsoft.com/download) - [Visual Studio Code](https://code.visualstudio.com/) editor - The [C\# DevKit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) Or - [Visual Studio](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft&utm_source=learn.microsoft.com&utm_campaign=inline+link) with the **.NET desktop development** workload installed. ## Create the app Create a .NET 9 console app project named "scl". 1. Create a folder named *scl* for the project, and then open a command prompt in the new folder. 2. Run the following command: .NET CLI Copy ``` dotnet new console --framework net9.0 ``` ### Install the System.CommandLine package - Run the following command: .NET CLI Copy ``` dotnet add package System.CommandLine ``` Or, in .NET 10+: .NET CLI Copy ``` dotnet package add System.CommandLine ``` ### Parse the arguments Replace the contents of *Program.cs* with the following code: C\# Copy ``` using System.CommandLine; using System.CommandLine.Parsing; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0; } foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1; } static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } ``` The preceding code: - Creates an [option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#options) named `--file` of type [FileInfo](https://learn.microsoft.com/en-us/dotnet/api/system.io.fileinfo) and adds it to the [root command](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#root-command): C\# Copy ``` Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); ``` - Parses `args` and checks whether any value was provided for the `--file` option. If so, it calls the `ReadFile` method using the parsed value and returns exit code `0`: C\# Copy ``` ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0; } ``` - If no value was provided for `--file`, it prints available parse errors and returns exit code `1`: C\# Copy ``` foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1; ``` - The `ReadFile` method reads the specified file and displays its contents on the console: C\# Copy ``` static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { 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 build output folder to run the executable: Console Copy ``` dotnet build cd bin/Debug/net9.0 scl --file scl.runtimeconfig.json ``` - Use `dotnet run` and pass option values to the app instead of to the `run` command by including them after `--`, as in the following example: .NET CLI Copy ``` dotnet run -- --file bin/Debug/net9.0/scl.runtimeconfig.json ``` (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. Output Copy ``` { "runtimeOptions": { "tfm": "net9.0", "framework": { "name": "Microsoft.NETCore.App", "version": "9.0.0" } } } ``` But if you ask it to display the help by providing `--help`, nothing gets printed to the console. That's because the app doesn't yet handle the scenario where `--file` isn't provided and there are no parse errors. ## Parse the arguments and invoke the ParseResult System.CommandLine allows you to specify an action that's invoked when a given symbol (command, directive, or option) is parsed successfully. The action is a delegate that takes a [ParseResult](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult) parameter and returns an `int` exit code. ([Async actions](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-parse-and-invoke#asynchronous-actions) are also available). The exit code is returned by the [ParseResult.Invoke(InvocationConfiguration)](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult.invoke#system-commandline-parseresult-invoke\(system-commandline-invocationconfiguration\)) method and can be used to indicate whether the command was executed successfully or not. Replace the contents of *Program.cs* with the following code: C\# Copy ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0; }); ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); } static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } ``` The preceding code: - Specifies that `ReadFile` is the method that will be called when the root command is **invoked**: C\# Copy ``` rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0; }); ``` - Parses `args` and **invokes** the result: C\# Copy ``` ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); ``` When you run the app, it displays the contents of the file specified by the `--file` option. If you ask it to display the help by specifying `scl --help`, the following output gets printed: Output Copy ``` Description: Sample app for System.CommandLine Usage: scl [options] Options: -?, -h, --help Show help and usage information --version Show version information --file The file to read and display on the console ``` [RootCommand](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.rootcommand) by default provides [Help option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-help#customize-help-output), [Version option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#version-option), and [Suggest directive](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#suggest-directive). The [ParseResult.Invoke(InvocationConfiguration)](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult.invoke#system-commandline-parseresult-invoke\(system-commandline-invocationconfiguration\)) method is responsible for invoking the action of the parsed symbol. It could be the action explicitly defined for the command or the help action defined by `System.CommandLine` for `System.CommandLine.Help.HelpOption`. Moreover, when the `Invoke` method detects any parse errors, it prints them to the standard error, prints help to standard output, and returns `1` as the exit code: Console Copy ``` scl --invalid bla ``` Output Copy ``` Unrecognized command or argument '--invalid'. Unrecognized command or argument 'bla'. ``` ## 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 let the end user 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](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/console-teleprompter). 1. Copy the [sampleQuotes.txt](https://github.com/dotnet/samples/blob/main/csharp/getting-started/console-teleprompter/sampleQuotes.txt) file from the dotnet samples repository into your project directory. (For information on how to download files, see the instructions in [Samples and tutorials](https://learn.microsoft.com/en-us/dotnet/samples-and-tutorials/#view-and-download-samples).) 2. Open the project file and add an `<ItemGroup>` element just before the closing `</Project>` tag: XML Copy ``` <ItemGroup> <Content Include="sampleQuotes.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> ``` Adding this markup causes the text file to be copied to the output 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. 3. In *Program.cs*, after the code that creates the `--file` option, create options to control the readout speed and text colors: C\# Copy ``` Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; ``` 4. After the line that creates the root command, delete the code that adds the `--file` option to it. You're removing it here because you'll add it to a new subcommand. 5. After the line that creates the root command, create a `read` subcommand. Add the options to this subcommand (by using collection initializer syntax rather than `Options` property), and add the subcommand to the root command. C\# Copy ``` Command readCommand = new("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); ``` 6. Replace the `SetAction` code with the following `SetAction` code for the new subcommand: C\# Copy ``` readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); ``` You're no longer calling `SetAction` on the root command because the root command no longer needs an action. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app. 7. Replace the `ReadFile` action method with the following code: C\# Copy ``` internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } ``` The app now looks like this: C\# Copy ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); Command readCommand = new("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(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. Console Copy ``` scl --file sampleQuotes.txt ``` Output Copy ``` '--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: -?, -h, --help Show help and usage information --version Show version 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. Console Copy ``` scl read -h ``` Output Copy ``` 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. Console Copy ``` 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. Console Copy ``` scl read --file sampleQuotes.txt --delay 0 ``` You can use `--fgcolor` and `--light-mode` to set text colors: Console Copy ``` scl read --file sampleQuotes.txt --fgcolor red --light-mode ``` If you provide an invalid value for `--delay`, you get an error message: Console Copy ``` scl read --file sampleQuotes.txt --delay forty-two ``` Output Copy ``` Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'. ``` If you provide an invalid value for `--file`, you get an exception: Console Copy ``` scl read --file nofile ``` Output Copy ``` Unhandled exception: System.IO.FileNotFoundException: Could not find file 'C:\bin\Debug\net9.0\nofile''. File name: 'C:\bin\Debug\net9.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: Expand table | Command | Options | Arguments | |---|---|---| | Root command | `--file` (*recursive*) | | | `quotes` | | | | `read` | `--delay`, `--fgcolor`, `--light-mode` | | | `add` | | `quote` and `byline` | | `delete` | `--search-terms` | | (A *recursive* 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: Console Copy ``` 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*, replace the code that creates the `--file` option with the following code: C\# Copy ``` Option<FileInfo> fileOption = new("--file") { Description = "An option whose argument is parsed as a FileInfo", Required = true, DefaultValueFactory = result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError("File does not exist"); return null; } else { return new FileInfo(filePath); } } }; ``` This code uses [ArgumentResult](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parsing.argumentresult) 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 [Option\<T\>.DefaultValueFactory](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.option-1.defaultvaluefactory#system-commandline-option-1-defaultvaluefactory) to custom parsing method. 2. After the code that creates `lightModeOption`, add options and arguments for the `add` and `delete` commands: C\# Copy ``` Option<string[]> searchTermsOption = new("--search-terms") { Description = "Strings to search for when deleting entries", Required = true, AllowMultipleArgumentsPerToken = true }; Argument<string> quoteArgument = new("quote") { Description = "Text of quote." }; Argument<string> bylineArgument = new("byline") { Description = "Byline of quote." }; ``` The [AllowMultipleArgumentsPerToken](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.option.allowmultipleargumentspertoken#system-commandline-option-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: Console Copy ``` scl quotes delete --search-terms David "You can do" scl quotes delete --search-terms David --search-terms "You can do" ``` 3. Replace the code that creates the root command and the `read` command with the following code: C\# Copy ``` RootCommand rootCommand = new("Sample app for System.CommandLine"); fileOption.Recursive = true; rootCommand.Options.Add(fileOption); Command quotesCommand = new("quotes", "Work with a file that contains quotes."); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new("read", "Read and display the file.") { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new("delete", "Delete lines from the file."); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new("add", "Add an entry to the file."); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add("insert"); quotesCommand.Subcommands.Add(addCommand); ``` This code makes the following changes: - Removes the `--file` option from the `read` command. - Adds the `--file` option as a recursive option to the root command. - Creates a `quotes` command and adds it to the root command. - Adds the `read` command to the `quotes` command instead of to the root command. - Creates `add` and `delete` commands and adds them to the `quotes` command. The result is the following command hierarchy: Root command └──`quotes` └──`read` └──`add` └──`delete` The app now implements the [recommended](https://learn.microsoft.com/en-us/dotnet/standard/commandline/design-guidance#symbols) pattern where the parent command (`quotes`) specifies an area or group, and its children commands (`read`, `add`, `delete`) are actions. Recursive 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. 4. After the `SetAction` code, add new `SetAction` code for the new subcommands: C\# Copy ``` deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); ``` Subcommand `quotes` doesn't have an action because it isn't a leaf command. Subcommands `read`, `add`, and `delete` are leaf commands under `quotes`, and `SetAction` is called for each of them. 5. Add the actions for `add` and `delete`. C\# Copy ``` internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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}"); } ``` The finished app looks like this: C\# Copy ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "An option whose argument is parsed as a FileInfo", Required = true, DefaultValueFactory = result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError("File does not exist"); return null; } else { return new FileInfo(filePath); } } }; Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; Option<string[]> searchTermsOption = new("--search-terms") { Description = "Strings to search for when deleting entries", Required = true, AllowMultipleArgumentsPerToken = true }; Argument<string> quoteArgument = new("quote") { Description = "Text of quote." }; Argument<string> bylineArgument = new("byline") { Description = "Byline of quote." }; RootCommand rootCommand = new("Sample app for System.CommandLine"); fileOption.Recursive = true; rootCommand.Options.Add(fileOption); Command quotesCommand = new("quotes", "Work with a file that contains quotes."); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new("read", "Read and display the file.") { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new("delete", "Delete lines from the file."); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new("add", "Add an entry to the file."); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add("insert"); quotesCommand.Subcommands.Add(addCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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}"); } } ``` 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: Console Copy ``` scl quotes read --file nofile ``` Output Copy ``` File does not exist ``` If you try to run subcommand `quotes`, you get a message directing you to use `read`, `add`, or `delete`: Console Copy ``` scl quotes ``` Output Copy ``` 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: Console Copy ``` 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: Console Copy ``` scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved" ``` Note The file in the output folder reflects the 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](https://learn.microsoft.com/en-us/dotnet/standard/commandline/). To enable tab completion capabilities, see [Tab completion for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-enable-tab-completion). Collaborate with us on GitHub The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute). ![](https://learn.microsoft.com/media/logos/logo_net.svg) ![](https://learn.microsoft.com/media/logos/logo_net.svg) .NET feedback .NET is an open source project. Select a link to provide feedback: [Open a documentation issue](https://github.com/dotnet/docs/issues/new?template=z-customer-feedback.yml&pageUrl=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial&pageQueryParams=&contentSourceUrl=https%3A%2F%2Fgithub.com%2Fdotnet%2Fdocs%2Fblob%2Fmain%2Fdocs%2Fstandard%2Fcommandline%2Fget-started-tutorial.md&documentVersionIndependentId=fa160b1d-66ed-8c5d-37cd-b7e02443e9f1&platformId=0f144444-1ce3-cdf7-87a6-c659d44e08d3&feedback=%0A%0A%5BEnter+feedback+here%5D%0A&author=%40gewarren&metadata=*+ID%3A+7705dc41-7297-0807-7e9c-2e6548c26b84%0A*+PlatformId%3A+0f144444-1ce3-cdf7-87a6-c659d44e08d3+%0A*+Service%3A+**dotnet-fundamentals**) [Provide product feedback](https://aka.ms/feedback/report?space=61) *** ## Feedback Was this page helpful? Yes No No Need help with this topic? Want to try using Ask Learn to clarify or guide you through this topic? Ask Learn Ask Learn Suggest a fix? *** ## Additional resources Training Module [Work with files and directories in a .NET app - Training](https://learn.microsoft.com/en-us/training/modules/dotnet-files/?source=recommendations) Learn how to use .NET, C\#, and System.IO to work with directories, paths, files, and the file system. *** - Last updated on 12/18/2025 ## In this article 1. [Prerequisites](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#prerequisites) 2. [Create the app](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#create-the-app) 3. [Test the app](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#test-the-app) 4. [Parse the arguments and invoke the ParseResult](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#parse-the-arguments-and-invoke-the-parseresult) 5. [Add a subcommand and options](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#add-a-subcommand-and-options) 6. [Add subcommands and custom validation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#add-subcommands-and-custom-validation) 7. [Next steps](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial#next-steps) Was this page helpful? Yes No No Need help with this topic? Want to try using Ask Learn to clarify or guide you through this topic? Ask Learn Ask Learn Suggest a fix? ## Ask Learn Preview Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation. Please sign in to use Ask Learn. [Sign in](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) [English (United States)](https://learn.microsoft.com/en-us/locale?target=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fdotnet%2Fstandard%2Fcommandline%2Fget-started-tutorial) [Your Privacy Choices](https://aka.ms/yourcaliforniaprivacychoices) Theme - Light - Dark - High contrast - [AI Disclaimer](https://learn.microsoft.com/en-us/principles-for-ai-generated-content) - [Previous Versions](https://learn.microsoft.com/en-us/previous-versions/) - [Blog](https://techcommunity.microsoft.com/t5/microsoft-learn-blog/bg-p/MicrosoftLearnBlog) - [Contribute](https://learn.microsoft.com/en-us/contribute) - [Privacy](https://go.microsoft.com/fwlink/?LinkId=521839) - [Consumer Health Privacy](https://go.microsoft.com/fwlink/?linkid=2259814) - [Terms of Use](https://learn.microsoft.com/en-us/legal/termsofuse) - [Trademarks](https://www.microsoft.com/legal/intellectualproperty/Trademarks/) - Β© Microsoft 2026
Readable Markdown
This tutorial shows how to create a .NET command-line app that uses the [`System.CommandLine` library](https://learn.microsoft.com/en-us/dotnet/standard/commandline/). You'll begin by creating a simple root command that has one option. Then you'll build on 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. - Read option values in command action code. - Use custom code for parsing and validating options. - The latest [.NET SDK](https://dotnet.microsoft.com/download) - [Visual Studio Code](https://code.visualstudio.com/) editor - The [C\# DevKit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) Or - [Visual Studio](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft&utm_source=learn.microsoft.com&utm_campaign=inline+link) with the **.NET desktop development** workload installed. Create a .NET 9 console app project named "scl". 1. Create a folder named *scl* for the project, and then open a command prompt in the new folder. 2. Run the following command: ``` dotnet new console --framework net9.0 ``` - Run the following command: ``` dotnet add package System.CommandLine ``` Or, in .NET 10+: ``` dotnet package add System.CommandLine ``` Replace the contents of *Program.cs* with the following code: ``` using System.CommandLine; using System.CommandLine.Parsing; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0; } foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1; } static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } ``` The preceding code: - Creates an [option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#options) named `--file` of type [FileInfo](https://learn.microsoft.com/en-us/dotnet/api/system.io.fileinfo) and adds it to the [root command](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#root-command): ``` Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); ``` - Parses `args` and checks whether any value was provided for the `--file` option. If so, it calls the `ReadFile` method using the parsed value and returns exit code `0`: ``` ParseResult parseResult = rootCommand.Parse(args); if (parseResult.Errors.Count == 0 && parseResult.GetValue(fileOption) is FileInfo parsedFile) { ReadFile(parsedFile); return 0; } ``` - If no value was provided for `--file`, it prints available parse errors and returns exit code `1`: ``` foreach (ParseError parseError in parseResult.Errors) { Console.Error.WriteLine(parseError.Message); } return 1; ``` - The `ReadFile` method reads the specified file and displays its contents on the console: ``` static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } ``` 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 build output folder to run the executable: ``` dotnet build cd bin/Debug/net9.0 scl --file scl.runtimeconfig.json ``` - Use `dotnet run` and pass option values to the app instead of to the `run` command by including them after `--`, as in the following example: ``` dotnet run -- --file bin/Debug/net9.0/scl.runtimeconfig.json ``` (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": "net9.0", "framework": { "name": "Microsoft.NETCore.App", "version": "9.0.0" } } } ``` But if you ask it to display the help by providing `--help`, nothing gets printed to the console. That's because the app doesn't yet handle the scenario where `--file` isn't provided and there are no parse errors. System.CommandLine allows you to specify an action that's invoked when a given symbol (command, directive, or option) is parsed successfully. The action is a delegate that takes a [ParseResult](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult) parameter and returns an `int` exit code. ([Async actions](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-parse-and-invoke#asynchronous-actions) are also available). The exit code is returned by the [ParseResult.Invoke(InvocationConfiguration)](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult.invoke#system-commandline-parseresult-invoke\(system-commandline-invocationconfiguration\)) method and can be used to indicate whether the command was executed successfully or not. Replace the contents of *Program.cs* with the following code: ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); rootCommand.Options.Add(fileOption); rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0; }); ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); } static void ReadFile(FileInfo file) { foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); } } } ``` The preceding code: - Specifies that `ReadFile` is the method that will be called when the root command is **invoked**: ``` rootCommand.SetAction(parseResult => { FileInfo parsedFile = parseResult.GetValue(fileOption); ReadFile(parsedFile); return 0; }); ``` - Parses `args` and **invokes** the result: ``` ParseResult parseResult = rootCommand.Parse(args); return parseResult.Invoke(); ``` When you run the app, it displays the contents of the file specified by the `--file` option. If you ask it to display the help by specifying `scl --help`, the following output gets printed: ``` Description: Sample app for System.CommandLine Usage: scl [options] Options: -?, -h, --help Show help and usage information --version Show version information --file The file to read and display on the console ``` [RootCommand](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.rootcommand) by default provides [Help option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-help#customize-help-output), [Version option](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#version-option), and [Suggest directive](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#suggest-directive). The [ParseResult.Invoke(InvocationConfiguration)](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parseresult.invoke#system-commandline-parseresult-invoke\(system-commandline-invocationconfiguration\)) method is responsible for invoking the action of the parsed symbol. It could be the action explicitly defined for the command or the help action defined by `System.CommandLine` for `System.CommandLine.Help.HelpOption`. Moreover, when the `Invoke` method detects any parse errors, it prints them to the standard error, prints help to standard output, and returns `1` as the exit code: ``` scl --invalid bla ``` ``` Unrecognized command or argument '--invalid'. Unrecognized command or argument 'bla'. ``` In this section, you: - Create more options. - Create a subcommand. - Assign the new options to the new subcommand. The new options let the end user 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](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/console-teleprompter). 1. Copy the [sampleQuotes.txt](https://github.com/dotnet/samples/blob/main/csharp/getting-started/console-teleprompter/sampleQuotes.txt) file from the dotnet samples repository into your project directory. (For information on how to download files, see the instructions in [Samples and tutorials](https://learn.microsoft.com/en-us/dotnet/samples-and-tutorials/#view-and-download-samples).) 2. 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 output 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. 3. In *Program.cs*, after the code that creates the `--file` option, create options to control the readout speed and text colors: ``` Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; ``` 4. After the line that creates the root command, delete the code that adds the `--file` option to it. You're removing it here because you'll add it to a new subcommand. 5. After the line that creates the root command, create a `read` subcommand. Add the options to this subcommand (by using collection initializer syntax rather than `Options` property), and add the subcommand to the root command. ``` Command readCommand = new("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); ``` 6. Replace the `SetAction` code with the following `SetAction` code for the new subcommand: ``` readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); ``` You're no longer calling `SetAction` on the root command because the root command no longer needs an action. When a command has subcommands, you typically have to specify one of the subcommands when invoking a command-line app. 7. Replace the `ReadFile` action method with the following code: ``` internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } ``` The app now looks like this: ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "The file to read and display on the console" }; Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; RootCommand rootCommand = new("Sample app for System.CommandLine"); Command readCommand = new("read", "Read and display the file.") { fileOption, delayOption, fgcolorOption, lightModeOption }; rootCommand.Subcommands.Add(readCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } } ``` 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: -?, -h, --help Show help and usage information --version Show version 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 ``` If you provide an invalid value for `--delay`, 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'. ``` If you provide an invalid value for `--file`, you get an exception: ``` scl read --file nofile ``` ``` Unhandled exception: System.IO.FileNotFoundException: Could not find file 'C:\bin\Debug\net9.0\nofile''. File name: 'C:\bin\Debug\net9.0\nofile'' ``` This section creates the final version of the app. When finished, the app will have the following commands and options: | Command | Options | Arguments | |---|---|---| | Root command | `--file` (*recursive*) | | | `quotes` | | | | `read` | `--delay`, `--fgcolor`, `--light-mode` | | | `add` | | `quote` and `byline` | | `delete` | `--search-terms` | | (A *recursive* 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" ``` 1. In *Program.cs*, replace the code that creates the `--file` option with the following code: ``` Option<FileInfo> fileOption = new("--file") { Description = "An option whose argument is parsed as a FileInfo", Required = true, DefaultValueFactory = result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError("File does not exist"); return null; } else { return new FileInfo(filePath); } } }; ``` This code uses [ArgumentResult](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.parsing.argumentresult) 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 [Option\<T\>.DefaultValueFactory](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.option-1.defaultvaluefactory#system-commandline-option-1-defaultvaluefactory) to custom parsing method. 2. After the code that creates `lightModeOption`, add options and arguments for the `add` and `delete` commands: ``` Option<string[]> searchTermsOption = new("--search-terms") { Description = "Strings to search for when deleting entries", Required = true, AllowMultipleArgumentsPerToken = true }; Argument<string> quoteArgument = new("quote") { Description = "Text of quote." }; Argument<string> bylineArgument = new("byline") { Description = "Byline of quote." }; ``` The [AllowMultipleArgumentsPerToken](https://learn.microsoft.com/en-us/dotnet/api/system.commandline.option.allowmultipleargumentspertoken#system-commandline-option-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" ``` 3. Replace the code that creates the root command and the `read` command with the following code: ``` RootCommand rootCommand = new("Sample app for System.CommandLine"); fileOption.Recursive = true; rootCommand.Options.Add(fileOption); Command quotesCommand = new("quotes", "Work with a file that contains quotes."); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new("read", "Read and display the file.") { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new("delete", "Delete lines from the file."); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new("add", "Add an entry to the file."); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add("insert"); quotesCommand.Subcommands.Add(addCommand); ``` This code makes the following changes: - Removes the `--file` option from the `read` command. - Adds the `--file` option as a recursive option to the root command. - Creates a `quotes` command and adds it to the root command. - Adds the `read` command to the `quotes` command instead of to the root command. - Creates `add` and `delete` commands and adds them to the `quotes` command. The result is the following command hierarchy: Root command └──`quotes` └──`read` └──`add` └──`delete` The app now implements the [recommended](https://learn.microsoft.com/en-us/dotnet/standard/commandline/design-guidance#symbols) pattern where the parent command (`quotes`) specifies an area or group, and its children commands (`read`, `add`, `delete`) are actions. Recursive 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. 4. After the `SetAction` code, add new `SetAction` code for the new subcommands: ``` deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); ``` Subcommand `quotes` doesn't have an action because it isn't a leaf command. Subcommands `read`, `add`, and `delete` are leaf commands under `quotes`, and `SetAction` is called for each of them. 5. Add the actions for `add` and `delete`. ``` internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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}"); } ``` The finished app looks like this: ``` using System.CommandLine; namespace scl; class Program { static int Main(string[] args) { Option<FileInfo> fileOption = new("--file") { Description = "An option whose argument is parsed as a FileInfo", Required = true, DefaultValueFactory = result => { if (result.Tokens.Count == 0) { return new FileInfo("sampleQuotes.txt"); } string filePath = result.Tokens.Single().Value; if (!File.Exists(filePath)) { result.AddError("File does not exist"); return null; } else { return new FileInfo(filePath); } } }; Option<int> delayOption = new("--delay") { Description = "Delay between lines, specified as milliseconds per character in a line", DefaultValueFactory = parseResult => 42 }; Option<ConsoleColor> fgcolorOption = new("--fgcolor") { Description = "Foreground color of text displayed on the console", DefaultValueFactory = parseResult => ConsoleColor.White }; Option<bool> lightModeOption = new("--light-mode") { Description = "Background color of text displayed on the console: default is black, light mode is white" }; Option<string[]> searchTermsOption = new("--search-terms") { Description = "Strings to search for when deleting entries", Required = true, AllowMultipleArgumentsPerToken = true }; Argument<string> quoteArgument = new("quote") { Description = "Text of quote." }; Argument<string> bylineArgument = new("byline") { Description = "Byline of quote." }; RootCommand rootCommand = new("Sample app for System.CommandLine"); fileOption.Recursive = true; rootCommand.Options.Add(fileOption); Command quotesCommand = new("quotes", "Work with a file that contains quotes."); rootCommand.Subcommands.Add(quotesCommand); Command readCommand = new("read", "Read and display the file.") { delayOption, fgcolorOption, lightModeOption }; quotesCommand.Subcommands.Add(readCommand); Command deleteCommand = new("delete", "Delete lines from the file."); deleteCommand.Options.Add(searchTermsOption); quotesCommand.Subcommands.Add(deleteCommand); Command addCommand = new("add", "Add an entry to the file."); addCommand.Arguments.Add(quoteArgument); addCommand.Arguments.Add(bylineArgument); addCommand.Aliases.Add("insert"); quotesCommand.Subcommands.Add(addCommand); readCommand.SetAction(parseResult => ReadFile( parseResult.GetValue(fileOption), parseResult.GetValue(delayOption), parseResult.GetValue(fgcolorOption), parseResult.GetValue(lightModeOption))); deleteCommand.SetAction(parseResult => DeleteFromFile( parseResult.GetValue(fileOption), parseResult.GetValue(searchTermsOption))); addCommand.SetAction(parseResult => AddToFile( parseResult.GetValue(fileOption), parseResult.GetValue(quoteArgument), parseResult.GetValue(bylineArgument)) ); return rootCommand.Parse(args).Invoke(); } internal static void ReadFile(FileInfo file, int delay, ConsoleColor fgColor, bool lightMode) { Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black; Console.ForegroundColor = fgColor; foreach (string line in File.ReadLines(file.FullName)) { Console.WriteLine(line); Thread.Sleep(TimeSpan.FromMilliseconds(delay * line.Length)); } } internal static void DeleteFromFile(FileInfo file, string[] searchTerms) { Console.WriteLine("Deleting from file"); var lines = File.ReadLines(file.FullName).Where(line => searchTerms.All(s => !line.Contains(s))); File.WriteAllLines(file.FullName, lines); } 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}"); } } ``` 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 ``` If you try to run subcommand `quotes`, 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 The file in the output folder reflects the changes from the `add` and `delete` commands. The copy of the file in the project folder remains unchanged. In this tutorial, you created a simple command-line app that uses `System.CommandLine`. To learn more about the library, see [System.CommandLine overview](https://learn.microsoft.com/en-us/dotnet/standard/commandline/). To enable tab completion capabilities, see [Tab completion for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-enable-tab-completion).
Shard168 (laksa)
Root Hash14615152987638977768
Unparsed URLcom,microsoft!learn,/en-us/dotnet/standard/commandline/get-started-tutorial s443