Option Parsing: Preventing Multiple Argument Values

When dealing with multiple argument values, there are four basic behaviors: overwrite, append, prevent, and ignore.

Last week’s post contained a few examples of the append behavior, which is supported by having the property setter place the values into a backing list.

The default behavior in the Nito.KitchenSink option parsing library is to overwrite previous values. In other words, options coming later on the command line may “override” options earlier on the command line. Consider this example:

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

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

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

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

> CommandLineParsingTest.exe -l 3
Level: 3

> CommandLineParsingTest.exe -l 3 -l 9
Level: 9

This is the default behavior, and is probably what users expect. However, for some options, the prevent or ignore behaviors may make sense.

The prevent and ignore behaviors are closely related. Like last week’s post, these behaviors are implemented by placing special code in the property setter.

The prevent behavior can be implemented by having a nullable backing value, and throwing from the setter if it is already set. The only tricky part is choosing the exception to throw from the setter; I recommend throwing an exception derived from OptionParsingException, since that indicates a usage error. Any exception thrown from a property setter will be wrapped in an OptionParsingException.OptionArgumentException (in versions 1.1.2 and newer).

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    private int? level;

    [Option("level", 'l')]
    public int? Level
    {
      get
      {
        return this.level;
      }

      set
      {
        if (this.level.HasValue)
          throw new OptionParsingException.OptionArgumentException("The value may only be specified once.");
        this.level = value;
      }
    }
  }

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

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

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

> CommandLineParsingTest.exe -l 3
Level: 3

> CommandLineParsingTest.exe -l 3 -l 9
The value may only be specified once.

Likewise, the ignore behavior can be implemented by having a nullable backing value, and ignoring the setter if it is already set:

class Program
{
  private sealed class Options : OptionArgumentsBase
  {
    private int? level;

    [Option("level", 'l')]
    public int? Level
    {
      get
      {
        return this.level;
      }

      set
      {
        if (!this.level.HasValue)
          this.level = value;
      }
    }
  }

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

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

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

> CommandLineParsingTest.exe -l 3
Level: 3

> CommandLineParsingTest.exe -l 3 -l 9
Level: 3

Note that the ignore behavior may confuse users; most command-line programs use overwrite behavior, which is the default.