Friday, February 12, 2010

Command Line Parsing

Command Line Parsing

Introduction
From time to time, I am still asked to create a console application for someone, and most of the time PowerShell is still not accepted by my clients for various reasons. Just like method parameters can become unwieldy in an under-designed system as requirements change, so can command line arguments. To minimize the mess, regular expressions can be used to access a command/arguments pattern (e.g. EncryptionUtil.exe /Decrypt /File="c:\My Documents\foo.xml"). Furthermore, if argument are passed in as a key/value pairing (e.g. /File="c:\My Documents\foo.xml"), the order of the arguments should not matter.

Determining the Command
/// 
/// Parses command line arguments to find the "command" 
/// that should be executed.
/// 
/// The command line arguments.
/// 
/// The command that is expected to be executed. 
/// Note: The value has been set to lower case (Invariant). 
/// 
/// 
/// Throws and exception if more than one command is found.
/// 
static string GetArgumentCommand(string[] args)
{
const string regex = "^/([^/][^=]+?)$";
var reg = new Regex(regex);

string result = null;

//Loop through arguments looking for mataches to regex.
foreach (var arg in args)
{
//Argument didn't match, continue to next arg.
if (!reg.IsMatch(arg))
continue; 

//More than one argument matched. Throw an error.
if (!string.IsNullOrEmpty(result))
throw new TooManyCommandsException();

//Argument matched  
var match = reg.Match(arg);
//Check for null/empty
var validMatch = match.Groups.Count >= 2 
&& match.Groups[1].Value != null;
result = validMatch ?
match.Groups[1].Value.ToLowerInvariant() : 
null;
}

return result;
}


Determining the Arguments
/// 
/// Parses command line arguments and creates a dictionary
/// of key/value pair arguments.
/// Note: If a key is used more than once, the first one is used.
/// 
/// The command line arguments.
/// 
/// Dictionary containing arguements.
/// Note: All keys have been set to lower case (Invariant). 
/// 
static Dictionary<string, string> GetArgumentDictionary(string[] args)
{
const string regex = "^/([^/][\\S]+?)\\=([\\S\\s]+?)$";
var reg = new Regex(regex);

var dic = new Dictionary<string, string>();

//Loop through arguments looking for mataches to regex.
foreach (var arg in args)
{
//Argument didn't match, continue to next arg.
if (!reg.IsMatch(arg))
continue;


//Argument matched
var match = reg.Match(arg);

//Determining Key
var validMatch = match.Groups.Count >= 3 
&& match.Groups[1].Value != null;
var key = validMatch ? 
match.Groups[1].Value.ToLowerInvariant() : 
null;
if (string.IsNullOrEmpty(key))
continue;

//Check for duplicate key.
if (dic.ContainsKey(key))
continue;

//Determine value
validMatch = match.Groups.Count >= 3;
var val = validMatch ? match.Groups[2].Value : null;

//Add key/value pair to dictionary.
dic.Add(key, val);
}

return dic;
}

Putting it Together
/// 
/// Application entry point.
/// 
/// The command line arguments.
static void Main(string[] args)
{
try
{
//Get the command and dictionary of arguments.
var cmd = GetArgumentCommand(args);
var dic = GetArgumentDictionary(args);

//Execute appropriate command.
switch (cmd) //remember commands are lower case
{
case "foo":
ExecuteFooCommand(dic);
break;
case "bar":
ExecuteBarCommand(dic);
break;
case "?": //redundant, but makes the point.
default:
DisplayHelpText(dic);
break;
}
}
catch (Exception ex)
{
Console.WriteLine("An exception occured.");
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
}


/// 
/// Executes the foo command.
/// 
/// Dictionary containing key/value arguements.
static void ExecuteFooCommand(Dictionary<string, string> dic)
{
//Add appropriate code here.
}

/// 
/// Executes the bar command.
/// 
/// Dictionary containing key/value arguements.
static void ExecuteBarCommand(Dictionary<string, string> dic)
{
//Add appropriate code here.
}

/// 
/// Displays help text to the console.
/// 
/// Dictionary containing key/value arguements.
static void DisplayHelpText(Dictionary<string, string> dic)
{
//Add appropriate code here.
}

1 comment:

  1. I noticed today that there is some inconsistency in my error conditions. When there is more than one command, an exception is thrown. However, when a duplicate key exists in the key/value pairs, the first one is accepted and the second one is ignored. I believe this can lead to confusion, and have altered the duplicate key scenario to throw an exception as well.

    ReplyDelete