Skip to content

Options

DoctorKrolic edited this page Apr 13, 2024 · 4 revisions

In library's terminology an "option" is a named value, which syntax follows GNU syntax conventions:

  • Arguments are options if they begin with a hyphen delimiter (-)
  • Multiple options may follow a hyphen delimiter in a single token if the options do not take arguments. Thus, -abc is equivalent to -a -b -c
  • Short option names are single alphanumeric characters. Here the library is a bit strickter and allows only letters as a valid short option names
  • An option and its argument may or may not appear as separate tokens. (In other words, the whitespace separating them is optional.) Thus, -o foo and -ofoo are equivalent
  • Options typically precede other non-option arguments (but that is not a requirement)
  • The argument -- terminates all options; any following arguments are treated as non-option arguments, even if they begin with a hyphen
  • A token consisting of a single hyphen character is interpreted as an ordinary non-option argument
  • Options may be supplied in any order, or appear multiple times. The interpretation is left up to the particular application program. In case of this library having option defined multiple times creates DuplicateOptionError(s) unless option if of a sequence type
  • Long options consist of -- followed by a name made of alphanumeric characters and dashes. Option names are typically one to three words long, with hyphens to separate words. Users can abbreviate the option names as long as the abbreviations are unique
  • --name=value syntax can be used to specify option's value

Declaring options

Each option corresponds to a C# property in options type. In order to declare an option property is annotated with [Option] attribute from ArgumentParsing namespace, e.g.:

using ArgumentParsing;

[OptionsType]
class Options
{
    [Option]
    public string MyOption { get; set; }
}

Each option has short single character name, used in short argument syntax (e.g. option with short name a can be referenced by argument -a), and a long name, used in long argument syntax (e.g. option with long name option-a can be referenced by argument --option-a). By default options, annotated with pure [Option] attribute, are automatically assigned a long name, which is corresponding C# property name in lower kebab case, e.g. in the example above MyOption will be automatically assigned my-option long name. Short name is never assigned automatically. There are several constructors of [Option] attribute, which allow to set or override default option's short and long names:

using ArgumentParsing;

[OptionsType]
class Options
{
    [Option] // No short name, long name `option-a`
    public string OptionA { get; set; }

    [Option('b')] // Short name `b`, long name `option-b`
    public string OptionB { get; set; }

    [Option("my-option-c")] // No short name, long name `my-option-c`
    public string OptionC { get; set; }

    [Option('d', "my-option-d")] // Short name `d`, long name `my-option-d`
    public string OptionD { get; set; }

    [Option('e', null)] // Short name `e`, no long name
    public string OptionE { get; set; }
}

Note

The following scenario is not valid:

using ArgumentParsing;

[OptionsType]
class Options
{
    [Option(null)] // This technically declares an option with no short name and no long name
    public string Option { get; set; }
}

There are certain rules applied to option names:

  • Short option name can only be a letter
  • Long option name must start with a letter
  • Long option name can only contain alphanumeric characters and hyphen characters (-)

If short or long name doesn't follow these rules an error is reported on option declaration. If there are two options with same short or long name, an error is reported on both option declarations to easily identify problematic cases.

Option types

Options support a wide variety of types.

Base types

  • string and char
  • Integer numeric types: byte, sbyte, short, ushort, int, uint, long, ulong and BigInteger
  • Real numeric types: float, double and decimal
  • bool
  • Any enum
  • DateTime, DateOnly, TimeOnly
  • TimeSpan

Parse strategies for each category are the following:

  • string option values are directly captured from supplied arguments
  • If value is single-character long it can be assigned to a char option. If char option encounters a value of length greater than one, a BadOptionValueFormatError is reported
  • All numeric types, enums and date/time-related options are parsed with their TryParse methods. If parsing fails, a BadOptionValueFormatError is reported. CultureInfo.InvariantCulture is used as a format provider for parsing when there is a TryParse overload, which accepts an IFormatProvider
  • bool options are somewhat special. If bool option is not specified in arguments, it is false by default. If it is specified, however, it automatically becomes true. bool options do not accept values. If a bool option is supplied with a value, a FlagOptionValueError is reported

Nullable types

In addition to base types, any Nullable<T> option type is valid as long as underlying T is a valid base value type. If a nullable option is not supplied in arguments, it is null by default, otherwise it contains corresponding value of type T. This allows to distinguish between cases when option value is not supplied and when it is assigned a default value, e.g. an option of type int? will be null if it is not supplied in arguments, but it can be assigned value 0 in arguments (which is a default value for an underlying int type) and these are two distinct cases. If that option had int type, not supplying it in arguments and supplying a value of 0 would be undistinguishable. Options of bool? type have special behavior since they combine semantics of bool and Nullable<T> option types. If bool? option is not supplied in arguments, it is null by default. If it is supplied without a value, it it true. However, unlike bool options, bool? options can accept an optional value, which is gonna be parsed and assigned to the option. So in the end for a bool? option with short name b and long name option-b the following behavior is applied:

Arguments Option value
<no args> null
-b true
-b true true
-b false false
--option-b true
--option-b true true
--option-b=true true
--option-b false false
--option-b=false false

Passing incorrect value will cause BadOptionValueFormatError, e.g. in case -b badValue is encountered.

Sequence types

There is also a separate subclass of options called "sequence" options. Valid types of sequence options are:

  • IEnumerable<T>
  • IReadOnlyCollection<T>
  • IReadOnlyList<T>
  • ImmutableArray<T>

...where T is a valid base type.

Note

The list of valid sequence type collections is intentionally limited to immutable collection types/interfaces. Future additions to this list will follow the same principal, meaning that sequence types like List<T> are probably never gonna be added

Important

Due to the nature of source generators you will have direct access to the underlying types of sequence interfaces (unless they are provided as gnerated collections with file accessibility, which is not currently the case), meaning, that you, for instance, can declare a sequence of IEnumerable<T> type and after getting options object downcast this option to its actual type. Such scenarios are not supported! The "contract" you have with a library is that you get IEnumerable<T>, but nothing more concrete. The underlying type of such interfaces can chage between releases or even be different for different options in one program depending on certain logic internal to the generator

Sequence options can take multiple values of their type T in a row, e.g. for the given options type:

using ArgumentParsing;
using System.Collections.Generic;

[OptionsType]
class Options
{
    [Option('i')]
    public IEnumerable<int> Ints { get; set; }
}

... the following arguments are valid:

Arguments Option value
<no args> []
-i []
--ints []
-i 1 [1]
-i 1 2 3 [1, 2, 3]
--ints 1 [1]
--ints 1 2 3 [1, 2, 3]

If option value is "directly assigned", i.e. using short syntax without space (-i1) or --name=value syntax, subsequent arguments are not allowed:

-i5 6 7 // illegal
--ints=1 2 3 // illegal

Duplicate arguments of sequence options are not allowed as well:

-i 1 2 3 -i 4 5 6 // illegal
--ints 1 2 3 --ints 4 5 6 //illegal

These restrictions are in place to avoid confusing usage scenarios (e.g. --ints=1 --ints=2) and leave some space for extending sequence options in the future.

Required options

Options can be made required. In such case MissingRequiredOptionError is reported if option is not provided in arguments.

There are two ways of making an option required:

  1. Declare option property as required. This is a recommended way if your environment (C# version + runtime metadata attribute) supports required properties
  2. Annotate option with [System.ComponentModel.DataAnnotations.Required] attribute

There are 2 special cases when it comes to required options:

  1. bool options cannot be made required. Due to their semantics making a bool option required will force it to be present in arguments, which implies that the value of such option in all valid cases will always be true. Thus required bool options don't make sense and are forbidden (error diagnostic is reported if you try to declare one)
  2. It doesn't make sense to declare nullable required options as well, since this makes null value of such options impossible to get in all valid cases. But since they can still have different underlying values, this is just a warning and not an error
Clone this wiki locally