Life: The Soundtrack

Tuesday, February 15, 2005

Work - Command Line Configuration

Track/Stream: 3 Doors Down - Kryptonite

Still don't have much to do at work, so I finished a little project I started yesterday. Through a few of my projects now I've had to deal with command line stuff. In general it's a pain in the ass to support a bunch of dumb options. These things also tend to be formulaic, so I decided to refactor all of my command line handling into a few quick classes and put a bunch of junk into config files.

So the end result is a pretty lithe class (around 150 lines) that does the basic stuff, but can be easily extended to do whatever you want. None of it is commented, but most of it should be pretty obvious.

using System;
 
namespace System.Configuration
{
  public sealed class CommandLineSectionHandler : System.Configuration.IConfigurationSectionHandler, System.Collections.IEnumerable
  {
    private CommandLineParameter[] _Parameters = new CommandLineParameter[0];
 
    public void LoadCommandLine(string[] args)
    {
      for (int i = 0; i < args.Length; i++)
        for (int j = 0; j < _Parameters.Length; j++)
        {
          bool handled;
          bool expectMore;
          object state = null;
 
          _Parameters[j].LoadCommandLineSegment(args[i], ref state, out expectMore, out handled);
 
          if (handled)
          {
            while (expectMore)
            {
              _Parameters[j].LoadCommandLineSegment(args[++i], ref state, out expectMore, out handled);
              if (!handled)
                throw new CommandLineException("Invalid Command Line Parameter", _Parameters[j].Name);
            }
            break;
          }
        }
 
      foreach (CommandLineParameter param in _Parameters)
        if ((param.Required) && (!param.Satisfied))
          throw new CommandLineException("Missing Required Command Line Parameter", param.Name);
    }
 
    public string Help
    {
      get
      {
        System.Text.StringBuilder Result = new System.Text.StringBuilder();
        foreach (CommandLineParameter param in _Parameters)
        {
          string nextChunk = param.Help;
          if (nextChunk != null)
            Result.AppendFormat("{0}\n", nextChunk);
        }
        return Result.ToString();
      }
    }
 
    public CommandLineParameter this[string Name]
    {
      get
      {
        foreach (CommandLineParameter parameter in _Parameters)
          if (parameter.Name == Name)
            return parameter;
 
        return null;
      }
    }
 
    public System.Collections.IEnumerator GetEnumerator()
    {
      return _Parameters.GetEnumerator();
    }
 
    public object Create(object Parent, object ConfigContext, System.Xml.XmlNode Section) 
    { 
      int i = 0;
 
      _Parameters = new CommandLineParameter[Section.SelectNodes("param").Count];
 
      foreach (System.Xml.XmlElement parameter in Section.SelectNodes("param"))
      {
        if (parameter.GetAttributeNode("type") == null)
          throw new ConfigurationException("'type' attribute not specified", parameter);
        if (parameter.GetAttributeNode("name") == null)
          throw new ConfigurationException("'name' attribute not specified", parameter);
 
        string typeName = parameter.GetAttribute("type");
 
        Type paramType = Type.GetType(typeName);
 
        if (paramType == null)
          paramType = System.Reflection.Assembly.GetEntryAssembly().GetType(typeName, false, true);
 
        if (paramType == null)
          paramType = System.Reflection.Assembly.GetCallingAssembly().GetType(typeName, false, true);
 
        if (paramType == null)
          paramType = System.Reflection.Assembly.GetExecutingAssembly().GetType(typeName, false, true);
 
        if (paramType == null)
          paramType = System.Reflection.Assembly.GetExecutingAssembly().GetType(this.GetType().FullName + "+" + typeName, false, true);
 
        if (paramType == null)
          paramType = Type.GetType(typeName, false, true);
 
        if (paramType == null)
          throw new ConfigurationException("Parameter type not recognized", parameter);
 
        _Parameters[i++] = (CommandLineParameter)((CommandLineParameter)paramType.GetConstructor(System.Type.EmptyTypes).Invoke(null)).Create(this, null, parameter);
      }
 
      return this;
    }
 
    public class CommandLineException : Exception
    {
      private string _Parameter;
      public  string  Parameter { get { return _Parameter; } }
 
      public CommandLineException(string Message, string Parameter, Exception exx) : base(Message, exx) { _Parameter = Parameter; }
      public CommandLineException(string Message, string Parameter) : this(Message, Parameter, null) { }
    }
 
    public abstract class CommandLineParameter : System.Configuration.IConfigurationSectionHandler
    {
      protected string _Name;
      public    string  Name { get { return _Name; } }
 
      protected bool   _Required;
      public    bool    Required { get { return _Required; } }
 
      protected string _Description = null;
 
      public object Create(object Parent, object ConfigContext, System.Xml.XmlNode Section) 
      {
        _Name     = ((System.Xml.XmlElement)Section).GetAttribute("name");
        if (((System.Xml.XmlElement)Section).GetAttributeNode("required") != null)
          _Required = bool.Parse(((System.Xml.XmlElement)Section).GetAttribute("required"));
 
        System.Xml.XmlNode description = Section.SelectSingleNode("description");
        if (description != null)
          _Description = description.InnerText;
 
        this.Configure(Section);
        return this;
      }
 
      public virtual string Help { get { return _Description; } }
 
      protected abstract void Configure(System.Xml.XmlNode Section);
 
      public abstract void LoadCommandLineSegment(string Segment, ref object State, out bool ExpectMore, out bool Handled);
 
      public abstract bool Satisfied { get; }
    }
  }
}


Since this is just a framework, we need a couple examples of how to extend this. Plus it'd be nice to have a configuration example. To extend this, you'd need to start by implementing a CommandLineParameter class. That's actually pretty easy. You need to implement two functions and a property.

The property, Satisfied, is actually pretty easy. You return whether or not you consider yourself properly initialized. This is used after the command line has been parsed. Any required parameter that does not report itself as satisfied will throw an error.

The Configure() method is called when configuring and is passed the param element from the config file (more on that later). These nodes are plain XML and can be processed as you see fit.

The LoadCommandLineSegment() method does the bulk of your work. You will be passed each segment of the command line (in the Segment variable) which was not handled by a prior parameter. If this parameter is handled by your handler, you should set Handled to true so that no subsequent handlers will be called. If you expect more data to come immediately after this, set ExpectMore to true as well. You will have exclusive access to subsequent data until you set ExpectMore to false. Finally, should you need to keep track of any state, it is suggested that you do so using the State parameter. This will be passed back to you in an unaltered form and can be any object of your choice.

That's all there is to it. So here are a couple examples. I start with another abstract class to make my life easier. It just handles configuration of a key in the form /key. If a value follows, it is up to subsequent implementations to include it.

    public abstract class KeyedCommandLineParameter : CommandLineParameter
    {
      protected string _KeyName = null;
 
      protected override void Configure(System.Xml.XmlNode Section)
      {
        System.Xml.XmlElement parameter = (System.Xml.XmlElement)Section;
 
        if (parameter.GetAttributeNode("key") == null)
          throw new ConfigurationException("'key' attribute not specified", parameter);
 
        _KeyName = parameter.GetAttribute("key");
        if (!_KeyName.StartsWith("/"))
          _KeyName = "/" + _KeyName;
      }
 
      public override string Help
      {
        get
        {
          if (_Description != null)
            return string.Format("{0,-20} {1}", _KeyName, _Description);
          else
            return null;
        }
      }
    }
 


Pretty simple. Here are a pair of implementations of parameters. One covers boolean flags. The flag defaults to false unless the value is present. The other covers integer flags. They can have a default value (0 if none is specified) and the value is specified as a second parameter (i.e. /foo 15).

    public sealed class IntegerCommandLineParameter : KeyedCommandLineParameter
    {
      private int  _Value = 0;
      public  int   Value { get { return _Value; } }
 
      private bool _FoundValue = false;
 
      protected override void Configure(System.Xml.XmlNode Section)
      {
        base.Configure(Section);
 
        System.Xml.XmlElement parameter = (System.Xml.XmlElement)Section;
 
        try
        {
          if (parameter.GetAttributeNode("default") != null)
            _Value = int.Parse(parameter.GetAttribute("default"));
        }
        catch (Exception exx)
        {
          throw new ConfigurationException("'default' attribute not properly formatted", exx, Section);
        }
      }
 
      public override void LoadCommandLineSegment(string Segment, ref object State, out bool ExpectMore, out bool Handled)
      {
        ExpectMore = false;
 
        if (State == null)
        {
          if (string.Compare(_KeyName, Segment, true) == 0)
          {
            Handled = true;
            ExpectMore = true;
            State = new object();
          }
          else
            Handled = false;
        }
        else
        {
          try
          {
            _Value = int.Parse(Segment);
            _FoundValue = true;
            Handled = true;
          }
          catch (Exception exx)
          {
            throw new CommandLineException("Invalid Parameter Value", _Name, exx);
          }
        }
      }
 
      public override bool Satisfied
      {
        get
        {
          return _FoundValue;
        }
      }
 
      public override string ToString()
      {
        return String.Format("{0} - {1} ({2})", _Name, _Value, _KeyName);
      }
    }
 
    public sealed class BooleanCommandLineParameter : KeyedCommandLineParameter
    {
      private bool _Value = false;
      public  bool  Value { get { return _Value; } }
 
      public override void LoadCommandLineSegment(string Segment, ref object State, out bool ExpectMore, out bool Handled)
      {
        if (string.Compare(_KeyName, Segment, true) == 0)
        {
          Handled = true;
          _Value = true;
        }
        else
          Handled = false;
 
        ExpectMore = false;
      }
 
      public override string ToString()
      {
        return String.Format("{0} - {1} ({2})", _Name, _Value, _KeyName);
      }
 
      public override bool Satisfied { get { return true; } }
    }


Here is a sample config. This is pretty standard config framework. On each param node, you are expected to have a type and name attribute. You may also have a description child element (used when displaying Help). Anything else depends on your parameter implementations.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="system.configuration.commandline" type="System.Configuration.CommandLineSectionHandler, CommandLineParams"/> 
  </configSections>
  
  <system.configuration.commandline>
    <param type="BooleanCommandLineParameter" name="booltest"  key="foo">
    <param type="BooleanCommandLineParameter" name="booltest2" key="foob">
    <param type="IntegerCommandLineParameter" name="inttest"   key="bah">
    <param type="IntegerCommandLineParameter" name="inttest2"  key="bahb">
    <param type="IntegerCommandLineParameter" name="inttest3"  key="bahc" default="5">
      <description>This is a parameter</description>
    </param>
  </system.configuration.commandline>
</configuration>


Finally, it's nice to know how to do stuff with it. Try something like this:

      System.Configuration.CommandLineSectionHandler StartupConfig = (System.Configuration.CommandLineSectionHandler)System.Configuration.ConfigurationSettings.GetConfig("system.configuration.commandline");
 
      StartupConfig.LoadCommandLine(args);
 
      System.Console.WriteLine(StartupConfig.Help);
 
      foreach(System.Configuration.CommandLineSectionHandler.CommandLineParameter param in StartupConfig)
        System.Console.WriteLine(param.ToString());
 
      System.Console.WriteLine("inttest3: {0}\n", ((System.Configuration.CommandLineSectionHandler.IntegerCommandLineParameter)StartupConfig["inttest3"]).Value);


This loads up the config, passes in the command line arguments and processes the whole deal. Then it shows the help text followed by a list of each parameter and the associate value, then shows you the value of a parameter called "inttest3" (I used the config file listed above).

0 Comments:

Post a Comment