fsprojects / Argu

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

How to differentiate between cases when `--help` was passed explicitly and there was an argument parse error? #154

Closed ForNeVeR closed 4 years ago

ForNeVeR commented 4 years ago

Description

I have an argument set like this (with more subcommands, of course):

open Argu

type SubcommandArguments =
    | [<Unique>] Json
    interface IArgParserTemplate with
        member s.Usage =
            match s with
            | Json -> "write output in JSON format."

[<RequireQualifiedAccess>]
type MainArguments =
    | [<CliPrefix(CliPrefix.None)>] Subcommand of ParseResults<SubcommandArguments>

And I want to achieve the following behavior:

  1. My application should print the usage when it couldn't parse the arguments or if the user has explicitly requested usage (app.exe subcommand --help or app.exe --help)
  2. Argument parse error should go to stderr; explicitly requested help message should go to stdout
  3. My application should return 0 if it was explicitly requested to show help (e.g. app.exe --help → exit code 0)
  4. My application should return 1 if there was an argument parse error (e.g. app.exe --blahblah → exit code 1)

Currently, I feel like Argu lacks the functionality I need to implement these requirements. Which is the best way to do it with Argu?

The approaches I've tried are listed below. So far, only option 3 works to fulfill all the requirements, and I don't like it.

1. raiseOnUsage = true

[<EntryPoint>]
let main _ =
    let args = [|"subcommand"; "--help"|]
    let parser = ArgumentParser.Create<MainArguments>()
    try
        let arguments = parser.ParseCommandLine(args, raiseOnUsage = true)
        0
    with
    | :? ArguParseException as ex ->
        eprintfn "%s" ex.Message
        1

This example should return with exit code 0 (because it was asked for --help explicitly), but it has no way to differentiate the parse errors or explicit --help argument, so it returns 1.

2. Check IsUsageRequested

[<EntryPoint>]
let main _ =
    let args = [|"subcommand"; "--help"|]
    let parser = ArgumentParser.Create<MainArguments>()
    try
        let arguments = parser.ParseCommandLine(args, raiseOnUsage = false)
        if arguments.IsUsageRequested
        then printfn "%s" <| parser.PrintUsage()
        0
    with
    | :? ArguParseException as ex ->
        eprintfn "%s" ex.Message
        1

This doesn't work properly, too, because arguments.IsUsageRequested is false if only subcommand has been requested for help.

3. Check IsUsageRequested for every subcommand

[<EntryPoint>]
let main _ =
    let args = [|"subcommand"; "--help"|]
    let parser = ArgumentParser.Create<MainArguments>()
    try
        let arguments = parser.ParseCommandLine(args, raiseOnUsage = false)
        if arguments.IsUsageRequested
        then printfn "%s" <| parser.PrintUsage()

        let subcommandArguments = arguments.GetResult MainArguments.Subcommand
        if subcommandArguments.IsUsageRequested
        then printfn "%s" <| subcommandArguments.Parser.PrintUsage()
        0
    with
    | :? ArguParseException as ex ->
        eprintfn "%s" ex.Message
        1

This one does work, but requires me to write the same boilerplate code for every subcommand, which I don't like to do.

Related information

Argu version used: 6.1.1, platform: .NET Core.

eiriktsarpalis commented 4 years ago

I think you might be able to achieve this by implementing the IExiter interface.

ForNeVeR commented 4 years ago

Thank you! This, indeed, has solved all my problems, and helped me to simplify the argument parsing code.