Click here to Skip to main content
15,905,508 members
Articles / Programming Languages / C#
Tip/Trick

An elegant command line options parser

Rate me:
Please Sign up or sign in to vote.
4.58/5 (19 votes)
14 Feb 2015MIT1 min read 32.1K   475   35   4
A simple and powerful command line options parser.

Introduction

Nowadays, we see different syntax for specifying command line options, here are some examples:

  1. msbuild myproject.csproj /verbosity:diag /fileLogger
  2. padrino g project badacm -d mongoid -e slim -c sass
  3. git log --pretty=format: --name-only --diff-filter=A
  4. gem install nokogiri -- --use-system-libraries --with-xml2-config=/path/to/xml2-config
  5. tinycore waitusb=5 host=TCABCD tce=sda1 opt=sda1 home=sda1

Well, which one is better?

According to the following considerations, I think the 5th one is the best.

  • Easy to read, appealing to the eyes.
  • Easy to write, has good expression ability.
  • Easy to understand and remember the options.
  • Easy to implement a parser, and the algorithm is efficient to execute.
  • Easy to access the options when writing programs.

So I write a program to parse this kind of command line options.

CommandLine ::= Command Subcommand? Option*
Option ::= OptionName | OptionName '=' OptionValue

Using the code

The CommandLineOptions class converts an array of arguments to a dictionary of options, with an optional sub command.

C#
using System;
using System.Collections.Generic;

namespace CommandLineUtil
{
    public class CommandLineOptions : IEnumerable<string>
    {
        public string SubCommand { get; private set; }
        public Dictionary<string, string> Options { get; private set; }
        private StringBuilder errors = new StringBuilder();

        public CommandLineOptions(string[] args, bool hasSubcommand = false)
        {
            int optionIndex = 0;
            this.Options = new Dictionary<string, string>();

            if (hasSubcommand)
            {
                if (args.Length > 0)
                {
                    this.SubCommand = args[0];
                    optionIndex = 1;
                }
            }

            for (int i = optionIndex; i < args.Length; i++)
            {
                string argument = args[i];
                int sepIndex = argument.IndexOf('=');

                if (sepIndex < 0)
                {
                    AddOption(argument, null);
                }
                else if (sepIndex == 0)
                {
                    AddOption(argument.Substring(1), null);
                }
                else if (sepIndex > 0)
                {
                    string name = argument.Substring(0, sepIndex);
                    string value = argument.Substring(sepIndex + 1);

                    AddOption(name, value);
                }
            }

            if (errors.Length > 0)
            {
                throw new ArgumentException(errors.ToString());
            }
        }

        public void AddOption(string name, string value)
        {
            if (string.IsNullOrEmpty(name))
            {
                errors.AppendLine("Invalid option: = ");
                return;
            }

            if (this.Options.ContainsKey(name))
            {
                errors.AppendLine("Duplicate option specified: " + name);
            }

            this.Options[name] = value;
        }

        public bool HasOption(string name)
        {
            return this.Options.ContainsKey(name);
        }

        public string this[string name]
        {
            get
            {
                if (this.Options.ContainsKey(name))
                {
                    return this.Options[name];
                }
                else
                {
                    return null;
                }
            }
        }

        public IEnumerator<string> GetEnumerator()
        {
            return this.Options.Keys.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.Options.Keys.GetEnumerator();
        }
    }
}

That's it, very simple yet better than many other solutions.

Note: In this implementation, option names are case sensitive.

To invoke sub commands, either use a switch statement:

C#
static void Main(string[] args)
{
    var options = new CommandLineOptions(args, true);
    switch (options.SubCommand)
    {
        case "GetMachineList":
            GetMachineList(options);
            break;
        case "AbortOverdueJobs":
            AbortOverdueJobs(options);
            break;
        case "ClearOverdueMaintenanceJobs":
            ClearOverdueMaintenanceJobs(options);
            break;
        case "AddParameterToJobs":
            AddParameterToJobs(options);
            break;
        default:
            Console.WriteLine("Unknown subcommand: " + options.SubCommand);
            break;
    }
}

Or get the method by name and invoke:

C#
static void Main(string[] args)
{
    var options = new CommandLineOptions(args, true);

    var method = typeof(Program).GetMethod(
        options.SubCommand,
        BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
        null,
        new Type[] { typeof(CommandLineOptions) },
        null);
    if (method != null)
    {
        method.Invoke(null, new object[] { options });
    }
    else
    {
        Console.WriteLine("Unknown subcommand: " + options.SubCommand);
    }
}

To process the command options, either access the options on need:

C#
string branchName = options["Branch"];
string jobName = options["Job"];
string parameter = options["Parameter"];
AddParamterToJobs(branchName, jobName, parameter);
Console.WriteLine("Finished.");

Or iterate through and report invalid ones:

C#
foreach (string category in options)
{
    switch (category)
    {
        case "RR":
            ExportMachineList("RR.txt", Queries.GetRRMachines());
            break;
        case "TK5":
            ExportMachineList("TK5.txt", Queries.GetTK5Machines());
            break;
        default:
            Console.WriteLine("Unknown machine category: " + category);
            break;
    }
}

Boolean switch options can be checked like this:

C#
      bool reportOnly = options.HasOption("ReportOnly");
      bool noMail = options.HasOption("NoMail");
      double hours = options.HasOption("Hours") ?
          hours = double.Parse(options["Hours"]) :
          double.Parse(ConfigurationManager.AppSettings["OverdueLimitInHours"]);

Points of Interest

Some examples for specifying command options.

If options are boolean switches, just list the option names.

BuildTrackerClient.exe ClearOverdueMaintenanceJobs Hours=48 NoMail ReportOnly

If options value has space inside, enclose the value with double quotes.

BuildTrackerClient.exe AddParameterToJobs Job="Reverse Integration" Parameter=SourceBranchTimeStamp

If the option is not a name value pair and contains equal sign, precede it with a equal sign.

> CommandLineUtil.exe ShowOptions =D:\1+1=2.txt
D:\1+1=2.txt:

If the option is a name value pair, then the option name cannot contain a equal sign.

History

2014-12-12 First post.

2014-12-15 Added HasOption method.

2015-02-15 Throw exception for invalid options.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Architect YunCheDa Hangzhou
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Suggestioncommand line options parser reply Pin
technoidog19-Dec-14 6:31
technoidog19-Dec-14 6:31 
GeneralMy vote of 4 Pin
Peter Birch16-Dec-14 14:24
Peter Birch16-Dec-14 14:24 
GeneralThoughts PinPopular
PIEBALDconsult12-Dec-14 12:19
mvePIEBALDconsult12-Dec-14 12:19 
AnswerRe: Thoughts Pin
Liu Junfeng14-Dec-14 21:14
Liu Junfeng14-Dec-14 21:14 
I agree SPACE should not be allowed in option names.
For the syntax, as you see, your program is much longer and complex than mine.
And I don't need to predefine options with attributes which is easier to use.
Define options with attributes do have some advantages, it depends on software requirement.
I am happy to work with people doing great projects.

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.