§ January 31, 2008

A C# Command Line Args processing class

The other day I was working on our video game's content server app. The way its set up is fairly complicated. It basically has 3 modes that it can run in (as a windows service, as a fully blown console shell, or as a windows application).

Being tired of writing long ugly blocks of code for command line arg's processing, I decided to build out an object (not at all like my last command line parser class) that would manage the command line args in an intuitive manner.

There are Three basic ways to use this class:
  1. Sort of like a dictionary
  2. With one big event handler
  3. By registering specific event handlers
What I typically do is add the args that are passed into the main function into an arraylist or List<string> and simply ask if the list contains this switch or that switch:
static class Program {
    static void Main(string[] args) {
        List<string> cmdArgs = new List<string>(args);
        if(cmdArgs.Contains("/myswitch"))) {
            // ... whatever ...
        }
    }
}

As you can see, it still takes quite a bit of code. My first iteration of my CommandLineArgs class parsed out the command line switches and placed everything in a dictionary
static class Program {
    static void Main(string[] args) {
        CommandLineArgs cmdArgs = new CommandLineArgs();
        cmdArgs.IgnoreCase = true;
        // this adds a switch prefix regular expression used to determine what is a switch and what is not.
        // whats passed in is a normal Regex pattern.
        cmdArgs.PrefixRegexPatternList.Add("/{1}"); // /switch
        cmdArgs.PrefixRegexPatternList.Add("-{1,2}"); // --switch or -switch
        // once the args are parsed, the class strips off the switch prefix (here either / or - or --).
        cmdArgs.ProcessCommandLineArgs(args);
        if(cmdArgs.ContainsSwitch("myswitch"))) {
            // ... whatever ...
        }
    }
}
But that was pretty much just like using the list like I had been doing.

The next iteration added a SwitchMatch event so that when ProcessCommandLineArgs was called, as switches were parsed, it would kick of an event for each match, providing the switch name (minus the switch prefix) and the value of that switch.
static class Program {
    static void Main(string[] args) {
        CommandLineArgs cmdArgs = new CommandLineArgs();
        cmdArgs.IgnoreCase = true;
        cmdArgs.PrefixRegexPatternList.Add("/{1}");
        cmdArgs.PrefixRegexPatternList.Add("-{1,2}");
        cmdArgs.SwitchMatch += (sender, e) => {
            if(!e.IsValidSwitch) return
            switch(e.Switch) {
                case "foo": {
                    // ... handle the /foo -foo or --foo switch logic here ...
                } break;
            }
        };
        cmdArgs.ProcessCommandLineArgs(args);
    }
}
By the way, I'm using .NET 3.5 here (notice the nice lambda event handler there... yes, I am lazy and lambda's lend themselves nicely to how I do things) if you couldn't tell. If you're using .NET 1.1 or 2.0 then you can just create an event handler like you normally would.

This worked nicely. Only problem now is that I have to have the huge switch statement in my main class. So in the next iteration I decided it would be nice to be able to register specific switch handlers (one for "/foo", one for "--bar" and so on.

static class Program {
    static void Main(string[] args) {
        CommandLineArgs cmdArgs = new CommandLineArgs();
        cmdArgs.IgnoreCase = true;
        cmdArgs.PrefixRegexPatternList.Add("/{1}");
        cmdArgs.PrefixRegexPatternList.Add("-{1,2}");
        cmdArgs.RegisterSpecificSwitchMatchHandler("foo", (sender, e) => {
            // handle the /foo -foo or --foo switch logic here.
            // this method will only be called for the foo switch.
        });
        cmdArgs.ProcessCommandLineArgs(args);
    }
}
One other item I should also note is that there is also an InvalidArgs array too. On the events, there is an IsValidSwitch property that will let you know whether or not the switch is valid or not (since the SwitchMatch event is triggered whether or not the switch is valid) so if your command line contains any values that are not directly tied to a switch. For instance if you don't quote a string with spaces: /foo this is a string like this /foo "this is a string" then /foo's value will be "this" and is, a, string will all be contained in the invalid args list. If you use a switch prefix not accounted for in your prefix regex list, it will also be added to your InvalidArgs list (ex: yourapp.exe ^switch "values" both ^switch and values will be placed in your InvalidArgs list.

This class accepts the following formats:
  • <switch prefix>switch=value
  • <switch prefix>switch value


So in the spirit of sharing, here is my CommandLineArgs class:
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class CommandLineArgs {
    public const string InvalidSwitchIdentifier = "INVALID";
    List<string> prefixRegexPatternList = new List<string>();
    Dictionary<string, string> arguments = new Dictionary<string, string>();
    List<string> invalidArgs = new List<string>();
    Dictionary<string, EventHandler<CommandLineArgsMatchEventArgs>> handlers = new Dictionary<string, EventHandler<CommandLineArgsMatchEventArgs>>();
    bool ignoreCase = true;

    public event EventHandler<CommandLineArgsMatchEventArgs> SwitchMatch;

    public int ArgCount { get { return arguments.Keys.Count; } }

    public List<string> PrefixRegexPatternList {
        get { return prefixRegexPatternList; }
    }

    public bool IgnoreCase {
        get { return ignoreCase; }
        set { ignoreCase = value; }
    }

    public string[] InvalidArgs {
        get { return invalidArgs.ToArray(); }
    }

    public string this[string key] {
        get {
            if (ContainsSwitch(key)) return arguments[key];
            return null ;
        }
    }

    protected virtual void OnSwitchMatch(CommandLineArgsMatchEventArgs e) {
        if (handlers.ContainsKey(e.Switch) && handlers[e.Switch] != null ) handlers[e.Switch](this, e);
        else if (SwitchMatch != null ) SwitchMatch(this, e);
    }

    public void RegisterSpecificSwitchMatchHandler(string switchName, EventHandler<CommandLineArgsMatchEventArgs> handler) {
        if (handlers.ContainsKey(switchName)) handlers[switchName] = handler;
        else handlers.Add(switchName, handler);
    }

    public bool ContainsSwitch(string switchName) {
        foreach (string pattern in prefixRegexPatternList) {
            if (Regex.IsMatch(switchName, pattern, RegexOptions.Compiled)) {
                switchName = Regex.Replace(switchName, pattern, "", RegexOptions.Compiled);
            }
        }
        if (ignoreCase) {
            foreach (string key in arguments.Keys) {
                if (key.ToLower() == switchName.ToLower()) return true;
            }
        } else {
            return arguments.ContainsKey(switchName);
        }
        return false;
    }

    public void ProcessCommandLineArgs(string[] args) {
        for (int i = 0; i < args.Length; i++) {
            string value = ignoreCase ? args[i].ToLower() : args[i];
            foreach (string prefix in prefixRegexPatternList) {
                string pattern = string.Format("^{0}", prefix);
                if (Regex.IsMatch(value, pattern, RegexOptions.Compiled)) {
                    value = Regex.Replace(value, pattern, "", RegexOptions.Compiled);
                    if (value.Contains("=")) { // "<prefix>Param=Value"
                        int idx = value.IndexOf('=');
                        string n = value.Substring(0, idx);
                        string v = value.Substring(idx + 1, value.Length - n.Length - 1);
                        OnSwitchMatch(new CommandLineArgsMatchEventArgs(n, v));
                        arguments.Add(n, v);
                    } else { // "<prefix>Param Value"
                        if (i + 1 < args.Length) {
                            string @switch = value;
                            string val  = args[i + 1];
                            OnSwitchMatch(new CommandLineArgsMatchEventArgs(@switch, val));
                            arguments.Add(value, val);
                            i++;
                        } else {
                            OnSwitchMatch(new CommandLineArgsMatchEventArgs(value, null ));
                            arguments.Add(value, null );
                        }
                    }
                } else { // invalid arg ...
                    OnSwitchMatch(new CommandLineArgsMatchEventArgs(InvalidSwitchIdentifier, value, false));
                    invalidArgs.Add(value);
                }
            }
        }
    }
}

public class CommandLineArgsMatchEventArgs : EventArgs {
    string @switch;
    string value;
    bool isValidSwitch = true;

    public string Switch {
        get { return @switch; } 
    }

    public string Value {
        get { return value; }
    }

    public bool IsValidSwitch {
        get { return isValidSwitch; }
    }

    public CommandLineArgsMatchEventArgs(string @switch, string value)
        : this(@switch, value, true) { }

    public CommandLineArgsMatchEventArgs(string @switch, string value, bool isValidSwitch) {
        this.@switch = @switch;
        this.value = value;
        this.isValidSwitch = isValidSwitch;
    }
}

Posted 17 years, 6 months ago on January 31, 2008

 Comments can be posted in the forums.

© 2003 - 2024 NullFX
Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License