J4JCommandLine is an easy to use command line parser for Net5 and above applications. It works with Microsoft's IConfiguration API so that the resulting configuration object can draw from a variety of sources in addition to the command line.
Introduction
There are a number of command line parsers available for .NET, some of which work with Microsoft's IConfiguration
API. I wrote J4JCommandLine
because I was intrigued by how command line parsing works behind the scenes... and because the parsers I found struck me as difficult to configure. In fairness, that's probably because they are more flexible than J4JCommandLine
, allowing you to bind command lines to all sorts of targets.
Restricting the parser's scope to the IConfiguration
system makes it less flexible as to what it can target. But it gains more than it loses (in my opinion, at least :)) because it can now tap into configuration information from a variety of sources: the command line, JSON configuration files, user secrets, and more.
Background
Originally, J4JCommandLine
was "just another command line parser" because I was curious about LL(1) parsers and wanted to write one.
"LL(1)" means the parser scans tokens from Left to right, and only looks 1 token ahead (the second "L" is about following/constructing "left hand" routes in a tree data structure; I'm not sure about that because I didn't use a tree-based approach in J4JCommandLine
).
J4JCommandLine
isn't, technically, an LL(1) parser because it does some pre-processing of the tokens it generates before parsing them. The main such step being to merge all tokens between a starting "quoter" token and an ending "quoter" token into a single test token.
You can read more about J4JCommandLine
and conceptually how it parses command lines by consulting the Github documentation. But for now, the key milestone for me was when, after getting it working, I realized there was a way to make it work with Microsoft's IConfiguration
system.
You create an instance of IConfiguration
(technically, IConfigurationRoot
) by creating an instance of ConfigurationBuilder
, adding providers (of configuration information) to it and then calling its Build()
method:
var parser = testConfig.OperatingSystem.Equals( "windows", StringComparison.OrdinalIgnoreCase )
? Parser.GetWindowsDefault( logger: Logger )
: Parser.GetLinuxDefault(logger: Logger);
_options = parser.Collection;
_configRoot = new ConfigurationBuilder()
.AddJsonFile("some file path")
.AddUserSecrets<ConfigurationTests>()
.AddJ4JCommandLine(
parser,
out _cmdLineSrc,
Logger )
.Build();
Behind the scenes, the providers are queried for key/value pairs of property names and values. When you request an object from the IConfiguration
system, those are used to initialize the object:
var configObject = _configRoot.Get<SomeConfigurationObject>();
J4JCommandLine
's parser works with its provider to translate command line text into configuration values.
Using the Code
J4JCommandLine
is pretty configurable but basic usage is very simple provided the defaults work for you. All you need to do is add the provider to your ConfigurationBuilder
instance, invoke Build()
and go.
However, that won't result in your command line being parsed... because you have to tell the parser what command line options it may encounter, what types those are, whether they're required, etc. You do that by binding properties of your configuration
object to command line tokens.
Here's an example using a simple configuration
object:
public class Configuration
{
public int IntValue { get; set; }
public string TextValue { get; set; }
}
In your startup code, you'd do something like this:
var config = new ConfigurationBuilder()
.AddJ4JCommandLineForWindows( out var options, out _ )
.Build();
options!.Bind<Configuration, int>(x => Program.IntValue, "i")!
.SetDefaultValue(75)
.SetDescription("An integer value");
options.Bind<Configuration, string>(x => Program.TextValue, "t")!
.SetDefaultValue("a cool default")
.SetDescription("A string value");
options.FinishConfiguration();
Now when you call:
var parsed = config.Get<Configuration>();
the resulting Configuration
object will reflect the command line arguments.
There is also a TryBind<>()
method which you can use instead of Bind
so you don't have to check the return value for being non-null
(which would indicate the binding could not be done). I didn't include the check for null
code in the example just to keep things simple.
What Operating Systems Are Supported?
I've tried to make J4JCommandLine
"operating system agnostic" since .NET can run on non-Windows platforms these days. Frankly, I haven't tested it under anything other than Windows... but the logic to support other systems is there.
You control how J4JCommandLine
works, operating-system-wise, by telling it what "lexical elements" to use and whether or not command line keys (e.g., the x in /x) are case sensitive or not. Behind the scenes, that's part of what the AddJ4JCommandLineForWindows()
and AddJ4JCommandLineForLinux()
extension methods do: they set those operating system specific parameters to reasonable defaults.
Here are the defaults:
Operating System Defaults
| Lexical Elements | Keys Case Sensitive? |
Windows | quoters: " '
key prefix: /
separators: [space] [tab]
value prefix: = | No |
Linux | quoters: " '
key prefixes: - --
separators: [space] [tab]
value prefix: = | Yes |
Quoters define text elements that should be treated as a single block. Key prefixes indicate the start of an option key (e.g., /x or -x). Separators separate tokens on the command line. Value prefixes (which I rarely use since regular separators seem to work just fine) link an option key to a text element (e.g., /x=abc).
Keep in mind that when there are multiple valid key prefixes (e.g., - and --) either can be used with any key. So -a, --ALongerKey, -ALongerKey and --a are all valid so far as J4JCommandLine
is concerned. That's not the "Linux way" but I haven't figured out how to do things "right" yet.
What Kinds of Properties Can You Bind To?
J4JCommandLine
cannot bind command line options to any random C# type. It has to be able to convert one or more text values to instances of the target type and that requires there be a conversion method available. In that, it's no different from the IConfiguration
API itself, which depends on C#'s built-in Convert
class to convert text to type instances.
However, J4JCommandLine
is extensible as regards conversions; you can define your own converter methods for changing text into an instance of a custom type. Consult the Github documentation for details... and keep in mind that portion of my codebase isn't well-tested.
As a practical matter, I doubt you'll need to define your own converters. By default, J4JCommandLine
can convert any type which has a corresponding method in C#'s built-in Convert
class (in fact, J4JCommandLine
simply wraps those methods to match its specific needs).
Binding to Collections
In addition to being able to bind to most commonly used configuration types, J4JCommandLine
can also bind to certain types of collections of those types:
- arrays of supported types (e.g.,
string[]
) - lists of supported types (e.g.,
List<string>
)
The IConfiguration
system also allows you to bind to Dictionaries
. But I haven't figured out how to make that work within the context of a command line yet.
There is another, less obvious limitation of what can be bound. J4JCommandLine
can bind to enums
natively, even flagged enums
(i.e., enums
marked with the [Flag] attribute
). But it cannot bind to collections of flagged enums
. That's because it can't tell whether a sequence of text tokens corresponding to enum
values should be concatenated into a single, OR'd result or should be treated as a collection of unconcatenated enums
.
Nested Properties and Public Parameterless Constructors
J4JCommandLine
shares IConfiguration
's requirement that configuration objects, and the properties being bound, must generally have public
parameterless constructors
. That's because the IConfiguration
API has to be able to create instances internally, without any knowledge of how to actually do that.
There is an exception to that requirement, however: if the constructor logic of your configuration object takes care of initializing the properties, then those properties do not need public parameterless constructors
. Here's an example:
public class EmbeddedTargetNoSetter
{
public BasicTargetParameteredCtor Target1 { get; } = new( 0 );
public BasicTargetParameteredCtor Target2 { get; } = new( 0 );
}
public class BasicTargetParameteredCtor
{
private readonly int _value;
public BasicTargetParameteredCtor( int value )
{
_value = value;
}
public bool ASwitch { get; set; }
public string ASingleValue { get; set; } = string.Empty;
public List<string> ACollection { get; set; } = new();
public TestEnum AnEnumValue { get; set; }
public TestFlagEnum AFlagEnumValue { get; set; }
}
Even though the type BasicTargetParameteredCtor
doesn't have a public
parameterless constructor
, you can still bind to the Target1
and Target2
properties because they are initialized by EmbeddedTargetNoSetter
's constructor logic (in this case, implicitly via those new()
calls).
This example also highlights another thing about J4JCommandLine
: you can bind to nested properties:
Bind<EmbeddedTargetNoSetter, bool>( _options!, x => x.Target1.ASwitch, testConfig );
Bind<EmbeddedTargetNoSetter, string>( _options!, x => x.Target1.ASingleValue, testConfig );
Bind<EmbeddedTargetNoSetter, TestEnum>( _options!, x => x.Target1.AnEnumValue, testConfig );
Bind<EmbeddedTargetNoSetter, TestFlagEnum>
( _options!, x => x.Target1.AFlagEnumValue, testConfig );
Bind<EmbeddedTargetNoSetter, List<string>>
( _options!, x => x.Target1.ACollection, testConfig );
Providing Help
It's not uncommon for users to provide invalid arguments on the command line. Letting them know what they should've entered is important. J4JCommandLine
addresses this by providing an interface for you to use to display help information that you associate with bound options. Here's how you might do this:
options!.Bind<Program, int>(x => Program.IntValue, "i")!
.SetDefaultValue(75)
.SetDescription("An integer value")
.IsOptional();
options.Bind<Program, string>(x => Program.TextValue, "t")!
.SetDefaultValue("a cool default")
.SetDescription("A string value")
.IsRequired();
options.FinishConfiguration();
var help = new ColorHelpDisplay(new WindowsLexicalElements(), options);
help.Display();
You describe an option by calling extension-methods on it. These let you set a default value, describe what it's for, and indicate whether it's required or optional (the default is optional).
Normally, of course, you wouldn't always display the help information the way this code snippet does. You'd only display help if the user asked for it (e.g., by setting a command line option) or if there was a problem with whatever configuration object got created.
Note that J4JCommandLine
does not automatically display help when it detects a problem. That's up to you.
The J4JCommandLine
assembly includes a plain-vanilla help display engine called DefaultHelpDisplay
. But I generally prefer to use the more colorful display provided by the ColorfulHelp
library which is included in the github repository. Whichever you use (or if you roll your own) you have to configure the help system with information about the "lexical elements" being used (i.e., the special characters, like / ? = ") and the option collection you've defined. That's so the help system can extract and display information about how the options should be specified/formatted, their default values, whether they're required, etc.
Logging
J4JCommandLine
uses my companion library J4JLogger
to log problems. The logging capability is optional -- by default, nothing is logged. But if you want to know what's going on inside J4JCommandLine,
it's a good thing to include.
Refer to the CodeProject article on J4JLogger or its github repository for more information.
Points of Interest
When I started digging into how the IConfiguration
system works, I expected to find something along the lines of how I structured J4JCommandLine
's conversion capabilities. Interestingly, I couldn't find it. Instead, it looks like the IConfiguration
API simply relies on the built-in Convert
class and fails if there isn't a conversion method available.
In a similar vein, the IConfiguration
system does its checks (e.g., for a property type having a public
parameterless constructor) in an unstructured sense. There doesn't appear to be a set of (potentially extensible) rules. Instead, the checks are simply hard-wired into the codebase.
History
- 1st October, 2021: Initial release on CodeProject