Since really .NET Framework 1, the ability for .NET Console apps to parse command line flags and actually provide helpful feedback to the user on even the availability of such flags has been severely lacking.
What do I mean by that? Well when you create a new console application in C#/.NET/.NET Core, your code will be given a simple array of string arguments. These won’t be filtered in any way and will basically just be given to you wholesale. From there, it’s up to you to create your own level of boilerplate to parse them out, run any validation you need to, *then* finally get on to actually creating the logic for your app :
static int Main(string[] args) { //Boilerplate for parsing the args array goes here }
And it’s not like out of the box, someone running the console application can get helpful feedback on the flags either. If you compare that to say a simple “dotnet” command. Running it without any flags gives you atleast some helpful information on possible options to get things up and running.
C:\Users\wadeg> dotnet Usage: dotnet [options] Usage: dotnet [path-to-application] Options: -h|--help Display help. --info Display .NET information. --list-sdks Display the installed SDKs. --list-runtimes Display the installed runtimes. path-to-application: The path to an application .dll file to execute.
But all that’s about to change with Microsoft’s new library called System.CommandLine!
Creating A Simple Console App The Old Fashioned Way
Before we go digging into the new goodies. Let’s take a look at how we might implement a simple console application parsing the string args ourselves.
Here’s a console application I created earlier that simply greets a user with their given name, title, and will change the greeting depending on if we pass in a flag saying it’s the evening.
static int Main(string[] args) { string name = string.Empty; string title = string.Empty; bool isEvening = false; for (int i = 0; i < args.Length; i++) { var arg = args[i].ToLower(); if (arg == "--name") { name = args[i + 1]; } if (arg == "--title") { title = args[i + 1]; } if (arg == "--isevening") { isEvening = true; } } if (string.IsNullOrEmpty(name)) { Console.WriteLine("--name is a required flag"); return -1; } var greeting = isEvening ? "Good evening " : "Good day "; greeting += string.IsNullOrEmpty(title) ? string.Empty : title + " "; greeting += name; Console.WriteLine(greeting); return 0; }
The code is actually quite simple, but let’s take a look at it bit by bit.
I’ve had to create a sort of loop over the args to work out which ones were actually passed in by the user, and which ones weren’t. Because the default args doesn’t actually distinguish between what’s a flag and what’s a passed in parameter value, this is actually quite messy.
I’ve also had to write my own little validator for the “–name” flag because I want this to be mandatory. But there’s a small problem with this..
How can a user know that the name flag is mandatory other than trial and error? Really they can’t. They would likely run the application once, have it fail, and then add name to try again. And for our other flags, how does a user know that these are even an option? We would have to rely on us writing good documentation and hope that the user reads it before running (Very unlikely these days!).
There really isn’t any inbuilt help with this application, we could try and implement something that if a user passed in a –help flag, we would return some static text to help them work out how everything runs, but this isn’t self documenting and would need to be updated each time a flag is updated, removed or added.
The reality is that in most cases, this sort of helpful documentation is not created. And in some ways, it’s relegated C# console applications to be some sort of quick and dirty application you build for other power users, but not for a general everyday developer.
Adding System.CommandLine
System.CommandLine is actually in beta right now. To install the current beta in your application you would need to run the following from your Package Manager Console
Install-Package System.CommandLine -Version 2.0.0-beta1.20574.7
Or alternatively if you’re trying to view it via the Nuget Browser in Visual Studio, ensure you have “Include prerelease” ticked.
Of course by the time you are reading this, it may have just been released and you can ignore all that hassle and just install it like you would any other Nuget package!
I added the nuget package into my small little greeter application, and rejigged the code like so :
static int Main(string[] args) { var nameOption = new Option( "--name", description: "The person's name we are greeting" ); nameOption.IsRequired = true; var rootCommand = new RootCommand { nameOption, new Option( "--title", description: "The official title of the person we are greeting" ), new Option( "--isevening", description: "Is it evening?" ) }; rootCommand.Description = "A simple app to greet visitors"; rootCommand.Handler = CommandHandler.Create<string, string, bool>((name, title, isEvening) => { var greeting = isEvening ? "Good evening " : "Good day "; greeting += string.IsNullOrEmpty(title) ? string.Empty : title + " "; greeting += name; Console.WriteLine(greeting); }); return rootCommand.Invoke(args); }
Let’s work through this.
Unfortunately, for some reason the ability to make an option “required” cannot be done through an option constructor, hence why our first option for –name has been setup outside our root command. But again, your mileage may vary as this may be added before the final release (And it makes sense, this is probably going to be a pretty common requirement to make things as mandatory).
For the general setup of our flags in code, it’s actually pretty simple. We say what the flag name is, a description, and we can even give it a type right off the bat so that it will be parsed before getting to our code.
We are also able to add a description to our application which I’ll show shortly why this is important.
And finally, we can add a handler to our command. The logic within this handler is exactly the same as our previous application, but everything has been set up for us and passed in.
Before we run everything, what happens if we just say run the application with absolutely no flags passed in.
Option '--name' is required. CommandLineExample: A simple app to greet visitors Usage: CommandLineExample [options] Options: --name <name> (REQUIRED) The person's name we are greeting --title <title> The official title of the person we are greeting --isevening Is it evening? --version Show version information -?, -h, --help Show help and usage information
Wow! Not only has our required field thrown up an error, but we’ve even been given the full gamut of flags available to us. We’ve got our application description, each flag, and each flags description of what it’s intended to do. If we run our application with the –help flag, we would see something similar too!
Of course there’s only one thing left to do
CommandLineExample.exe --name Wade Good Day Wade
Pretty powerful stuff! I can absolutely see this becoming part of the standard .NET Core Console Application template. There would almost be no reason to not use it from now on. At the very least, I could see it becoming a checkbox when you create a Console Application inside Visual Studio to say if you want “Advanced Arguments Management” or similar, it really is that good!
this could work too, and will reduce a line or two –
new Option(“–name”, description: “The person’s name we are greeting”){ IsRequired = true }
Good point! I have this irrational hate for using a constructor *and* an object initializer on the same line. But expanding a single line out to multiple lines in my original example is definitely worse.
With the DragonFruit add-on package (https://github.com/dotnet/command-line-api/blob/main/docs/Your-first-app-with-System-CommandLine-DragonFruit.md). You can just use direct parameters in the Main method (in place of args) and it will generate the command line flags and fill in the values:
This is great for small quick projects. One downside is that it doesn’t support a short alias for the flag (I’ve requested this as a feature). Another command line parser tool, Cocona, supports this using OptionAttributes on the parameters (https://github.com/mayuki/Cocona#options) which I think is a nice solution.
Pretty nice, have you put the code in some repo?