pcapriotti / optparse-applicative

Applicative option parser
BSD 3-Clause "New" or "Revised" License
910 stars 116 forks source link

Options shown in Global section can include unreachable alternatives. #461

Open ChrisPenner opened 2 years ago

ChrisPenner commented 2 years ago

Hello 👋🏼

The CLI for Unison has one very common use-case (launching the unison shell) and several other 'utility' commands. The way we have things set up right now is as follows:

The way we've implemented this is by using our launch option parser as a fallback for our subcommand parser:

topLevelParser =
  hsubparser subcommands <|> launchCommand

Meaning that if the user doesn't provide a command, we interpret it as a 'launch'.

The issue is that currently optparse-applicative interprets all of the options of launch as global options, even though all of those options will fail if passed to the run subcommand for example.

Is there any way to indicate options as being only relevant if no subcommand is provided, rather than showing them as global?

Effectively I want the empty command to just be treated as another subcommand.

This may not be the most common setup, but if you can think of a way to make this work I'd love to hear it 😄

Thank you for maintaining such an awesome tool, it saves us a lot of time :D

HuwCampbell commented 2 years ago

The parser you mentioned should just work and be pretty intuitive. An option passed for the launch command will lock in one side alternative. Indeed everything should be wrapped parenthetically with an alternative marker.

Can you give me an example of what the help looks like and what you think it should look like? Or some CLI examples with desired vs current behaviour given a particular parser?

ChrisPenner commented 2 years ago

The executable itself works as expected, it seems to be just the help messages that are out of whack; The issue is that currently optparse-applicative interprets all of the options of launch as global options, even though all of those options will fail if passed to the run subcommand for example.

So, for example, for the unison executable, launch has a --port flag, but run does not.

If I get the help for run, it shows ALL of the options for launch under "Global options" even though some of them only work for the launch command specifically (or the empty top-level command which ends up calling launch)

$ ucm run --help
Usage: unison-trunk run SYMBOL [RUN-ARGS]
  Execute a definition from the codebase, passing on the provided arguments. To pass flags to your
  program, use `run <symbol> -- --my-flag`

Available options:
  -h,--help                Show this help text

Global options:
  -v,--version             Show version
  -c,--codebase CODEBASE/PATH
                           The path to an existing codebase
  -C,--codebase-create CODEBASE/PATH
                           The path to a new or existing codebase (one will be created if there
                           isn't one)
  --exit                   Exit repl after the command.
  --token STRING           API auth token
  --host STRING            Codebase server host
  --port NUMBER            Codebase server port
  --ui DIR                 Path to codebase ui root
  --no-base                if set, a new codebase will be created without downloading the base
                           library, otherwise the new codebase will download base

Notice that --port is listed here as a global option, but it's not actually a valid option for the run command; and opt-parse applicative will correctly show an error if we try to use it (although the option still incorrectly appears in the global options of the usage info):

$ ucm run --port 5050
Invalid option `--port'

Usage: unison-trunk run SYMBOL [RUN-ARGS]
  Execute a definition from the codebase, passing on the provided arguments. To pass flags to your
  program, use `run <symbol> -- --my-flag`

Available options:
  -h,--help                Show this help text

Global options:
  -v,--version             Show version
  -c,--codebase CODEBASE/PATH
                           The path to an existing codebase
  -C,--codebase-create CODEBASE/PATH
                           The path to a new or existing codebase (one will be created if there
                           isn't one)
  --exit                   Exit repl after the command.
  --token STRING           API auth token
  --host STRING            Codebase server host
  --port NUMBER            Codebase server port
  --ui DIR                 Path to codebase ui root
  --no-base                if set, a new codebase will be created without downloading the base
                           library, otherwise the new codebase will download base

Hopefully that helps clear up the issue; it's really just a matter of things showing up in global options that aren't actually global :)

Let me know if any further clarification would help!

HuwCampbell commented 2 years ago

Ok I see.

The issue is that global options can appear in the help text which are invalid when an alternative is taken which includes a subparser. And yes, that's not ideal.

The globals are pulled in from the parent parsers as they were before any data was parsed, so they don't "know" about any alternatives accepted. I did this so that you'd still see the help text for options which were provided as well.

Workarounds:

If you don't actually have global options, the simplest thing to do is just not include helpShowGlobals when building the parser. In general that flag works best with subparserInline anyway, as then the global ones can actually mix in with the child ones during use.

Alternatively, you can use the noGlobal modifier to suppress individual options which you don't want to bubble into the subparsers.

I'll leave this issue open (and maybe rename it) as there is an underlying bug there.

ChrisPenner commented 2 years ago

Manually adding noGlobal seems to do the trick for now, thanks for the tip!