Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C# 4.0

Intelligent Command Line Parser

Rate me:
Please Sign up or sign in to vote.
4.90/5 (27 votes)
29 May 2012CPOL4 min read 38.1K   1K   83   19
Parses command line arguments and converts them into objects for use in your application

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. 

C#
[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
C#
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:

C#
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. 

C#
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. 

C#
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. 

Image 1

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.

C#
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.

C#
public static object ConvertFromString(string s, Type t)
{
    if (t.IsEnum)
    {
        return Enum.Parse(t, s);
    }

    // boolean accepts argument values of true, True, false, False, 1, 0
    if (t == typeof(bool))
    {
        // If type is bool with null or empty value return true
        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: 

Image 2

History

  • May 28, 2012: Initial version submitted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Performance Web
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralWell that's pretty good Pin
PIEBALDconsult5-Jan-13 5:07
mvePIEBALDconsult5-Jan-13 5:07 
GeneralMy vote of 5 Pin
Vitaly Tomilov10-Jun-12 4:57
Vitaly Tomilov10-Jun-12 4:57 
GeneralRe: My vote of 5 Pin
Jordan Pullen10-Jun-12 12:40
professionalJordan Pullen10-Jun-12 12:40 
GeneralMy vote of 4 Pin
Paul_Williams5-Jun-12 1:31
Paul_Williams5-Jun-12 1:31 
QuestionMono.GetOptions Pin
Dude224-Jun-12 15:38
Dude224-Jun-12 15:38 
QuestionA good solution to part of the problem Pin
Peter_in_27804-Jun-12 13:46
professionalPeter_in_27804-Jun-12 13:46 
Questionnuget Pin
theperm4-Jun-12 11:29
theperm4-Jun-12 11:29 
GeneralMy vote of 5 Pin
Derek Viljoen4-Jun-12 9:34
Derek Viljoen4-Jun-12 9:34 
QuestionThanks, have a five... Pin
pt14011-Jun-12 23:23
pt14011-Jun-12 23:23 
AnswerRe: Thanks, have a five... Pin
Jordan Pullen3-Jun-12 11:36
professionalJordan Pullen3-Jun-12 11:36 
QuestionNice Approach: my 5! Pin
Andreas Gieriet31-May-12 7:37
professionalAndreas Gieriet31-May-12 7:37 
AnswerRe: Nice Approach: my 5! Pin
Jordan Pullen1-Jun-12 10:52
professionalJordan Pullen1-Jun-12 10:52 
GeneralMy vote of 5 Pin
Ravi Lodhiya29-May-12 19:06
professionalRavi Lodhiya29-May-12 19:06 
Excellent Jordan!
GeneralMy vote of 5 Pin
Jerome Vibert29-May-12 9:22
Jerome Vibert29-May-12 9:22 
GeneralRe: My vote of 5 Pin
Jordan Pullen29-May-12 9:37
professionalJordan Pullen29-May-12 9:37 
GeneralIntresting approach Pin
Jani Giannoudis29-May-12 8:31
Jani Giannoudis29-May-12 8:31 
GeneralRe: Intresting approach Pin
Jordan Pullen29-May-12 9:37
professionalJordan Pullen29-May-12 9:37 
QuestionAwesome Jordan! Pin
Tom Clement29-May-12 4:50
professionalTom Clement29-May-12 4:50 
AnswerRe: Awesome Jordan! Pin
Jordan Pullen29-May-12 5:04
professionalJordan Pullen29-May-12 5:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.