At some point, you'll have to write an application accepting command line arguments. Typically a console application, but GUI apps can accept command lines too. Its features and options will grow and you will become frustrated with .Net's default abilities. You'll desire a robust parsing ability, and you'll wish to configure it easily, automatically and in an extensible way. Welcome to today's topic
Big whup, eh? But let's see how simple it was to create: just decorate a class' properties with attributes to describe the mapping.
Join me on a magic carpet ride into simplicity as I further describe some of the interesting highlights of how to use it, how it works. and how you can steal the code, make it your own and add the quirky bits unique to your situation. The full Visual Studio 2005 project code is available.
Admit it, you can tell how its supposed to work just from reading the property declaration, right? Features worth mentioning listed below. (or let me draw you a picture [in a new window], if you prefer)
- Both fields and properties are supported. As are booleans (Switch), enums and nullable types
- Re-usable validation (e.g. ValidDirectory, IntArgument), I'll show how easy it is to plug in your attributes for custom validation
- Ad-hoc validation in property setters (e.g. Age), or ad-hoc re-interpretation of arguments (e.g. Email)
- Required parameter validation
- Use Position to pass parameters with out specifying the token (e.g. "CommandLineParsing.exe James"), or use tokens to pass parameters in any order
- Use HelpText for auto-generating parameter help files (e.g. "-?")
- Decorate properties of any class. Imagine your own hierarchy of parameter classes which could be re-used across many programs
Note: The code's a tad too verbose to dump full contents into this article. I'll omit the boring shit and provide full code for download. The full code is better commented, but within this article I'll reduce or omit inline comments for brevity and discuss in paragraph form.
Y'all Know What Time It Is?
The .Net libraries provide only limited parsing of arguments. It turns the command line into an array of strings, and in my experience parsing command lines is done differently in every situation, copy + pasted around and done half-assed except when required. I first had the idea of using attributes to describe the desired parsing a few years back, started coding a super-generic solution, then stopped. My current job has around 40+ batch exes, and when discussion recently turned to how we could improve the horrid inconsistent parsing situation, I revived my old idea, it took over a chunk of my free time, and ended here.
I've decided that a one-sized-fits-all parse is impossible. There are a frazillion variations on command line parsing, conflicting conventions, and perhaps you've already got a situation that this won't handle. For instance, we use "Exe name:value name:value" at my current job, cmd's attrib command accepts things like "attrib -r +hs /s", and in fact, I hate how .net parses strings into it's array and BAH HUMBUG! Forget it!! No fucking one-size-fits-all!!! I'll show you later where to swap out the parsing layer into something of your own choosing, but keep the binding and attributes layer, which is the interesting part anyways.
Invocation is simple
If you're only interested in using the code I've already written, this is to show you there's nothing to fear. You've already seen an example of an interesting Arguments class. Let's see how simple the my sample program is. You are not required to use this pattern, it's just how I chose to use the Arguments class.
Next let's see the boilerplate to invoke it that program. The highlights are the ArgumentBindings and the ArgumentParser. The rest is mere error trapping, plumbing and niceties. Notice there's a binding phase that's seperate from the parsing phase.
- Binding uses reflection to analyze your custom Arguments object and it's properties;
- errors are the fault of the developer (e.g. decorating a non-boolean field with a a SwitchArgument).
- Parsing interprets the command line parameters and attempts to assign values to properties of the Arguments object;
- errors are the fault of the user (e.g. omitting a required field)
So that's it, that's how to use this unholy beast. Why are you showing me this boring junk you scream? To stress the simplicity of usage. Spend your time marking up properties instead. After all, command line parsing should be simple.
Arguing with Attributes
I won't be dicussing the HelpText, Required and Position attributes, which contain no logic. Let's jump instead right into the CommandLineArgument. It is meant to decorate a string field (or property). It is also the base-class from which all other attributes descend.
What the heck is a MemberInfoAdapter? (seen referenced in SetValue)
- It's an uninteresting class allowing us to do things like SetValue, or obtain the Type.
- Works for both FieldInfo or PropertyInfo object.
- The two classes both support such operations, but do not share enough ancestory, or a common interface.
- The adapter simplifies the Argument classes, allowing them to easily support both fields and properties.
As discussed in the Invocation section, there is a serpate binding phase. The ValidateBind method provides a way for argument classes to validate they've been applied to a field of the correct type, or any other validation you can think of, based on the reflection information. The CommandLineArgument, displayed above, provides a good example of a ValidateBind implementation.
Let's examine the EnumArgument's ValidateBind for a bit of contrast.
- It's meant to be applied to any enumerated type, used to restrict values accepted from user input.
- SetValue translates a string to the proper enumerated value
- GetHelpTextValue, returns a descriptive string, displayed during "my.exe -?" type operations
If all properties successfully bind, we move onto the parsing phase. The SetValue method will be called with details the user entered on the command line. This is where value validation or re-interpretation can occur. For example, you might ensure a numeric UserId actually exists in a database before assigning it the property. Taking it further, you might accept a UserId but assign a UserName to the object's property.
Here's a descendant property that ensures the user specifies a valid directory:
Finally, the ExpectsValue is meant primarily for the SwitchArgument, or switch-like arguments. It allows "my.exe -recursive" instead of a clunkier "my.exe -recursive true". It allows helps the parser determine wether "my.exe -nice -6", means "token nice has a value of -6" or "token nice exists; token 6 exists" (or "token nice exists; unamed value of -6 exists, use positional logic). Likely the SwitchArgument's bool support is all you'll ever need, but perhaps you can imagine arguments that do something else within SetValue?
Final Thoughts & Download
That's all that's required to use it. If you curious about the the technical workings of Binding & Parsing, read through the appendix. If you have customized parsing requirements or are integrating into an existing framework you'll want to take a look for sure. This code is very much a receipe, feel free to download, tweak and customize it to your heart's content. I've also listed included a Future Enhancements appendix, with some ideas of how to build on the command line parsing that I've alredy written.
I've tried to be brief, and there are much more comments in the included code. Feel free to use it, modify it, etc. Just include a link back to this webpage in the source, and maybe drop me a comment to let me know you found this useful. There's a lot here, and we've found it useful base at my current work. Its unlikely you have a command line project now, but one day you will. Next time you're faced with a command line, remember Feelings of White, come back and grab the code that will save you mountains of time.
Appendix A: Future Enhancements
- As mentioned in Appendix B, the ArgumentParser is most likely the bit you'll need to improve/replace, to fit your unique situation. It might be that some of the simpler choices I've made won't fit your need.
- You might need to allow Argument Attributes to participate in the parsing process themselves. While this makes a tighter coupling of parse & attributes, it also allows things like both "-r42" and "-r 42", allowing the attribute to determine dynamically whether it expects a value. The waters have already been muddied by the ExpectsValue and GetHelpTextValue
- You might have a radically different parse algorithm (e.g. "name:value" format), values mixed with attributes (e.g. "-r42"), multiple switches being set within a single string (e.g. "+rh -s"), or any other things you can think of.
- There are many generic CommandLineArgument descendants that could be written, left as an exercise for the reader. Some ideas: Date Parsing, Float parsing, allowing comma,seperate,lists to be written to a List<string> property as well as your own domain specific attributes (the CustomerID attribute, etc)
- A better automated help text generation system could be written. I included it both for simple convenience, but also as an example of what else you could do with the Binding metadata gathered.
Appendix B: Binding & Parsing
You may be curious about what translates the argument attributes into useful calls to the custom arguments class. Let's take a look at the binding and parsing phase in more depth. I've removed quite a bit of extra code from the ArgumentBindings excerpt below, to better focus on the BindMember method (you'll have to scroll down a bit). In essence, it's doing a few System.Reflection calls to determine the attributes decorated from a _argumentObject, our custom parameter object, and parsing them into a Dictionary to make the information a little easier to deal with.
As I mentioned, there are many more methods omitted from this article which appear in the full code. Chiefly I've skipped the WriteHelpText method mentioned during the invocation boilerplate (it loops through the _argumentHash and displays the strings captured by HelpText). Also skipped is the implementation details of the IArgumentAssignment:
The IArgumentAssignment is the contract by which the Parser interacts with the Binding information. Calls to GetArgumentType use the hash and CommandLineArgument's ExpectValue to determine the result, the next two are passthroughs to the SetValue, while FinishedParsing just ensures that all Required fields are specified.
I don't feel this article could be complete without including the parsing code that shows the interaction between command line and the bindings. But at the same time it's merely Yet Another String Parsing Problem. I'll leave discussion on it's future potential to another day's session. Feel free to skip the next code block, unless the nitty gritty of the argument parse are your cup of tea. It's also the most likely candidate for your customization and/or replacement by you.