Skip to content

Latest commit

 

History

History
149 lines (121 loc) · 7.53 KB

File metadata and controls

149 lines (121 loc) · 7.53 KB

Configuring the parser

Overview

Configuring the parser is the processing of informing it what arguments and options are available to the user and what properties of the options model the parsed values are mapped to.

Conventions

The argument parser understands the following conventions:

  • Options are short or long form GNU style identifiers that associate with a parameter value. A parameter can be concatenated to the symbol itself using : or =, or it can follow as a separate argument. Examples: -u, --user, --user-id, --user-id:sa.
  • Switches are options that are always represented as the value true.
  • Option/switch groups are syntatic shortcuts for users where multiple switches and a single option can be combined into one token. For example, -abc:red is treated equivalently as input -a -b -c red.
  • Position arguments are parameter values whose semantic meaning is inferred by their positions in the input.
  • Variadic arguments are position arguments that can be indefinately repeated. An application can only define one variadic position argument at most for each unique command path. When used with other position arguments, it must be in the last parsing position.
  • Directives are symbols that invoke applcation functions outside of the command/model framework. The parser recognizes the following pattern for directives: \[(?<identifier>\w+)([:=](?<parameter>.+))?\].
  • Annotations are paths to response files. When a file path is preceded by the @ character, the tokens in the file are injected into the input.

Configuration

Options, switches, and position arguments are configured by pairing each with a model property and specifying the following characterstics:

  • Aliases identify options and switches with GNU short/long form identifiers. If omitted, the binding property's name in lower/kebab cased form is used (UserId becomes --user-id).
  • Ordinal position determines what order the parser evaluates position arguments (required). Each position argument requires a unique ordinal position.
  • Default values are used in the absence of user input. Provide a default for optional symbols that map to non-nullable types (or expect default(T)! as the mapped value).
  • Required indicates whether an option with a parameter value or position argument must be provided, whether by user input or a default value.
  • Arity defines the expected and allowed number of uages for a multi-valued option or position argument. An arity with no maximum constraint is variadic.
  • Input values can be validated using contextual evaluation.
  • A help topic provides information to the user for the option or argument.

The following example illustrates configuring the parser for a command model type:

// Commmand model:
[GeneratedBinding]
interface IUploadOptions
{
    string UserId { get; }
    string Password { get; }
    bool UseBrowser { get; }
    string[] FilePaths { get; }
}

// Parser configuration:
var app = new CommandLineApplication(rootCommand);

app.ConfigureParser<IUploadOptions>(parser => parser
    .ParseOption(
        expression: x => x.UserId,
        aliases: ["-u", "--user-id"],
        required: true,
        helpTopic: "User ID that has access to the storage account")
    .ParseOption(
        expression: x => x.Password,
        aliases: ["-p", "--password"],
        required: true,
        helpTopic: "Password to the account")
    .ParseSwitch(
        expression: x => x.UseBrowser,
        helpTopic: "Whether to use the browser for authorization flow.")
    .ParseRepeatableArgument(
        expression: x => x.FilePaths,
        arity: Arity.OneOrMore,
        helpTopic: "Path to one or more files to upload.",
        validate: fileInfo => fileInfo.MustExist()));

Supported property types

The parser will automatically convert and set the following property types:

  • Types that implement IParsable<T> or IParsable<T?>. This covers System primitives and their nullable value-type variants.
  • Enum/Enum? value types parsed using case-insensitive matching.
  • string, FileInfo, DirectoryInfo, and Uri.

Additionally, the parser can set the following multi-value property types:

  • Arrays
  • List<T>, LinkedList<T>, HashSet<T>, SortedSet<T>, Stack<T>, and Queue<T>
  • ImmutableArray<T>, ImmutableList<T>, ImmutableHashSet<T>, ImmutableSortedSet<T>, ImmutableStack<T>, and ImmutableQueue<T>
  • IEnumerable<T>, ICollection<T>, IReadOnlyCollection<T>, IList<T>, IReadOnlyList<T>, ISet<T>, and IReadOnlySet<T>

Variadic arugment limitations

When the maxium arity of a multi-valued symbol is undefined, it is considered variadic. The parser places the following constraints on their use:

  • A final command model may only have one variadic symbol defined or inherited.
  • When multiple position arguments are used in conjunction with a variadic argument, the variadic argument must have the highest ordinal position in relation to the other arguments.

Custom value conversion

For each model property, the parser converts a string to the expected type. In the case of mutli-valued properties that are backed by arrays or collections, the parser must then convert enumerations of the values into collections of the value type.

For scalar value types, conversion can be implemented on a type in one of two ways:

  • Implement IParsable<T> on the scalar type. The source generator will automatically configure the conversion service for the target type.
  • When the application does not control the scalar type, register a Converter<string, T> delegate to the conversion service.

For collection types, conversion can be implemented by registering a Converter<TElement, TCollection> delegate to the conversion service where:

  • TElement is the scalar value type.
  • TCollection is the collection type that implements IEnumerable<TElement>.

In either implementation, applications should throw an exception if the conversion cannot complete.

The following example demonstrates how to bind argument values to a dictionary:

// Model
interface IOptions
{
    IReadOnlyDictionary<string, sring> Properties { get; }
}

// Configuration
app.ConfigureParser<IOptions>(parser => parser
    .AddRepeatableOption<
        KeyValuePair<string, string>,
        IReadOnlyDictionary<string, string>>(
            x => x.Properties,
            aliases: "--prop",
            arity: Arity.ZeroOrMore,
            helpTopic: "A key/value pair property, e.g. --prop user:sa"));

// Convert from string argument to key/value pair
var app = new CommandLineApplication(rootCommand);

// Adds parser support for KeyValuePair<string, string>. Alternatively, the
// regular expression could be wrapped into the collection converter.
app.AddArgumentConverter(str => 
{
    if (Regex.Match(str, @"(?<key>\w+)[:=](?<value>.+)") is not { Success: true } match)
        throw new ArgumentException("invalid key/value pair format.");
    
    return new KeyValuePair<string, string>(
        match.Groups["key"].Value,
        match.Groups["value"].Value);
});

// Create the dictionary with key/value pairs. Note the collection type
// must match the interface's property type so the converter can be
// located.
app.AddCollectionConverter<
    KeyValuePair<string, string>, 
    IReadOnlyDictionary<string, string>>(entries => 
    {
        var dictionary = new Dictionary<string, string>();
        foreach (var kv in entries)
        {
            if (dictionary.TryAdd(kv.Key, kv.Valeu)) continue;
            throw new ArgumentException($"duplicate key '{kv.Key}'.");
        }
        return dictionary;
    };