Software Is Hardwork

ISimplicityAffinative: The endless pursuit of anti-complexity.
The technology-centric blog of D. P. Bullington.

Email D. P. Bullington View D. P. Bullington\ Follow D. P. Bullington on Twitter Get Software Is Hardwork code on CodePlex

Blog Post(s)

Command Line Argument Validation with C# 3.0 Voodoo
Thursday, August 14, 2008

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
Example valid command lines: program.exe -force:true -food:pizza program.exe -force:true -food:pizza -food:cake program.exe -force:false -mode:complex -food:steak program.exe -force:true -food:pizza -food:cake -file"c:\documents\junk.txt" Example invalid command lines: program.exe -food:pizza program.exe -force:true -food:pizza -mode:simple -mode:complex program.exe -force:false -mode:dumb -food:steak program.exe -force:true -food:pizza -file"c:\documents\junk.txt" -unknown:value Now that we have a basic model for defining the static structure of command line arguments which are valid to the application, let’s look at how we can leverage C# anonymous classes, lambda expressions, and some additional voodoo to make the work of validation simple, generic, and painless. We first need to parse the command line arguments for those which match our chosen format. We can use a regular expression to easily accomplish this:

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:

Jeff Brown said...

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;

Chuni-Muni said...

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);

}
}

Speaking Enagements

  • 11/18/2010 | Charlottesville .NET Users Group | Charlottesville, VA | Topic TBD
  • 09/14/2010 | Hampton Roads .NET Users Group | Cheaspeake, VA | Topic TBD
  • 07/01/2010 | Richmond .NET Users Group | Richmond, VA | Topic TBD
  • (past) 12/08/2009 | Hampton Roads .NET Users Group | Cheaspeake, VA | SharePoint Antithesis - A Case Study in Pragmatic Software Architecture and Engineering Processes
  • (past) 10/04/2009 | Richmond Code Camp 2009.2 | Richmond, VA | Soothing the Pain Points: Data Access, Validation, Rules, UI, Presentation, et. al
  • (past) 07/23/2009 | Charlottesville .NET Users Group | Charlottesville, VA | Debugging on the Windows Platform
  • (past) 05/23/2008 | NoVa CodeCamp 2009.01 | Reston, VA | Going Proxy-less - The WCF Proxy Factory
  • (past) 04/25/2009 | Richmond Code Camp 2009.1 | Richmond, VA | Software Programmer to Software Engineer: Concepts to Span the Divide
  • (past) 02/05/2009 | Richmond .NET Users Group | Richmond, VA | Debugging on the Windows Platform
  • (past) 10/04/2008 | Richmond Code Camp 2008.2 | Richmond, VA | Going Proxy-less - The WCF Proxy Factory

Blog Archive

Post Labels

.NET (64) .NETv4.0 (3) ACID (1) ActiveDirectory (1) ADF (2) Affiliate (1) Agile (6) AJAX (1) Allocator (3) Analysis (1) AOP (4) ASP.NET (6) ASP.NET MVC (1) Assembly (2) BadIdeaPile (1) BagOfBolts (5) Blogger (1) Books (2) BuildMgmt (8) C# (46) ChoDNUG (1) CLR (1) CLRv4.0 (2) CMP (1) CMS (2) CodeCamp (2) COM (1) Conversation (1) Coverage (1) CUI (1) Database (2) DDD (1) DeadFxs (1) Debugging (9) Design (4) DevAuto (3) DevCfg (1) Development (118) DI (6) DiffMerge (1) Domain (1) DTfW (2) EclipseIDE (1) ECM (1) EntityFramework (1) Estimating (1) FileShare (1) Frameworks (7) GAC (2) Google (1) Hardware (2) HRNUG (1) Humor (6) IIS (4) ILDASM (1) Impersonation (2) InstallError (1) IoC (6) KingTodd (1) LinkedIn (1) LINQtoSQL (2) MarketingHype (1) MBUnit (1) Mentoring (22) Metadata (1) Microsoft (7) MOSS2007 (5) MSBuild (2) MSIL (4) MSSCCI (2) NAnt (2) NCore (2) NCover (1) NDatabase (4) NetUse (1) NHibernate (2) NoVaCodeCamp (2) NTSD/CDB (2) NUnit (1) Observation (2) Office (2) OOD (7) OOP (6) OpenSource (14) Opinion (19) Personal (3) PMP (1) Polymorphism (1) PowerPoint (1) PowerShell (2) Presentation (3) Process (4) ProjectManagement (2) PublicKeyToken (1) QA (2) RDNUG (1) Reflection (2) Registry (2) Resharper (1) Reversing (2) RichmondCodeCamp (5) SCM (11) Scrum (5) Security (2) Series (3) Server2008 (4) ServicePack (1) SES (7) SharePoint (7) Silverlight (1) SoC (3) Software (49) SoftwareIsHardwork (17) Speaking (7) SQL (2) SSO (2) StrongName (2) Suite2008 (9) Suite2010 (1) SwEng (19) TechBlunder (1) Testing (14) Thread (3) Tools (8) Troubleshooting (10) Twitter (3) Types (2) UAC (1) UIP (1) Vault (2) VB6 (1) VC (1) Vista (3) VisualStudio (15) VSIP (2) VSTS (1) WCF (4) Web (4) WebForms (1) Win32 (3) WinDBG (3) WindowsIdentity (3) WinForms (1) WIT (1) Workhorse (1) WoW64 (1) WPF (1) WSS3 (2) x64 (2) x86 (2) xUnit (1)

Disclaimer

© D. P. Bullington, all rights reserved. Everything posted on this blog is my personal opinion and does not represent the views of my employer nor serves to infringe on the sovereignty of any nation.