65.9K
CodeProject is changing. Read more.
Home

An elegant command line options parser

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (18 votes)

Dec 12, 2014

MIT

1 min read

viewsIcon

32564

downloadIcon

479

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.

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:

        
        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:

        
        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:

            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:

        
            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:

        
      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.