fsprojects / Argu

A declarative CLI argument parser for F#
https://fsprojects.github.io/Argu
MIT License
451 stars 75 forks source link

Invalid error when parsing a sub command with only one parameter fixed with the 'ExactlyOnce' attribute. #216

Open bryjen opened 8 months ago

bryjen commented 8 months ago

Description

Attempting to parse a sub command with only one parameter fixed with the ExactlyOnce attribute errors even though the correct format is provided.

Repro steps

open Argu

type Arguments =
    | [<First; CliPrefix(CliPrefix.None)>] Item_Group of ParseResults<SubArguments>

    interface IArgParserTemplate with
        member this.Usage =
            match this with
            | Item_Group _ -> "Specifies an item group."

and SubArguments =
    | [<ExactlyOnce>] Path of path:string list

    interface IArgParserTemplate with
        member this.Usage =
            match this with
            | Path _ -> "Some path."

Expected behavior

Suppose we have the following main function and input as the program args: item-group --path elem1 elem2

[<EntryPoint>]
let main argv =
    let parser = ArgumentParser.Create<Arguments>(errorHandler = ExceptionExiter())

    try
        let parseResults = parser.ParseCommandLine () 
        printfn $"%A{parseResults}"
    with
        | ex -> printfn $"%s{ex.Message}"
    0

Since only one of the path parameters was provided, it is expected to parse normally with the following output:

[Item_Group [Path ["elem1"; "elem2"]]]

Actual behavior

Instead, it errors, indicating that the '--path' parameter is missing despite being provided.

ERROR: missing parameter '--path'.
USAGE: ConsoleApplication.exe item-group [--help] --path [<path>...]

OPTIONS:

    --path [<path>...]    Some path.
    --help                display this list of options.

It has the following stack trace:

   at Argu.ExceptionExiter.Argu.IExiter.Exit[a](String msg, ErrorCode errorCode) in /_//src/Argu/Types.fs:line 63
   at Argu.ArgumentParser`1.ParseCommandLine(FSharpOption`1 inputs, FSharpOption`1 ignoreMissing, FSharpOption`1 ignoreUnrecognized, FSharpOption`1 raiseOnUsage) in /_//src/Argu/ArgumentParser.fs:line 140
   at Program.main(String[] argv) in C:\###\fsharp_proj\src\ConsoleApplication\Program.fs:line 24

Known workarounds

Maintaining the condition of having the parameter be provided exactly once, a work around is by having another hidden parameter like so:

// ...
and SubArguments =
    | [<Hidden>] Hidden
    | [<ExactlyOnce>] Path of path:string list

    interface IArgParserTemplate with
        member this.Usage =
            match this with
            | Hidden -> "" 
            | Path _ -> "Some path."

Parsing then works as expected, with the constraint (exactly once) being enforced as well.

Related information

bartelink commented 8 months ago

An alternate approach is to specify that a Path is a string, and use GetResults to combine the instances into a list (I rarely use lists for things that I need 0/1 of - preferring to declare it in the singular, and then use Contains, TryGetResult, GetResult(x, defVal), GetResult(x, defThunk) to extract.

In that case, you'd be saying that Path is Mandatory. The fun bit is that this does not work... I've always used Unique as a workaround (to stop you supplying more than one). If you fancy having a go at fixing it, the simplest thing to do might be to figure out how to make Mandatory work correctly for subcommands.

I believe what will work is to declare it as a string, and then use PostProcessResults to error if you've the wrong number of items, i.e. parseResults.PostProcessResults(Path, fun xs -> xs |> List.exactlyOne)

bartelink commented 6 months ago

@brygen were you able to get something that works your side?


~I feel that expecting these semantics for list types is debatable - ultimately the main way that the syntax works is that you specify per item whether it is Mandatory or Unique or leave it. Then you can use Conttains, GetResult, TryGetResult, GetResults, to check/extract/get the group. As such, I feel it might be best to close it as long as you have a way you can make your scenario work? (Alternately, someone will need to spec out the exact behavior change and do a PR - having this languish unactionable and/or unactioned for ages helps nobody...)~ Having thought more about it, my initial thought was incorrect - the tutorial uses string list types, so it's a first class feature that should work (i.e. having to do --path one --path two is not the same as being able to spec --paths one two, even if it winds up identical from the consumption point of view give or take a collect operation to flatten the list)