Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An elegant command line options parser

4.58/5 (19 votes)
14 Feb 2015MIT1 min read 32.5K   475  
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