Whippet F# source generator plugin, for generating arg parsers.
$ dotnet add package WoofWare.Whippet.Plugin.ArgParserThis is a Whippet plugin defining an argument parser.
It is a copy of the corresponding Myriad arg parser in WoofWare.Myriad, taken from commit d59ebdfccb87a06579fb99008a15f58ea8be394e.
Define an Args.fs file like the following:
namespace MyNamespace
[<ArgParser>]
type LoadsOfTypes =
{
Foo : int
Bar : string
Baz : bool
SomeFile : FileInfo
SomeDirectory : DirectoryInfo
SomeList : DirectoryInfo list
OptionalThingWithNoDefault : int option
[<PositionalArgs>]
Positionals : int list
[<ArgumentDefaultFunction>]
OptionalThing : Choice<bool, bool>
[<ArgumentDefaultFunction>]
AnotherOptionalThing : Choice<int, int>
[<ArgumentDefaultEnvironmentVariable "CONSUMEPLUGIN_THINGS">]
YetAnotherOptionalThing : Choice<string, string>
}
static member DefaultOptionalThing () = true
static member DefaultAnotherOptionalThing () = 3
In your fsproj:
<Project>
<ItemGroup>
<Compile Include="Args.fs" />
<Compile Include="GeneratedArgs.fs">
<WhippetFile>Args.fs</WhippetFile>
</Compile>
</ItemGroup>
<ItemGroup>
<!-- Runtime dependency: you use attributes to give instructions to the generator.
Specify the `Version` appropriately by getting the latest version from NuGet.org.
-->
<PackageReference Include="WoofWare.Whippet.Plugin.ArgParser.Attributes" Version="" />
<!-- Development dependencies, hence PrivateAssets="all". Note `WhippetPlugin="true"`. -->
<PackageReference Include="WoofWare.Whippet.Plugin.ArgParser" WhippetPlugin="true" Version="" />
<PackageReference Include="WoofWare.Whippet" Version="" PrivateAssets="all" />
</ItemGroup>
</Project>
The generator will produce a file like the following:
[<RequireQualifiedAccess>]
module LoadsOfTypes =
// in case you want to test it, you get one with dependencies injected
let parse' (getEnvVar : string -> string) (args : string list) : LoadsOfTypes = ...
// this is the one we expect you actually want to use, if you don't want to test the arg parser
let parse (args : string list) : LoadsOfTypes = ...
Choice<'a, 'a>: you get a Choice1Of2 if the user provided the input, or a Choice2Of2 if the parser filled in your specified default value.[<ArgumentDefaultEnvironmentVariable "ENV_VAR">]. If such an arg is not supplied on the command line, its value is parsed from the value of that env var.[<ArgumentDefaultFunction>]. If an arg [<ArgumentDefaultFunction>] Foo : Choice<'a, 'a> is not supplied on the command line, the parser calls DefaultFoo : unit -> 'a to obtain its value.[<PositionalArgs>] will accumulate all args which didn't match anything else. By default, the parser will fail if any of these arguments looks like an arg itself (i.e. it starts with --) but comes before a positional arg separator --; you can optionally give this attribute the argument (* includeFlagLike = *) true to instead just put such flag-like args into the accumulator.Choice<'a, 'a> list, in which case we tell you whether the arg came before (Choice1Of2) or after (Choice2Of2) any -- positional args separator.[<InvariantCulture>] and [<ParseExact @"hh\:mm\:ss">] attributes.[<ArgParser (* isExtensionMethod = *) false>].--help appears in a position where the parser is expecting a key (e.g. in the first position, or after a --foo=bar), the parser fails with help text. The parser also makes a limited effort to supply help text when encountering an invalid parse.type DryRun = | [<ArgumentFlag false>] Wet | [<ArgumentFlag true>] Dry. Then you can consume the flag like a bool: [<ArgParser>] type Args = { DryRun : DryRun }, so --dry-run is parsed into DryRun.Dry.[<ArgumentLongForm "alternative-name">] Foo : int, so that instead of accepting the default --foo=3, we accept --alternative-name=3.[<ArgumentHelpText "this text is displayed next to the arg when the user calls --help">], and similarly help text for the entire args object is supplied with [<ArgParser>] [<ArgumentHelpText "hi!">] type Args = ....This is very bare-bones, but do raise GitHub issues if you like (or if you find cases where the parser does the wrong thing).
It should work fine if you just want to compose a few primitive types, though.