Wednesday, March 27, 2013

A declarative argument parser for F#

When it comes to command line argument parsing in the F# projects I work on, my library of choice so far has been the argument parser available with the F# powerpack. While ArgParser is a simple implementation that works well, I always felt that it didn't offer as declarative an experience as I would like it to have: extracting the parsed results can only be done through the use of side-effects.

It becomes even uglier when you need to combine this with configuration provided from App.Config, resulting in arduous code that (in most cases) adheres to the following pattern:

  1. Parse configuration file for parameter "foo".
  2. Parse command line arguments for parameter "foo".
  3. Configuration file is overriden if declared otherwise in command line.
I felt that this configuration parsing scheme is a pattern that can and should be handled transparently. After a bit of experimentation, I ended up with a library of my own, which you can find uploaded in github. What follows is an informal walkthrough of what it does.

The Basic Idea

The library is based on the simple observation that configuration parameters can be naturally described using discriminated unions. For instance:

UnionArgParser takes such discriminated unions and generates a corresponding argument parsing scheme. For example, the parser generated from the above template recognizes the syntax

--working-directory /var/run --listener localhost 8080 --detach

yielding outputs of type Argument list. The syntax is infered from the union type without the need to specify any additional metadata.

The parser will also look for the following keys in the AppSettings section of the application config file: As mentioned previously, command line arguments will override their corresponding configuration file entries. This default behaviour can be changed, however.

Usage

A minimal example using the above union can be written as follows:

While getting a single list of all parsed results might be useful for some cases, it is more likely that you need to query the results for specific parameters: Querying using quotations enables a simple and type safe way to deconstruct parse results into their constituent values.

Customization

The parsing behaviour of the configuration parameters can be customized by fixing attributes to the union cases: In this case,

  • Mandatory: parser will fail if no configuration for this parameter is given.
  • NoCommandLine: restricts this parameter to the AppSettings section.
  • AltCommandLine: specifies an alternative command line switch.

The following attributes are also available:

  • NoAppConfig: restricts to command line.
  • Rest: all remaining command line args are consumed by this parameter.
  • Hidden: do not display in the help text.
  • GatherAllSources: command line does not override AppSettings.
  • ParseCSV: AppSettings entries are given as comma separated values.
  • CustomAppSettings: sets a custom key name for AppSettings.

Post Processing

It should be noted here that arbitrary unions are not supported by the parser. Union cases can only contain fields of certain primitive types, that is int, bool, string and float. This means of course that user-defined parsers are not supported. For configuration inputs that are non-trivial, a post-process facility is provided. This construct is useful since exception handling is performed by the arg parser itself.

Final Remarks

I have used this library extensively in quite a few of my projects and have so far been satisfied with its results. As always, you are welcome to try it out for yourselves and submit your feedback. I anticipate that some people might complain that this isn't a very idiomatic implementation, since most of it depends on reflection. Using parser records would have been much more straightforward implementation-wise, but, there is a certain elegance in declaring unions of parameters that simply cannot be ignored :-)

EDIT: A NuGet package has now been uploaded.

5 comments:

  1. This is very nice and extremely useful. Will you be making this available via NuGet please?

    ReplyDelete
  2. It seems extremely useful. Nice idea.

    ReplyDelete
  3. Nice job, but you need a link to this article from the github project readme.md

    ReplyDelete