diff --git a/complete.go b/complete.go index 75eed927..d03207b1 100644 --- a/complete.go +++ b/complete.go @@ -2,6 +2,7 @@ package carapace import ( "github.com/rsteube/carapace/internal/config" + "github.com/rsteube/carapace/internal/shell/bash" "github.com/rsteube/carapace/internal/shell/nushell" "github.com/rsteube/carapace/pkg/ps" "github.com/spf13/cobra" @@ -16,9 +17,21 @@ func complete(cmd *cobra.Command, args []string) (string, error) { default: initHelpCompletion(cmd) - if shell := ps.DetermineShell(); shell == "nushell" { - args = nushell.Patch(args) + switch ps.DetermineShell() { + case "nushell": + args = nushell.Patch(args) // handle open quotes + LOG.Printf("patching args to %#v", args) + case "bash": // TODO what about oil and such? + var err error + args, err = bash.Patch(args) // handle redirects + LOG.Printf("patching args to %#v", args) + if _, ok := err.(bash.RedirectError); ok { + LOG.Printf("completing redirect target for %#v", args) + context := NewContext(args...) + return ActionFiles().Invoke(context).value(args[0], args[len(args)-1]), nil + } } + action, context := traverse(cmd, args[2:]) if err := config.Load(); err != nil { action = ActionMessage("failed to load config: " + err.Error()) diff --git a/example/cmd/_test/bash-ble.sh b/example/cmd/_test/bash-ble.sh index df7bf7ba..a41976c4 100644 --- a/example/cmd/_test/bash-ble.sh +++ b/example/cmd/_test/bash-ble.sh @@ -1,6 +1,7 @@ #!/bin/bash _example_completion() { export COMP_WORDBREAKS + export COMP_LINE local compline="${COMP_LINE:0:${COMP_POINT}}" local IFS=$'\n' diff --git a/example/cmd/_test/bash.sh b/example/cmd/_test/bash.sh index 5489de30..4c9f3383 100644 --- a/example/cmd/_test/bash.sh +++ b/example/cmd/_test/bash.sh @@ -1,6 +1,7 @@ #!/bin/bash _example_completion() { export COMP_WORDBREAKS + export COMP_LINE local compline="${COMP_LINE:0:${COMP_POINT}}" local IFS=$'\n' diff --git a/go.work.sum b/go.work.sum index 9a24310d..4ae81efc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -7,4 +7,6 @@ github.com/rsteube/carapace-pflag v0.1.0 h1:CPJRlj3jbyOnxuMf5pdrM76hEwdQ0STDDmkA github.com/rsteube/carapace-pflag v0.1.0/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/rsteube/carapace-shlex v0.0.1 h1:8uvsc+ISKw7uoITSp92nNisFUOulYMz+Uu7N5nbHTiM= github.com/rsteube/carapace-shlex v0.0.1/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= +github.com/rsteube/carapace-shlex v0.0.3 h1:QzcD31o9L4EK0ga9AxUU1QrfvfYb9TCdgOYUhpIstpQ= +github.com/rsteube/carapace-shlex v0.0.3/go.mod h1:zPw1dOFwvLPKStUy9g2BYKanI6bsQMATzDMYQQybo3o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= diff --git a/internal/shell/bash/patch.go b/internal/shell/bash/patch.go new file mode 100644 index 00000000..8b2fd44a --- /dev/null +++ b/internal/shell/bash/patch.go @@ -0,0 +1,49 @@ +package bash + +import ( + "os" + + shlex "github.com/rsteube/carapace-shlex" +) + +// RedirectError current position is a redirect like `echo test >[TAB]`. +type RedirectError struct{} + +func (r RedirectError) Error() string { + return "current position is a redirect like `echo test >[TAB]`" +} + +// Patch patches args if `COMP_LINE` environment variable is set. +// +// Bash passes redirects to the completion function so these need to be filtered out. +// +// `example action >/tmp/stdout.txt --values 2>/tmp/stderr.txt fi[TAB]` +// ["example", "action", ">", "/tmp/stdout.txt", "--values", "2", ">", "/tmp/stderr.txt", "fi"] +// ["example", "action", "--values", "fi"] +func Patch(args []string) ([]string, error) { // TODO document and fix wordbreak splitting (e.g. `:`) + compline, ok := os.LookupEnv("COMP_LINE") + if !ok { + return args, nil + } + + if err := os.Unsetenv("COMP_LINE"); err != nil { // prevent it being passes along to embedded completions + return nil, err + } + + if compline == "" { + return args, nil + } + + tokens, err := shlex.Split(compline) + if err != nil { + return nil, err + } + + if len(tokens) > 1 { + if previous := tokens[len(tokens)-2]; previous.WordbreakType.IsRedirect() { + return append(args[:1], tokens[len(tokens)-1].Value), RedirectError{} + } + } + args = append(args[:1], tokens.CurrentPipeline().FilterRedirects().Words().Strings()...) + return args, nil +} diff --git a/internal/shell/bash/snippet.go b/internal/shell/bash/snippet.go index ca30645a..8b1021f3 100644 --- a/internal/shell/bash/snippet.go +++ b/internal/shell/bash/snippet.go @@ -13,6 +13,7 @@ func Snippet(cmd *cobra.Command) string { result := fmt.Sprintf(`#!/bin/bash _%v_completion() { export COMP_WORDBREAKS + export COMP_LINE local compline="${COMP_LINE:0:${COMP_POINT}}" local IFS=$'\n'