Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
synfinatic opened this issue Oct 17, 2023 · 1 comment
Open

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

synfinatic opened this issue Oct 17, 2023 · 1 comment

Comments

@synfinatic
Copy link

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
Copy link
Owner

WillAbides commented Oct 22, 2023

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
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants