WillAbides / kongplete

MIT License
29 stars 9 forks source link

[feature request] call auto-complete on next command #21

Open synfinatic opened 1 year ago

synfinatic commented 1 year ago

So my program behaves a bit like sudo. Ie: you can use it to exec other commands. Technically it does it as a sub-command, ie: aws-sso exec [flags] <command> [flags]

Doesn't seem to be a way to tell kongplete to do basically what sudo does? https://github.com/scop/bash-completion/blob/master/completions/sudo#L17

WillAbides commented 1 year ago

Thanks for the feature request. I spent some time looking into this, and it isn't straight forward.

First off, there isn't a good way to get the completion for the next command from within a Go program because it doesn't have access to the shell it was run from. The closest I see is for bash is something like exec.Command("bash", "-ilc", "your script here"). That will run your script in an interactive login shell, so you would be able to run complete and compgen, but you wouldn't have access to any completions the user registered in .bash_profile or anywhere else -- and .bash_profile is the usual entry point for bash completions.

With that eliminated, we need to register a completion outside of the go program. I came up with something that works for testme exec --myflag foo --, but it only works for bash and requires that the user use "--" to separate testme exec args from the command they are executing. The same idea could be applied to other shells, and I'm pretty sure you can get past the "--" requirement with some more clever shell scripting.

click to expand ```golang package main import ( "fmt" "os" "os/exec" "path/filepath" "text/template" "github.com/alecthomas/kong" "github.com/riywo/loginshell" "github.com/willabides/kongplete" ) type rootCmd struct { Exec execCmd `kong:"cmd"` Greet greetCmd `kong:"cmd"` Debug bool InstallCompletions installCompletionsCmd `kong:"cmd"` } type execCmd struct { Cmd []string `kong:"arg"` } func (c *execCmd) Run(ctx *kong.Context) error { if len(c.Cmd) == 0 { return nil } cmd := exec.Command(c.Cmd[0], c.Cmd[1:]...) cmd.Stdout = ctx.Stdout cmd.Stderr = ctx.Stderr cmd.Stdin = os.Stdin return cmd.Run() } type greetCmd struct { Name string `kong:"arg,default=World"` } func (c *greetCmd) Run() error { fmt.Printf("Hello, %s!\n", c.Name) return nil } var bashTmpl = template.Must(template.New("").Parse(` _kongplete_plain_completion_helper() { local bin="$1" if [ -z "$COMP_LINE" ]; then return fi COMPREPLY=( $( COMP_LINE=$COMP_LINE \ COMP_POINT=$COMP_POINT \ "$bin" ) ) } _kongplete_exec_completion_helper() { local bin="$1" local args=() local arg local arg_index=0 local exec_index=0 local dash_dash_index=0 local arg_count=$COMP_CWORD while [ $arg_index -lt $arg_count ]; do arg="${COMP_WORDS[arg_index]}" if [ "$arg" = "exec" ]; then exec_index=$arg_index fi if [ $exec_index -gt 0 ]; then if [ "$arg" = "--" ]; then dash_dash_index=$arg_index break fi fi args+=("$arg") arg_index=$((arg_index + 1)) done if [ $dash_dash_index -gt 0 ]; then _command_offset $((dash_dash_index + 1)) return fi _kongplete_plain_completion_helper "$bin" } _{{ .cmd }}_plain_completion() { _kongplete_plain_completion_helper "{{ .bin }}" } _{{ .cmd }}_exec_completion() { _kongplete_exec_completion_helper "{{ .bin }}" } complete -F _{{ .cmd }}_plain_completion {{ .cmd }} if declare -f _command_offset >/dev/null 2>&1; then complete -F _{{ .cmd }}_exec_completion {{ .cmd }} fi `)) type installCompletionsCmd struct{} func (c *installCompletionsCmd) Run(ctx *kong.Context) error { shell, err := loginshell.Shell() if err != nil { return fmt.Errorf("couldn't determine user's shell: %w", err) } cmd := ctx.Model.Name bin, err := os.Executable() if err != nil { return err } bin, err = filepath.Abs(bin) if err != nil { return err } if filepath.Base(shell) == "bash" { return bashTmpl.Execute(ctx.Stdout, map[string]string{ "cmd": cmd, "bin": bin, }) } return (&kongplete.InstallCompletions{}).BeforeApply(ctx) } func main() { var cmd rootCmd // Create a kong parser as usual, but don't run Parse quite yet. parser := kong.Must(&cmd, kong.UsageOnError()) // Run kongplete.Complete to handle completion requests kongplete.Complete(parser) // Proceed as normal after kongplete.Complete. ctx, err := parser.Parse(os.Args[1:]) parser.FatalIfErrorf(err) parser.FatalIfErrorf(ctx.Run()) } ```

I haven't done any significant testing of this, so it's status is strictly "works on my machine". I'm not sure if I would want to incorporate something like this into kongplete or not. It's more shell scripting than I want to be responsible for maintaining. I might just make it easier to hook custom scripts into install completions and use this as an example.

I will probably be adding an exec command to bindown soon, so I'm interested in seeing what you come up with.