A simple task such as validating program arguments generally would not be an interesting topic for a blog post, but I have found a cool use of anonymous types in C# to perform this magic. When it comes to command line arguments, the format can vary as there is no real standard (e.g. value, -flag, /flag, -key:value, etc.) I personally like the -key:value format, as it seems to support the most common command line switch scenarios (boolean and dictionaries of named values, including delimited lists). This post will focus exclusively on using this flexible style of command line arguments. After a quick and dirty analysis of all the ways I have used command line arguments, I have discovered that you can essentially boil down the validation into two buckets: static and dynamic. Static command line argument validation is essentially verifying, based on a "schema", without looking at actual values of the command line arguments (except for reoccurrence) or state of the application. Dynamic command line validation requires the use of rules which may depend on values of other command line parameters or the state of the program. We will focus on static command line validation for this exercise as dynamic command line argument validation is inherently application-specific. Before moving forward, let us define and elaborate on the "schema", referred to in the previous section. It consists of the following elements with respect to our format of choice: (-property:value): the set of all keys (options) allowed to be specified on the command line; the set of all valid discrete values for a given key -or- an open set; an indicator of whether a given key is required to be present on the command line at least once; and an indicator of whether a key may reoccur on the command line (if the value set is open, then reoccurrences must be unique; if non-open, no repetition of a discrete value may occur). Example "schema":
| KEY NAME | VALUE SET | IS REQUIRED | CAN REOCCUR |
| force | {true | false} | true | false |
| mode | { simple | complex | mediocre} | false | false |
| file | ? | false | true |
| food | {pizza | steak | cake} | true | true |
public static IDictionary<string, IList<string>> ParseArgs(string[] args)
{
const string ARGS_REGEX = @"-([a-zA-Z_][a-zA-Z_0-9]{0,}):(.{0,})";
IDictionary<string, IList<string>> parsedArgs;
string name, value;
Match match;
parsedArgs = new Dictionary<string, IList<string>>();
if (args != null)
{
foreach (string arg in args)
{
match = Regex.Match(arg, ARGS_REGEX, RegexOptions.IgnorePatternWhitespace);
if (match == null)
continue;
if (!match.Success)
continue;
if (match.Groups.Count != 3)
continue;
name = match.Groups[1].Value;
value = match.Groups[2].Value;
if (DataType.IsTrimNullOrZeroLength(name))
continue;
//if (DataType.IsTrimNullOrZeroLength(value))
// continue;
name = name.ToLower();
if (!parsedArgs.ContainsKey(name))
parsedArgs.Add(name, new List<string>());
if (!parsedArgs[name].Contains(value))
parsedArgs[name].Add(value);
}
}
return parsedArgs;
}
Not only does this code pick out command line argument which match the format (-property:value), it also places the result into an IDictionary<string,>> object. This allows our code to thumb through the arguments by key, obtaining a list of all values for that key. If a key is defined as allowing reoccurrences then there may be one or more value. Non-reoccurring keys have only one value in the list (at index zero of course). A key with zero values is an error condition, as (-property) usages are not allowed; empty value usages (-property:) are, however, valid. Any command line arguments not conforming to the regular expression and sanity checks are ignored.
Once we have the IDictionary<string,>> object containing the command line arguments as entered by the user, we can then perform the static validation. We need to codify the "schema", illustrated in the table above:
var paramTable = new[]
{
new
{
Key = "force",
Required = true,
Values = new[] { "true", "false" },
Reoccurs = false
},
new
{
Key = "mode",
Required = false,
Values = new[] { "simple", "compplex", "mediocre" },
Reoccurs = false
},
new
{
Key = "file",
Required = false,
Values = (string[])null, // open
Reoccurs = true
},
new
{
Key = "food",
Required = true,
Values = new[] { "pizza", "steak", "cake" },
Reoccurs = true
}
};
The previous code constructs a table using anonymous types, which will be used to validate the command line arguments. As you can see, there is no mechanism to represent complex validation, thus it is static in nature. The next step is to actually perform the static validation:
try
{
// generic code
foreach (var paramRow in paramTable)
{
var arg = commandLineArgs.ContainsKey(paramRow.Key) ? commandLineArgs[paramRow.Key] : null;
if (arg == null)
{
if (paramRow.Required)
throw new InvalidCommandLineArgumentException(string.Format("Missing required argument key: {0}.", paramRow.Key));
else
continue;
}
if (arg.Count == 0)
throw new InvalidCommandLineArgumentException(string.Format("Missing argument value for key: {0}.", paramRow.Key));
if (arg.Count > 1 && !paramRow.Reoccurs)
throw new InvalidCommandLineArgumentException(string.Format("Argument key cannot reoccur: {0}.", paramRow.Key));
foreach (string value in arg)
{
if (paramRow.Values != null &&
paramRow.Values.Length != 0 &&
Array.Find(paramRow.Values, match => match == value) == null)
throw new InvalidCommandLineArgumentException(string.Format("Argument value for key {0} is invalid: {1}.", paramRow.Key, value));
}
}
// generic code
foreach (KeyValuePair<string, IList<string>> keyValuePair in commandLineArgs)
{
var paramRow = Array.Find(paramTable, match => match.Key == keyValuePair.Key);
if (paramRow == null)
throw new InvalidCommandLineArgumentException(string.Format("Argument key unknown: {0}.", keyValuePair.Key));
}
}
catch (InvalidCommandLineArgumentException iclae)
{
// generic code
string usage = "USAGE:" + Environment.NewLine;
foreach (var paramRow in paramTable)
usage += string.Format("{0}-{1}:{{{2}}}{3}{4}" + Environment.NewLine, paramRow.Required ? "" : "[", paramRow.Key, paramRow.Values != null ? string.Join(" | ", new List<string>(paramRow.Values).ToArray()) : "?", paramRow.Reoccurs ? "*" : "", paramRow.Required ? "" : "]");
Console.WriteLine(iclae.Message);
Console.WriteLine();
Console.WriteLine(usage);
}
We can also print the command line arguments using the following code:
public static void PrintArgs(IDictionary<string, IList<string>> args)
{
if (args != null)
{
foreach (KeyValuePair<string, IList<string>> arg in args)
{
Console.Write("{0} {{", arg.Key);
if (arg.Value != null)
Console.Write(string.Join(" | ", new List<string>(arg.Value).ToArray()));
Console.WriteLine("}");
}
}
}
Putting it all together, you are halfway there to a complete command line argument validation solution, minus of course, dynamic validation which makes since in your application.
2 comments:
Pretty neat.
You might also be interested in an alternate approach based on attribute metadata attached to typed properties / fields that describe arguments.
eg.
[CommandLineArgument("documents", Description="Specifies the documents to process.")]
public string[] Documents;
The NConsoler open source library that provides command line parser functionality based on attribute metadata attached to type.
Library is very easy to add and use in your application. NConsoler gives ability to display help validation messages without any line of code.
http://nconsoler.csharpus.com/
Example code:
using System;
using NConsoler;
public class Program {
public static void Main(params string[] args) {
Consolery.Run(typeof(Program), args);
}
[Action]
public static void Method(
[Required] string name,
[Optional(true)] bool flag) {
Console.WriteLine("name: {0}, flag: {1}", name, flag);
}
}
Post a Comment