2011-07-28

Option Parsing: Allowing Multiple Argument Values

Some options need to take a sequence of argument values. There are several ways to accomplish this using the Nito.KitchenSink Option Parsing library.

Enumeration Flags

If the option values are a series of enumerated flags, then the built-in enumeration parser will handle multiple values automatically:

class Program
{
  [Flags]
  private enum FavoriteThings
  {
    None = 0x0,
    Mittens = 0x1,
    Kittens = 0x2,
    Snowflakes = 0x4,
  }

  private sealed class Options : OptionArgumentsBase
  {
    [Option("favorite-things", 'f')]
    public FavoriteThings FavoriteThings { get; set; }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      Console.WriteLine(options.FavoriteThings);

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe
None

> CommandLineParsingTest.exe /favorite-things Mittens
Mittens

> CommandLineParsingTest.exe /favorite-things Mittens,Kittens
Mittens, Kittens

> CommandLineParsingTest.exe /favorite-things "Mittens, Snowflakes"
Mittens, Snowflakes

> CommandLineParsingTest.exe /favorite-things DogBites
Could not parse  DogBites  as FavoriteThings

Using a Property Setter for Individual Values

The example above works well enough for enumerations, but not all arguments are that simple. In these situations, we can take advantage of the fact that arguments are applied to the options class by property setters.

The following example allows multiple individual values for an argument. As each argument value is set, it is saved into a collection of values.

Note that using a property setter in this fashion is not a good OOP practice; however, the adverse design affects are contained within the options class.

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    public Options()
    {
      Numbers = new List<int>();
    }

    public List<int> Numbers { get; private set; }

    [Option("number", 'n')]
    public int NumberOption
    {
      set
      {
        Numbers.Add(value);
      }
    }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      Console.WriteLine(string.Join(", ", options.Numbers));

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe


> CommandLineParsingTest.exe -n 3
3

> CommandLineParsingTest.exe -n 3 -n 6
3, 6

> CommandLineParsingTest.exe -n 3,6
Could not parse  3,6  as Int32

Note that the last test failed; the options class above only allows multiple individual arguments, not a group of values.

Using a Property Setter for Grouped Values

In this case, we want to be able to pass a sequence of values (delimited somehow) as a single argument, and have them interpreted as multiple individual values.

We can again take advantage of the property setter hack, but we have to do our own parsing of the delimited value. We will use a property type of string to prevent automatic parsing.

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    public Options()
    {
      Numbers = new List<int>();
    }

    public List<int> Numbers { get; private set; }

    [Option("number", 'n')]
    public string NumberOption
    {
      set
      {
        // Note: this example uses poor error handling!
        //  We *should* use TryParse and throw OptionParsingException.
        Numbers.AddRange(value.Split(';').Select(x => int.Parse(x)));
      }
    }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      Console.WriteLine(string.Join(", ", options.Numbers));

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe -n 3 -n 6
3, 6

> CommandLineParsingTest.exe -n 3;6
3, 6

This works, but still feels a bit "hackish". We're out of time for today, but in a few weeks we'll revisit this problem when we talk about custom argument parsers.

2011-07-21

System.Threading.Timer Constructor and Garbage Collection

This week, we take a break from the option parsing posts to bring you an interesting corner case from the BCL.

The System.Threading.Timer constructor has several overloads; all except one take a state parameter which is passed to the TimerCallback delegate when the timer fires.

It turns out that this state parameter (and the TimerCallback delegate) have an interesting effect on garbage collection: if neither of them reference the System.Threading.Timer object, it may be garbage collected, causing it to stop. This is because both the TimerCallback delegate and the state parameter are wrapped into a GCHandle. If neither of them reference the timer object, it may be eligible for GC, freeing the GCHandle from its finalizer.

The single-parameter constructor does not suffer from this problem, because it passes this for the state (not null). Most real-world usage of System.Threading.Timer either references the timer from the callback or uses the timer for the state, so this interesting garbage collection behavior will probably not be noticed.

This blog post was prompted by my own question on Stack Overflow.

2011-07-14

Option Parsing: Options with Optional Arguments

All of the examples so far have illustrated options with required arguments; that is, if the option is passed, it must be followed by an argument. It's also possible to define an option that takes an optional argument:

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    [Option("with-extreme-prejudice", 'p', OptionArgument.Optional)]
    public int? PrejudiceLevel { get; set; }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      if (options.PrejudiceLevel.HasValue)
        Console.WriteLine("Extreme Prejudice specified: " + options.PrejudiceLevel.Value);
      else
        Console.WriteLine("Regular prejudice will do.");

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe
Regular prejudice will do.

> CommandLineParsingTest.exe -p 3
Extreme Prejudice specified: 3

> CommandLineParsingTest.exe -p
Regular prejudice will do.

The last example above illustrates the problem with options that take optional arguments: there isn't an easy way to determine whether the option was passed without an argument or the option was not passed at all. In both of these cases, the property is left at the default value (null in this case).

The solution is to use the OptionPresent attribute, as such:

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    [Option("with-extreme-prejudice", 'p', OptionArgument.Optional)]
    public int? PrejudiceLevel { get; set; }

    [OptionPresent('p')]
    public bool PrejudiceLevelWasSpecified { get; set; }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse();

      if (!options.PrejudiceLevelWasSpecified)
        Console.WriteLine("Regular prejudice will do.");
      else if (options.PrejudiceLevel.HasValue)
        Console.WriteLine("Extreme Prejudice specified: " + options.PrejudiceLevel.Value);
      else
        Console.WriteLine("Extreme Prejudice specified.");

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe
Regular prejudice will do.

> CommandLineParsingTest.exe -p 3
Extreme Prejudice specified: 3

> CommandLineParsingTest.exe -p
Extreme Prejudice specified.

It is now possible to distinguish all possibilities. The OptionPresent example above uses the short option name, but this attribute also works with long names.

2011-07-07

Option Parsing: Default and Nullable Argument Values

Most of our examples so far have already dealt with options taking arguments, because most options in the real world do take arguments. Today we'll start looking at option arguments in depth.

The option pipeline post laid out the steps taken when using an Option Arguments class:

  1. The Option Arguments class is default-constructed.
  2. Attributes of properties on the class are used to produce a collection of option definitions.
  3. The command line is parsed, setting properties on the Option Arguments instance.

We'll take advantage of these steps to handle several common scenarios.

Default Values

Default argument values are set in the default constructor:

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    public Options()
    {
      // Set default values.
      Quality = 3;
    }

    [Option("level", 'l')]
    public int Level { get; set; }

    [Option("quality")]
    public int Quality { get; set; }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      Console.WriteLine("Level: " + options.Level);
      Console.WriteLine("Quality: " + options.Quality);

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe
Level: 0
Quality: 3

> CommandLineParsingTest.exe /level 7
Level: 7
Quality: 3

> CommandLineParsingTest.exe /quality 4
Level: 0
Quality: 4

Nullable Values

There are some situations where a "default value" doesn't make sense for an option; you need to know whether there was a value passed, and what the value is (if it was passed). In this situation, you can use a nullable value type for your property:

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    [Option("level", 'l')]
    public int? Level { get; set; }

    [Option("name", 'n')]
    public string Name { get; set; }
  }

  static int Main()
  {
    try
    {
      var options = OptionParser.Parse<Options>();

      if (options.Level.HasValue)
        Console.WriteLine("Level: " + options.Level.Value);
      else
        Console.WriteLine("Level not specified.");
      if (options.Name != null)
        Console.WriteLine("Name: " + options.Name);
      else
        Console.WriteLine("Name not specified.");

      return 0;
    }
    catch (OptionParsingException ex)
    {
      Console.Error.WriteLine(ex.Message);
      return 2;
    }
    catch (Exception ex)
    {
      Console.Error.WriteLine(ex);
      return 1;
    }
  }
}
> CommandLineParsingTest.exe
Level not specified.
Name not specified.

> CommandLineParsingTest.exe /level 3
Level: 3
Name not specified.

> CommandLineParsingTest.exe /level 0
Level: 0
Name not specified.

> CommandLineParsingTest.exe /name Bob
Level not specified.
Name: Bob

> CommandLineParsingTest.exe /name ""
Level not specified.
Name: