Not Last Console. Command Line Parser

 
event

Next in line to be tried is Command Line Parser.
Surely comes as a NuGet package but, surprisingly, project documentation in master points to a beta version, not the stable one. Until I noticed, it caused me a couple of head scratches and a certain deal of fist waving.

Hybrid-nature

During this walk-through, I will use the Beta version, not the latest stable one as support for verbs (a very central concept in our sample application) has improved drastically and because, well, it is what you are shown in the project documentation page.

The "hybrid" moniker comes from the fact that the definition of the arguments is detached from the command actions themselves. That is, one creates an argument object for each command and decorates it with attributes that provide the metadata for the command-line behavior: Verb for the container and Option for each property (with attribute properties to drive command-line behaviors: names, help text,...).
Once those verbs are ready, the arguments are parsed and mapped into each of the verbs where an action is provided for the execution when the command is dispatched.

Implementing our example

The implementation of our command options is simple and uncompromised:

[Verb("something", HelpText = "Do something")]
public class Something
{
[Option('l', "location", HelpText = "place where the something was done", Required = true)]
public string Location { get; set; }
[Option('t', "times", HelpText = "number of times something was done", Default = 1, Required = false)]
public int Times { get; set; }
[Option('a', "awesome", HelpText = "include if the something was awesome", Required = false)]
public bool Awesome { get; set; }
}
[Verb("something-else", HelpText = "Do something else")]
public class SomethingElse
{
[Option('l', "locations", HelpText = "places where the something else was done", Required = true, Separator = ',')]
public IEnumerable<string> Locations { get; set; }
[Option('a', "awesome", HelpText = "include if the something else was awesome", Required = false, Default = true)]
public bool Awesome { get; set; }
}
view raw arguments.cs hosted with ❤ by GitHub

Dispatching is also simple: instantiate, translate one argument object into another (redundant if we embraced the framework) and call the method.

static void Main(string[] args)
{
Parser.Default.ParseArguments<Something, SomethingElse>(args).MapResult(
(Something something) =>
{
var options = new OptionsForSomething
{
Location = something.Location,
Times = something.Times,
Awesome = something.Awesome
};
var command = new ADoerOfSomething(Console.Out);
command.Do(options);
return 0;
},
(SomethingElse somethingElse) =>
{
var options = new OptionsForSomethingElse
{
Locations = somethingElse.Locations.ToArray(),
NotSoAwesome = !somethingElse.Awesome
};
var command = new ADoerOfSomethingElse(Console.Out);
command.Do(options);
return 0;
},
errors => -1);
}
view raw Main.cs hosted with ❤ by GitHub

Its attribute-driven nature makes things more difficult for localisation.

The Challenges

Mandatory Arguments

Mandatory arguments are not the default, but setting to true the Required property of [Option] does the trick without any fuss and enables easy reporting of missing mandatory attributes:

*NotLastConsole_CommandLineParser*> .\doer.exe something
NotLastConsole_SystemCommandLine 1.0.0.0
Copyright © 2017
ERROR(S):
Required option 'l, location' is missing.
-l, --location Required. place where the something was done
-t, --times (Default: 1) number of times something was done
-a, --awesome include if the something was awesome
--help Display this help screen.
--version Display version information.
view raw mandatory.sh hosted with ❤ by GitHub

Extra, unmapped arguments will make an error appear, but that behavior can be changed.

Non-Textual arguments

Declaring the right type for the property works fine including flags (boolean properties).
Failures, are handled gracefully and dutifully reported:

*NotLastConsole_CommandLineParser*> .\doer.exe something -l here -t asd
NotLastConsole_SystemCommandLine 1.0.0.0
Copyright © 2017
ERROR(S):
Option 't, times' is defined with a bad format.
-l, --location Required. place where the something was done
-t, --times (Default: 1) number of times something was done
-a, --awesome include if the something was awesome
--help Display this help screen.
--version Display version information.
view raw non-textual.sh hosted with ❤ by GitHub

Default values are provided as objects, instead of strings.

Multi-arguments

Bi-dimensional types are supported out of the box as long as the property is defined as IEnumerable<> and the Separator property is set on the [Option].

*NotLastConsole_CommandLineParser*> .\doer.exe something-else -l here,there
I did something else in here, there
view raw multi.sh hosted with ❤ by GitHub

However, there is no help hint in regards of what that separator character might be when displaying help to the user.
For showing some common scenarios, the [Usage] attribute can be used. But it would have been nice to show the separator character just default values are shown.

Showing Help

Running the program without arguments (or with the help verb) provides a nice auto-generated list of supported verbs:

*NotLastConsole_CommandLineParser*> .\doer.exe
NotLastConsole_SystemCommandLine 1.0.0.0
Copyright © 2017
ERROR(S):
No verb selected.
something Do something
something-else Do something else
help Display more information on a specific command.
version Display version information.
view raw help.sh hosted with ❤ by GitHub

And using the help verb on a verb, drills down to the verb-level documentation:

*NotLastConsole_CommandLineParser*> .\doer.exe help something
NotLastConsole_SystemCommandLine 1.0.0.0
Copyright © 2017
-l, --location Required. place where the something was done
-t, --times (Default: 1) number of times something was done
-a, --awesome include if the something was awesome
--help Display this help screen.
--version Display version information.
view raw help-command.sh hosted with ❤ by GitHub

Command Dispatching

As mentioned previously, command definition happens by providing an action when parsing the arguments. The simplest would be using the default instance of Parser just as we have done in our example: Parser.Default.ParseArguments<Something, SomethingElse>(args) and then map the options object to an action via .Map().

There is no command object per-se so if we need some sort of dependency injection it can be easily achieved by calling the container from the command action.

Conclusion

CommandLineParser is a full featured framework that can do pretty much everything we need with command line arguments and, in its unstable version, supports all the scenarios from our baseline without any trouble.

It is both powerful and simple to get going. A nice tool to have in the console tool-belt.

Last Console Series

  1. The beginning
  2. GoCommando
  3. CommandLineParser (this)
  4. PowerArgs
  5. LBi.Cli.Arguments
  6. Command Line Utils
  7. CLAP
  8. Wrap-up