Introduction
This lightweight implementation approach of a command line parser is small enough to fit in one C# file. It is complete enough that it covers most of the common command line parsing tasks:
- 'Unlimited' number of Option Alias Names (e.g., for '-verbose', you can add these aliases: '-v', '/v', '/verbose', ..).
- Supports options starting with '-' or '/'.
- Supports option and parameter attached in one argument (e.g., -P=123 ).
- Or as an argument pair (e.g./ -P 123).
- Basic 'String', 'Integer', and 'Double' parameter options support.
- Intelligently handles different number decimal separators.
- Contains a basic usage (help) message of the available (registered) options.
but stays simple enough that it can be extended or changed.
Background
Looking for a command line parsing solution, I found that the existing approaches could be separated into two categories:
- Very simple or specialized solutions, or
- rather heavy weight solutions.
I missed a lightweight approach that is complete enough as a ready to use solution for quick jobs and extensible enough that it provides a solid base for extensions and changes.
The reason for more complete solutions to have the tendency to be heavy weight come from the complexity of parameter type conversion. Although it is very tempting to try to provide a solution for all different object types for 'option parameters', a better and more modern approach is to use 'method injection' to make it easy to create and add your own custom options.
An example
CMDLineParser parser = new CMDLineParser();
CMDLineParser.Option DebugOpt =
parser.AddBoolSwitch("-Debug", "Print Debug information");
DebugOpt.AddAlias("/Debug");
CMDLineParser.NumberOption NumOpt = parser.AddDoubleParameter("-NegNum=",
"A required negativ Number", true);
try
{
parser.Parse(args);
}
catch (CMDLineParser.CMDLineParseException ex)
{
Console.Write(parser.HelpMessage());
Console.WriteLine();
Console.WriteLine("Error: " + ex.Message);
return;
}
if (DebugOpt.isMatched) parser.Debug();
args = parser.RemainingArgs();
double num = NumOpt.Value;
In a Windows application, you do not have a console window. In that case, you may want to replace 'Console.WriteLine(..)
' with 'System.Windows.Forms.MessageBox.Show(..)
'.
Dealing with invalid command line arguments
The following basic 'Exceptions' can occur while parsing the command line:
MissingRequiredOptionException | A required option was not detected. |
DuplicateOptionException | A duplicate option was detected: this is an exception because a duplicate would override a previously set parameter! |
ParameterConversionException | Trying to convert an option parameter to a specified data type (e.g., double value) failed. |
InvalidOptionsException | Invalid, not registered options have been detected. |
An 'InvalidOptionsException
' is only thrown when:
parser.throwInvalidOptionsException = true;
It is also possible to disable the collection of invalid (not recognized) options:
parser.collectInvalidOptions = false;
leaving these options in the remaining arguments list:
args = parser.RemainingArgs();
For simplicity, all described exceptions can be caught using the base class CMDLineParser.CMDLineParseException
, as in the above example.
Note that in a 'NumberOption' like "-Num -16.5", the second argument will not be confused with an invalid command line option but will be identified as a parameter.
For those who just want to use the solution, this is probably all you need to know.
Discussion
Command line parsing can be separated into two problem areas:
- Identification of options and option parameters on the command line.
- Handling (type conversion) of parameter values (e.g., a double number).
The first point is a core feature of every command line parser. Regarding the second point, it could be questioned if converting parameters to different types or objects is better done through sub classing or by the application itself, instead of designing it as a core part of the command line parser. For example, a useful idea is to pass the format information for a specific parameter as a second parameter and use this information to do the actual conversion in the 'application context':
-myDate 2002-9-12 -myDateFormat "yyyy-m-dd"
From a good OO design perspective, you might want to 'inject' argument checking in your application (i.e., to expose such a check method to other interfaces) instead of having it hard coded as a part of the command line parser. Therefore, a better strategy than trying to provide an (command line) 'option' solution for every possible object type, and validation possibility which makes the parser solution heavy weight, is a 'method injection' approach. It has the advantage that option parameters can be converted and validated to your own needs.
Creating and adding custom options
Custom options can be created by sub classing CMDLineParser.Option
and providing an implementation of object parseValue(string parameter)
which does the actual conversion and validation, as in the following example:
class PastDateOption : CMDLineParser.Option
{
public PastDateOption(string name, string desription, bool required)
: base(name, desription, typeof(DateTime), true, required) { }
public override object parseValue(string parameter)
{
DateTime date= System.Convert.ToDateTime(parameter);
DateTime now = DateTime.Now;
if(date > now)
throw new System.ArgumentException("Date: " + date +
" is greater then: " + now);
return date;
}
}
Use AddOption(..)
to add the new option to the parser:
PastDateOption DateOpt =
new PastDateOption("-Date","A date in the past",false);
parser.AddOption(DateOpt);
If a custom option is matched on the command line, parseValue(..)
will automatically be called to convert the option parameter and set the option value. In cases of error, it will throw a ParameterConversionException
.
Note that in the example above, the date convert function will react dependent on the international settings of your local system.
Globalization issues
For good reason, most of the conversion functions built in C# provide the possibility to pass format information. Making use of a generic approach like Convert.ChangeType(parameter, type)
to convert all kinds of parameter data types is tempting, but has the implication that the run time result depends on the international settings of your local system. For numbers, we can provide a more intelligent solution.
Because two different decimal separators are defined in different 'Cultures', converting numbers from the command line makes it necessary to specifically set the 'number format' expected to be passed as a command line parameter when using:
Double.Parse(parameter, _numberstyle, _numberformat);
For the default NumberOption
, I intentionally did not make use of the local system setting using the 'CurrentCulture
' because it introduces an unnecessary globalization dependency. I remember many years ago (before C#!), how annoying it was if my command line parser did not understand a scientifically computed number (i.e., with "."); eventually, I figured out that it was reacting to my international settings (i.e., it expected ","). A better approach is to use the so called 'invariant Culture' as a default setting:
_numberformat = (new CultureInfo("", false)).NumberFormat;
This setting expects a decimal point "." as a decimal separator (similar to a pocket calculator). However, approximately 50% of the Culture settings (that use Arabic numbers) use a comma "," as a decimal separator symbol.
By simply changing the 'decimal separator' to the separator found in the parameter argument, we can easily provide an implementation that accepts both formats ('.' and ','). This works well as long as NumberStyles.AllowThousands
is turned off when parsing numbers. Taking into account that it is quite uncommon to use floating point numbers with 'Thousands' separator on the command line, accepting both formats ('.' and ',') is a more 'Culture Invariant' solution.
In some cases, parsing a specific (globalized) number format is desired. In that case, NumberFormatInfo
can be set by creating the NumberOption
:
NumberFormatInfo numberformat= (new CultureInfo( "de-DE", false )).NumberFormat;
CMDLineParser.NumberOption NegnumOpt =
parser.AddDoubleParameter("-Num", "A Number",
false, numberformat);
Conclusion
Parsing command line arguments is as old a requirement as the existence of console applications. This lightweight, 'one file' implementation aims to support projects or situations in which a lightweight implementation is sufficient.
References and credits
The article Automatic Command Line Parsing in C# by Ray Hayes influenced the creation of this article, so I would like to mention it here. It focuses on automatically creating command line options from class objects via Reflection.
In the Java world, the 'jargs project' has a similar 'lightweight' approach (with a focus on GNU compatible options).