summaryrefslogtreecommitdiffstats
path: root/src/util
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-04-27 18:36:37 +0900
committerGitHub <noreply@github.com>2024-04-27 18:36:37 +0900
commita4391aeedd4fec1865d2d646711f58d04058531b (patch)
tree73a6862010c323f380a3105f929b41a39c7a3753 /src/util
parentb86a967ee217f4c820249701218a17eaad2737ae (diff)
Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
Diffstat (limited to 'src/util')
-rw-r--r--src/util/util_unix.go56
-rw-r--r--src/util/util_windows.go106
2 files changed, 122 insertions, 40 deletions
diff --git a/src/util/util_unix.go b/src/util/util_unix.go
index 2991fd2c..4410a9bf 100644
--- a/src/util/util_unix.go
+++ b/src/util/util_unix.go
@@ -3,31 +3,71 @@
package util
import (
+ "fmt"
"os"
"os/exec"
+ "strings"
"syscall"
"golang.org/x/sys/unix"
)
-// ExecCommand executes the given command with $SHELL
-func ExecCommand(command string, setpgid bool) *exec.Cmd {
+type Executor struct {
+ shell string
+ args []string
+ escaper *strings.Replacer
+}
+
+func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = "sh"
+ args := strings.Fields(withShell)
+ if len(args) > 0 {
+ shell = args[0]
+ args = args[1:]
+ } else {
+ if len(shell) == 0 {
+ shell = "sh"
+ }
+ args = []string{"-c"}
}
- return ExecCommandWith(shell, command, setpgid)
+
+ var escaper *strings.Replacer
+ tokens := strings.Split(shell, "/")
+ if tokens[len(tokens)-1] == "fish" {
+ // https://fishshell.com/docs/current/language.html#quotes
+ // > The only meaningful escape sequences in single quotes are \', which
+ // > escapes a single quote and \\, which escapes the backslash symbol.
+ escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
+ } else {
+ escaper = strings.NewReplacer("'", "'\\''")
+ }
+ return &Executor{shell, args, escaper}
}
-// ExecCommandWith executes the given command with the specified shell
-func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
- cmd := exec.Command(shell, "-c", command)
+// ExecCommand executes the given command with $SHELL
+func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
+ cmd := exec.Command(x.shell, append(x.args, command)...)
if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
return cmd
}
+func (x *Executor) QuoteEntry(entry string) string {
+ return "'" + x.escaper.Replace(entry) + "'"
+}
+
+func (x *Executor) Become(stdin *os.File, environ []string, command string) {
+ shellPath, err := exec.LookPath(x.shell)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
+ Exit(127)
+ }
+ args := append([]string{shellPath}, append(x.args, command)...)
+ SetStdin(stdin)
+ syscall.Exec(shellPath, args, environ)
+}
+
// KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
diff --git a/src/util/util_windows.go b/src/util/util_windows.go
index aa69b99d..cbaa8ce0 100644
--- a/src/util/util_windows.go
+++ b/src/util/util_windows.go
@@ -6,60 +6,102 @@ import (
"fmt"
"os"
"os/exec"
+ "regexp"
"strings"
"sync/atomic"
"syscall"
)
-var shellPath atomic.Value
+type Executor struct {
+ shell string
+ args []string
+ shellPath atomic.Value
+}
+
+func NewExecutor(withShell string) *Executor {
+ shell := os.Getenv("SHELL")
+ args := strings.Fields(withShell)
+ if len(args) > 0 {
+ shell = args[0]
+ } else if len(shell) == 0 {
+ shell = "cmd"
+ }
+
+ if len(args) > 0 {
+ args = args[1:]
+ } else if strings.Contains(shell, "cmd") {
+ args = []string{"/v:on/s/c"}
+ } else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
+ args = []string{"-NoProfile", "-Command"}
+ } else {
+ args = []string{"-c"}
+ }
+ return &Executor{shell: shell, args: args}
+}
// ExecCommand executes the given command with $SHELL
-func ExecCommand(command string, setpgid bool) *exec.Cmd {
- var shell string
- if cached := shellPath.Load(); cached != nil {
+// FIXME: setpgid is unused. We set it in the Unix implementation so that we
+// can kill preview process with its child processes at once.
+// NOTE: For "powershell", we should ideally set output encoding to UTF8,
+// but it is left as is now because no adverse effect has been observed.
+func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
+ shell := x.shell
+ if cached := x.shellPath.Load(); cached != nil {
shell = cached.(string)
} else {
- shell = os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = "cmd"
- } else if strings.Contains(shell, "/") {
+ if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
shell = strings.Trim(string(out), "\n")
}
}
- shellPath.Store(shell)
+ x.shellPath.Store(shell)
}
- return ExecCommandWith(shell, command, setpgid)
+ cmd := exec.Command(shell, append(x.args, command)...)
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ HideWindow: false,
+ CreationFlags: 0,
+ }
+ return cmd
}
-// ExecCommandWith executes the given command with the specified shell
-// FIXME: setpgid is unused. We set it in the Unix implementation so that we
-// can kill preview process with its child processes at once.
-// NOTE: For "powershell", we should ideally set output encoding to UTF8,
-// but it is left as is now because no adverse effect has been observed.
-func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
- var cmd *exec.Cmd
- if strings.Contains(shell, "cmd") {
- cmd = exec.Command(shell)
- cmd.SysProcAttr = &syscall.SysProcAttr{
- HideWindow: false,
- CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
- CreationFlags: 0,
+func (x *Executor) Become(stdin *os.File, environ []string, command string) {
+ cmd := x.ExecCommand(command, false)
+ cmd.Stdin = stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Env = environ
+ err := cmd.Start()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
+ Exit(127)
+ }
+ err = cmd.Wait()
+ if err != nil {
+ if exitError, ok := err.(*exec.ExitError); ok {
+ Exit(exitError.ExitCode())
}
- return cmd
}
+ Exit(0)
+}
- if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
- cmd = exec.Command(shell, "-NoProfile", "-Command", command)
+func (x *Executor) QuoteEntry(entry string) string {
+ if strings.Contains(x.shell, "cmd") {
+ // backslash escaping is done here for applications
+ // (see ripgrep test case in terminal_test.go#TestWindowsCommands)
+ escaped := strings.Replace(entry, `\`, `\\`, -1)
+ escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
+ // caret is the escape character for cmd shell
+ r, _ := regexp.Compile(`[&|<>()@^%!"]`)
+ return r.ReplaceAllStringFunc(escaped, func(match string) string {
+ return "^" + match
+ })
+ } else if strings.Contains(x.shell, "pwsh") || strings.Contains(x.shell, "powershell") {
+ escaped := strings.Replace(entry, `"`, `\"`, -1)
+ return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
- cmd = exec.Command(shell, "-c", command)
- }
- cmd.SysProcAttr = &syscall.SysProcAttr{
- HideWindow: false,
- CreationFlags: 0,
+ return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
- return cmd
}
// KillCommand kills the process for the given command