diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2024-04-27 18:36:37 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-27 18:36:37 +0900 |
commit | a4391aeedd4fec1865d2d646711f58d04058531b (patch) | |
tree | 73a6862010c323f380a3105f929b41a39c7a3753 /src/util | |
parent | b86a967ee217f4c820249701218a17eaad2737ae (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.go | 56 | ||||
-rw-r--r-- | src/util/util_windows.go | 106 |
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 |