Introduction
Since I started working with MSBuild I have found it increasingly necessary to use command line versions of tools used to build applications. These vary from generators to database deployers. A common problem that I found was the lack of options when executing the programs. Developers lavish time on the GUI version of the tools but rarely think to add the same functionality the command line versions. This article should help add some much needed and easy to use functionality.
Background
There are many command line parsing utilities out there but not many that are just simple to use. What I wanted was a way to define a class in my application with all the command line options as properties and have the parser populate them from the command arguments, reporting any errors along the way. To make its use simple, we use attributes to identify the salient characteristics of each property in the class. I decided to write my own and here it is.
Using the code
Creating the Argument Class
Firstly create a class which implements the IArgumentDefinition
interface or the ArgumentDefinitionBase
class. These provide basic functionality such as whether the values are valid and a GetUsage
method which displays the argument usage instructions. The ArgumentDefinitionBase
class is an abstract class which provides a default usage output. This is demonstrated on the screenshot below. This is all automatically generated based on the properties defined in your argument class. If you wish to use your own implementation then use the interface and define your own GetUsage
method.
In order for the properties in your argument class to be utilised they must be decorated with the [Argument]
attribute. This maps the command line argument and value to the property using reflection. I have tested with basic types string
, int,
bool
etc and also with enumerations.
[Argument(ShortName = "n", LongName = "number", Description = "A numerical value for demonstration purposes", Required = true)]
public int Number { get; set; }
The meaning of the parameters are as follows:
ShortName
is the short name of the argument (-n or /n). LongName
is the full name of the argument (-number or /number) Description
is the description displayed to the user when the GetUsage
method is called.Required
lets the parser know that the application is expecting this argument. An exception will be thrown if the argument is not provided.
In the demonstration application I created the
ConsoleArguments
class as follows
public class ConsoleArguments : ArgumentDefinitionBase
{
[Argument(ShortName = "n", LongName = "number", Description = "A numerical value for demonstration purposes", Required = true)]
public int Number { get; set; }
[Argument(ShortName = "f", LongName = "filename", Description = "The file to be processed", Required = true)]
public string FileName { get; set; }
[Argument(ShortName = "a", LongName = "all", Description = "The process all files", Required = false)]
public bool All { get; set; }
}
Using in your application
Now you have created a class to hold the arguments you can go ahead and wire up your application to the parser. This is called the CommandLineManager
. To populate your ConsoleArguments
class use the following:
var arguments = CommandLineManager.GetCommandLineArguments<ConsoleArguments>(args, Console.Out);
The GetConsoleArguments
method takes the console output stream as a parameter so any errors can be printed to the console window such as an invalid parameter name. Now you can test if your parsing was successful.
if (arguments.IsValid == false)
{
Console.WriteLine(arguments.GetUsage());
Environment.Exit(1);
}
This makes use of the IsValid
property and GetUsage
method defined in the IArgumentDefinition
interface. In this case the usage text is displayed in the console window if the arguments are invalid.
There is no specific argument to supply in order to display the usage instructions. This is something that the user can include in their own application. You may wish to use -h, -help, -? etc. You can add this to your argument class and test if it is present.
Usage
- Each argument must be prefixed with either '-' or a '/' .
- Values are expressed as -argument:value.
- Boolean values can be expressed as 'true', 'True', 1, 'false', 'False'. They can also be expressed without value: -m (same as -m:true).
- Enumerations are supported provided they match those defined.
How it works
At the basic level the code works by using reflection to inspect the properties in the Argument class defined by the user. These are then mapped to the arguments supplied from the command line.
The CommandLineManager
class accepts the arguments as an array of strings together with a TextWriter
object which is used to output any error messages or usage instructions. If successful will return the populated arguments class. Any Exceptions are caught here and feedback is provided by writing to the TextWriter
class passed in.
public static T GetCommandLineArguments<T>(string[] args, TextWriter output )
where T : IArgumentDefinition, new()
{
T definition = new T();
definition.IsValid = true;
ArgumentMapManager mapManager = new ArgumentMapManager((IArgumentDefinition)definition);
Parser p = new Parser(mapManager);
try
{
p.ParseArguments(args);
}
catch(Exception ex)
{
definition.IsValid = false;
output.WriteLine(ex.Message);
}
return definition;
}
The job of the ArgumentMapManager
is to create a List of ArgumentMap
objects. This is a list of PropertyInfo
objects from the argument definition class mapped to the argument attributes. These are held for the Parser class to allow it to populate the user defined argument definition class with the values from the command line.
Next the Parser
object is created and passed the ArgumentMapManager
in the constructor. The Parser
class then can parse the arguments. Each argument is parsed in turn using the Argument
class.
public Argument(string arg)
{
string argument = string.Empty;
if (arg.StartsWith("/") || arg.StartsWith("-"))
{
argument = arg.Substring(1, arg.Length - 1);
}
else
{
throw new InvalidArgumentException(arg);
}
var vals = argument.Split(':');
for (int i = 0; i < vals.Length; i++)
{
if (i == 0)
{
this.Name = vals[i];
}
else
{
this.Value += vals[i];
if (i < vals.Length - 1)
{
this.Value += ":";
}
}
}
if (!string.IsNullOrWhiteSpace(this.Value))
{
this.Value = this.Value.Trim('"');
}
}
A list of required arguments is compiled from the ArgumentMapManager
. These are checked against the parsed arguments. The ObjectSerialiser
class then uses the SetValue
method to assign the value from the argument to the argument definition. The ConvertFromString
method converts the string value of the argument and returns the valid type.
public static object ConvertFromString(string s, Type t)
{
if (t.IsEnum)
{
return Enum.Parse(t, s);
}
if (t == typeof(bool))
{
if (string.IsNullOrWhiteSpace(s))
{
return true;
}
bool b;
if (bool.TryParse(s, out b))
{
return b;
}
int i = Convert.ToInt32(s);
if (i < 0 || i > 1)
{
throw new FormatException("Invalid boolean type. Must be 0 or 1.");
}
return Convert.ToBoolean(i);
}
return Convert.ChangeType(s, t);
}
Unit Tests
There is a unit test project included to test each class.
Demonstration Application
All the code described above is included in the demonstration application (ConsoleCommandLine). There is also a batch file with an example invocation.
@echo off
cd ./bin/debug
ConsoleCommandLine
ConsoleCommandLine -number:44 -f:"c:\test.txt"
pause
The first call is incorrect and will produce an error as required arguments (number and filename) have not been supplied. The second call is successful and the ConsoleArguments
class is populated. This is the output:
History
- May 28, 2012: Initial version submitted.