alecthomas / kong

Kong is a command-line parser for Go
MIT License
2.17k stars 141 forks source link

`passthrough` passes through a flag as positional #463

Open dbohdan opened 1 month ago

dbohdan commented 1 month ago

I have found a surprising passthrough behavior in Kong v1.2.1. The following example demonstrates it.

package main

import (
    "encoding/json"
    "fmt"

    "github.com/alecthomas/kong"
)

type cli struct {
    Command string   `arg:"" passthrough:""`
    Args    []string `arg:"" optional:"" name:"args"`
    Timeout float64  `short:"t" default:"0"`
}

func main() {
    var cliConfig cli
    kong.Parse(&cliConfig)

    m, err := json.Marshal(cliConfig)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(m))
}
$ ./foo -t 9 ls -la
{"Command":"ls","Args":["-la"],"Timeout":9}
$ ./foo -t 9 -la
{"Command":"-l","Args":["a"],"Timeout":9}

Kong takes the first flag and passes it through as a positional argument. It seems like this should be an error. Instead of -l becoming a positional argument, I expected it to be treated as an unknown flag and flags to only be passed through after a non-flag argument.

Edit: I have just learned about github.com/alecthomas/repr. :-)

boblail commented 1 week ago

@dbohdan, in your example, you're defining Command (ls) as something that wants to slurp up and pass through remaining arguments unparsed. The purpose for this feature is often to forward the remaining command line to another executable. It seems to me that Kong is correctly in this case.

Related: #435

dbohdan commented 1 week ago

To me, it seems passthrough on a positional argument should have the semantics of a regular positional argument. The readme currently suggests it does:

If present on a positional argument, it stops flag parsing when encountered, as if -- was processed before. Useful for external command wrappers, like exec. On a command it requires that the command contains only one argument of type []string which is then filled with everything following the command, unparsed.

IMO, it is surprising and contradicts the readme that passthrough treats a flag as a positional argument. I think #435 passthrough and pre-#435 passthrough should be separate tags available in the same version of Kong or an extra tag should activate the old behavior of passthrough. (Since Kong is now stable and you can't change the default.)

Also note that because of the flag behavior the -la argument in my example is not unparsed. When Kong splits the string -la into {"Command":"-l","Args":["a"]}, the result is parsed, just in an unexpected manner.

alecthomas commented 1 week ago

Yep, I agree it is unexpected behaviour.

i4ki commented 3 hours ago

I also found another strange behavior when upgrading to v1.4.0. I don't know if it's the same issue described by @dbohdan, if not please let me know and I open another issue.

I don't know if I'm doing something wrong but we used the configuration below for a long time and it did exactly what we wanted:

type runCommandFlags struct { // this defines the "terramate run" command
  // <any other flags here>

  Command    []string `arg:"" name:"cmd" predictor:"file" passthrough:"" help:"Command to execute"`
}

Before v1.2.1, it detected unknown flags correctly in the parser. Example:

$ go install github.com/terramate-io/terramate/cmd/terramate@v0.11.2
$ terramate run -X --does-not-exist echo ok
Error: parsing cli args [run -X --does-not-exist echo ok]
> unknown flag --does-not-exist
exit status 1

But now:

$ go install github.com/terramate-io/terramate/cmd/terramate@v0.11.3
$ terramate run -X --does-not-exist echo ok -X
terramate: Entering stack in /
Error: one or more commands failed
> executable file not found in $PATH: running `--does-not-exist echo ok -X` in stack /: --does-not-exist
exit status 1

Another regression was the handling of --. Before if you did terramate run -X -c -- echo ok, it parsed []string{"echo", "ok"} in the Command field but in v1.2.1 it parses []string{"--", "echo", "ok"}. We tried to account for this in terramate@v0.11.3 but later we found other issues and we had to rollback to v0.7.1.

Is there a configuration change to retain the old behavior or is this a bug?

alecthomas commented 1 hour ago

@i4ki these aren't regressions, this is the a behaviour change (modulo the bug this issue is for) - everything gets passed through.

I'd be okay with optionally supporting the old behaviour via eg. passthrough:"partial"