jawher / mow.cli

A versatile library for building CLI applications in Go
MIT License
871 stars 55 forks source link

Global StringArg + command: contextual help does not work #118

Closed 3coma3 closed 3 years ago

3coma3 commented 3 years ago

Hi. First of all, thanks for this excellent package. I hope it will be developed further because it shows an enormous potential.

With the following example code (simplified from docs):

package main

import (
    "fmt"
    "os"
    cli "github.com/jawher/mow.cli"
)

var filename *string

func main() {
    app := cli.App("app", "description")

    filename = app.StringArg("FILENAME", "", "a string argument")

    app.Command("command1", "this should show with -h", command1)

    app.Run(os.Args)
}

func command1(cmd *cli.Cmd) {
    cmd.Action = func() {
        fmt.Printf("Hello from command1")
    }
}

I see the following behavior:

./app command1 --help
Error: incorrect usage

Usage: app FILENAME COMMAND [arg...]

description

Arguments:     
  FILENAME     a string argument

Commands:      
  command1     this should show with -h

Run 'app COMMAND --help' for more information on a command.

I would expect that the contextual help worked here, or is this the intended semantics? I couldn't find anything about this in the documentation, and the sole example combining top level parameters with commands used options instead of string arguments.

In case this is the intended semantics, where can I find the rationale in docs? And if not, and you are kind enough to point me around the code that deals with this part, I hope I can be able to submit a patch (I will at least give it a try).

Thanks!

7fffffff commented 3 years ago

Take a look at the usage string. FILENAME is being specified before COMMAND. If you invoke the program with ./app file.txt command1 --help you should get the help message for command1

If you meant for FILENAME to be an argument to command1, it should be specified by calling cmd.StringArg inside command1

3coma3 commented 3 years ago

Hi @7fffffff , thanks for your reply!

I did look at the usage string, and opened this issue precisely because what the last line is suggesting, is inconsistent with having to pass top level arguments to be able to reach help for subcommands (which also applies for further nested commands):

Run 'app COMMAND --help' for more information on a command.

Actuallly, this is how CLI applications with subcommands work in general: you don't need to pass top level parameters to write CMD subcmd1 --help or CMD subcmd1 subcmd2 --help, ie, the contextual help is independent of "normal" invocations.

Examples: git, ip, btrfs, nmcli, virsh, and so on, (most commands with a subcommand structure). The CLIG standard, also neatly sums up this behavior.

As I mentioned in my initial post, the CLI I posted was just a theoretical, simplified example taken from the mow.cli documentation, to the effects of focusing on illustrating my point. My actual application is a REST wrapper, and it's more involved. It is available at this gist, so as to not extend this reply too much, and this is the help text:

Usage: pve-api-go HOSTPORT USERREALM [-i] [-v...] ((-p=<secret> [-r=<seconds>]) | -t=<tokenid=secret>) COMMAND [arg...]

Go client to the Proxmox Virtual Environment API

Arguments:                 
  HOSTPORT                 Login host:port, port defaults to 8006 if omitted (env $PVE_HOST)
  USERREALM                Login username@realm, realm defaults to pam if omitted (env $PVE_USER) (default "root")

Options:                   
  -h, --help               Show this help
  -V, --version            Show the version and exit
  -i, --insecure           Trust unknown TLS certificates
  -v                       Add v's to increase verbosity (max 3)
  -p, --pass               Login with password or ticket (env $PVE_PASSWORD)
  -r, --refresh            Minimum ticket lifetime to trigger automatic refresh, 0 to disable (default 0)
  -t, --token              Login with API token (env $PVE_API_TOKEN)

Commands:                  
  GET, g, get, list        issue method GET
  POST, p, post, create    issue method POST
  PUT, u, put, set         issue method PUT
  DELETE, d, del, delete   issue method DELETE

Run 'pve-api-go COMMAND --help' for more information on a command.

It was really this application which lead me to opening the issue originally. So here, I have global arguments that are used for all subcommands. But it doesn't make sense to pass a hostname when you want to follow the recommendation in the last line of the help and attempt to check the contextual help for a subcommand (for example pve-api-go POST --help).

7fffffff commented 3 years ago

Ok, I see what you mean. This seems related to #67, in that the bottom line of the help message doesn't reflect the invocation that's expected. The message can be fixed for accuracy and I've made an attempt, but that doesn't address the inconvenience of having to put in an arg just to get help for a subcommand.

I think the parsing process would have to change somewhat to support invoking help the way you want.

3coma3 commented 3 years ago

Yes, I see how the two issues are related. I understand also that a good start may be adjusting the message, and for a root solution I see two options:

  1. Somehow implement a "help mode", with a different AST (I am guessing the parser renders an AST that is then evaluated?, please correct me if I'm wrong)
  2. A generalization of 1. - implement different modes of invocation, each with its own syntax. This would solve not only help modes, but would also add that as a feature. As cool as this one looks, it might be overkill.. OR on the other hand, not a long shot from 1. What's more, if this style of solution leaves to the users the task of writing the syntax spec, it might be even easier than 1. as you would need to write manually the spec for contextual helps, instead of having to generate it automatically.

Anyway, I haven't really looked at the code, and I haven't written parsers yet (but I have written evaluators for DSLs). I hope to be able to help and send a test patch if I succeed. In the meantime, I'll just use it as it is.