Click here to Skip to main content
15,921,959 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.3K   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 
Interesting. However Junfeng, what does such an approach do to runtime execution dynamics, and performance? Doesn't such code have to have to be compiled, and updated whenever the I/F is modified? But, even if it is interpreted, then have you given any thought as to the Localization adaptation in the long(er) run? I'm just kind of curious as to the original motivation for such 'normalization'. The Bourne (sh), and then Bourne Again SHell (bash) interfaces present the most normalized command line human programming vocabularies that have ever existed, in regard to the Von Neuman generation of processing systems! Why bother?

Save yourself, and others a headache. Should you continue to develop exclusively on Windoze, then get ahold of Cygwin, and begin development with scripting that can be used on other runtime environments (just because you're programming bash) and increase your productivity by an order of magnitude, without doing anything extra! ;^)
Slainte, technoidog
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 

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.