Repository: rsteube/carapace (go)

Issue:
bash: redirects are  passed to completion function
### Current Behavior

Duh! Seems redirects are actually passed to the completion function in bash  so this needs to be handled as well.

### Expected Behavior

File completion for redirects

### Steps To Reproduce

```sh
echo test >[TAB]   # attached
echo test > [TAB]  # next arg
echo test <[TAB]   # attached
echo test < [TAB]  # next arg
echo test 1>[TAB]  # stdout
echo test 2>[TAB]  # stderr
echo test <>[TAB]  # both attached
echo test <> [TAB] # both next
````

### Version

0.42.0

### OS

- [X] Linux
- [ ] OSX
- [ ] Windows

### Shell

- [X] Bash
- [ ] Elvish
- [ ] Fish
- [ ] Nushell
- [ ] Oil
- [ ] Powershell
- [ ] Xonsh
- [ ] Zsh

### Anything else?

Prefix can be assumed for a quick solution but [carapace-shlex](https://github.com/rsteube/carapace-shlex)
 should be used to parse `COMP_LINE` to see that the redirect isn't quoted.
(Actually seems `COMP_WORDS` would also be an option, but for shlex needs to be able handle this for [Split](https://rsteube.github.io/carapace/carapace/action/split.html) anyway)

see https://www.gnu.org/software/bash/manual/html_node/Redirections.html

Pull request description:
bash: split COMP_LINE with shlex
fix #878 

Patch:
diff --git a/complete.go b/complete.go
index 75eed92..d03207b 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/go.work.sum b/go.work.sum
index 9a24310..4ae81ef 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 0000000..8b2fd44
--- /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 ca30645..8b1021f 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'


Test patch:
diff --git a/example/cmd/_test/bash-ble.sh b/example/cmd/_test/bash-ble.sh
index df7bf7b..a41976c 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 5489de3..4c9f338 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'