Introduction
Nowadays, we see different syntax for specifying command line options, here are some examples:
-
msbuild myproject.csproj /verbosity:diag /fileLogger
-
padrino g project badacm -d mongoid -e slim -c sass
-
git log --pretty=format: --name-only --diff-filter=A
-
gem install nokogiri -- --use-system-libraries --with-xml2-config=/path/to/xml2-config
-
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.