docopt / docopt.net

Port of docopt to .net
https://docopt.github.io/docopt.net/
Other
350 stars 33 forks source link

New (static) parsing API #158

Closed atifaziz closed 2 years ago

atifaziz commented 2 years ago

This PR introduces a new API on the Docopt class with the following benefits:

The new API introduces the notion of a strong-typed parser that produces 4 distinct results:

There is also an API for creating and configuring a parser given a docopt text source. Depending on the configuration, the parser changes its API shape to reflect the results it will produce (one of the four above). Consequently, there are also four parser types:

The table below summarizes the possible results produced by each parser when the Parse method of a parser is called:

Parser/Result IArgumentsResult<T> IInputErrorResult IHelpResult IVersionResult
IBaselineParser<T>
IHelpFeaturingParser<T>
IVersionFeaturingParser<T>
IParser<T>

There is a new static method on Docopt called CreateParser:

public static IHelpFeaturingParser<IDictionary<string, Value>> CreateParser(string doc)

Below is a demonstration of how it is designed to be used, taking NavalFate as an example:

using System;
using System.Collections.Generic;
using DocoptNet;

const string help = @"Naval Fate.

Usage:
  naval_fate.exe ship new <name>...
  naval_fate.exe ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.exe ship shoot <x> <y>
  naval_fate.exe mine (set|remove) <x> <y> [--moored | --drifting]
  naval_fate.exe (-h | --help)
  naval_fate.exe --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.

";

var argsParser = Docopt.CreateParser(help).WithVersion("Naval Fate 2.0");

Once the parser is initialized, it's Parse method can be given the command-line arguments. The result of the parsing can be done in one of two ways. Either using pattern-matching:

switch (argsParser.Parse(args))
{
    case IArgumentsResult<IDictionary<string, Value>> { Arguments: var arguments }:
        foreach (var (key, value) in arguments)
            Console.WriteLine("{0} = {1}", key, value);
        return 0;
    case IHelpResult:
        Console.WriteLine(help);
        return 0;
    case IVersionResult { Version: var version }:
        Console.WriteLine(version);
        return 0;
    case IInputErrorResult { Usage: var usage }:
        Console.Error.WriteLine(usage);
        return 1;
    case var result:
        throw new System.Runtime.CompilerServices.SwitchExpressionException(result);
}

or using the Match method of the parse result like so:

return argsParser.Parse(args)
                 .Match(Run,
                        result => { Console.WriteLine(result.Help); return 0; },
                        result => { Console.WriteLine(result.Version); return 0; },
                        result => { Console.Error.WriteLine(result); return 1; });

static int Run(IDictionary<string, Value> arguments)
{
    foreach (var (key, value) in arguments)
        Console.WriteLine("{0} = {1}", key, value);
    return 0;
}

The second approach provides strong compile-time guarantees. For example, it the earlier call to setup the parser is changed to read instead:

var argsParser = Docopt.CreateParser(help)/*.WithVersion("Naval Fate 2.0")*/;

then the version using pattern-matching will still compile, but the Match will produce a compile-time error since its arity will change and the code will need to be updated to compile again:

return argsParser.Parse(args)
                 .Match(Run,
                        result => { Console.WriteLine(result.Help); return 0; },
                     // result => { Console.WriteLine(result.Version); return 0; },
                        result => { Console.Error.WriteLine(result); return 1; });

See also tests/DocoptNet.Tests/ParserApiTests.cs for example uses.

The source generator has also been update to use this new API.