summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md13
-rw-r--r--man/man1/fzf.114
-rw-r--r--src/core.go9
-rw-r--r--src/options.go10
-rw-r--r--src/reader.go7
-rw-r--r--src/reader_test.go3
-rw-r--r--src/terminal.go53
-rw-r--r--src/terminal_test.go4
-rw-r--r--src/terminal_unix.go19
-rw-r--r--src/terminal_windows.go26
-rw-r--r--src/util/util_unix.go56
-rw-r--r--src/util/util_windows.go106
-rwxr-xr-xtest/test_go.rb2
13 files changed, 194 insertions, 128 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51e9a603..920bceff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,10 +3,23 @@ CHANGELOG
0.51.0
------
+- Added `--with-shell` option to start child processes with a custom shell command and flags
+ ```sh
+ gem list | fzf --with-shell 'ruby -e' \
+ --preview 'pp Gem::Specification.find_by_name({1})' \
+ --bind 'ctrl-o:execute-silent:
+ spec = Gem::Specification.find_by_name({1})
+ [spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
+ system "open", _1
+ end
+ '
+ ```
- Added `change-multi` action for dynamically changing `--multi` option
- `change-multi` - enable multi-select mode with no limit
- `change-multi(NUM)` - enable multi-select mode with a limit
- `change-multi(0)` - disable multi-select mode
+- `become` action is now supported on Windows
+ - Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
- Bug fixes and improvements
0.50.0
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 458c6a5f..742fba5a 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -818,6 +818,16 @@ the finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
+.B "--with-shell=STR"
+Shell command and flags to start child processes with. On *nix Systems, the
+default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR.
+On Windows, the default value is \fBcmd /v:on/s/c\fR when \fB$SHELL\fR is not
+set.
+
+.RS
+e.g. \fBgem list | fzf --with-shell 'ruby -e' --preview 'pp Gem::Specification.find_by_name({1})'\fR
+.RE
+.TP
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method.
@@ -932,6 +942,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br
.BR 2 " Error"
.br
+.BR 127 " Invalid shell command for \fBbecome\fR action"
+.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION
@@ -1441,8 +1453,6 @@ call.
\fBfzf --bind "enter:become(vim {})"\fR
-\fBbecome(...)\fR is not supported on Windows.
-
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list
diff --git a/src/core.go b/src/core.go
index ec137698..14aa781f 100644
--- a/src/core.go
+++ b/src/core.go
@@ -121,13 +121,16 @@ func Run(opts *Options, version string, revision string) {
})
}
+ // Process executor
+ executor := util.NewExecutor(opts.WithShell)
+
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
- }, eventBox, opts.ReadZero, opts.Filter == nil)
+ }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}
@@ -178,7 +181,7 @@ func Run(opts *Options, version string, revision string) {
mutex.Unlock()
}
return false
- }, eventBox, opts.ReadZero, false)
+ }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else {
eventBox.Unwatch(EvtReadNew)
@@ -209,7 +212,7 @@ func Run(opts *Options, version string, revision string) {
go matcher.Loop()
// Terminal I/O
- terminal := NewTerminal(opts, eventBox)
+ terminal := NewTerminal(opts, eventBox, executor)
maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0
heightUnknown := opts.Height.auto
diff --git a/src/options.go b/src/options.go
index c8a3fa15..66e0554e 100644
--- a/src/options.go
+++ b/src/options.go
@@ -120,6 +120,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering
+ --with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe)
--version Display version information and exit
@@ -356,6 +357,7 @@ type Options struct {
Unicode bool
Ambidouble bool
Tabstop int
+ WithShell string
ListenAddr *listenAddress
Unsafe bool
ClearOnExit bool
@@ -1327,10 +1329,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
actions = append(actions, &action{t: t, a: actionArg})
}
switch t {
- case actBecome:
- if util.IsWindows() {
- exit("become action is not supported on Windows")
- }
case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow:
@@ -1957,6 +1955,8 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
+ case "--with-shell":
+ opts.WithShell = nextString(allArgs, &i, "shell command and flags required")
case "--listen", "--listen-unsafe":
given, str := optionalNextString(allArgs, &i)
addr := defaultListenAddr
@@ -2073,6 +2073,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Padding = parseMargin("padding", value)
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
+ } else if match, value := optString(arg, "--with-shell="); match {
+ opts.WithShell = value
} else if match, value := optString(arg, "--listen="); match {
addr, err := parseListenAddress(value)
if err != nil {
diff --git a/src/reader.go b/src/reader.go
index 82648a68..8fa864e7 100644
--- a/src/reader.go
+++ b/src/reader.go
@@ -18,6 +18,7 @@ import (
// Reader reads from command or standard input
type Reader struct {
pusher func([]byte) bool
+ executor *util.Executor
eventBox *util.EventBox
delimNil bool
event int32
@@ -30,8 +31,8 @@ type Reader struct {
}
// NewReader returns new Reader object
-func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
- return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
+func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
+ return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@@ -242,7 +243,7 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
- r.exec = util.ExecCommand(command, true)
+ r.exec = r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
}
diff --git a/src/reader_test.go b/src/reader_test.go
index bf06fd09..56f9a1b0 100644
--- a/src/reader_test.go
+++ b/src/reader_test.go
@@ -10,9 +10,10 @@ import (
func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
+ exec := util.NewExecutor("")
reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true },
- eb, false, true)
+ eb, exec, false, true)
reader.startEventPoller()
diff --git a/src/terminal.go b/src/terminal.go
index 25f30150..8d114e1b 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -7,7 +7,6 @@ import (
"io"
"math"
"os"
- "os/exec"
"os/signal"
"regexp"
"sort"
@@ -245,6 +244,7 @@ type Terminal struct {
listenUnsafe bool
borderShape tui.BorderShape
cleanExit bool
+ executor *util.Executor
paused bool
border tui.Window
window tui.Window
@@ -640,7 +640,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
}
// NewTerminal returns new Terminal object
-func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
+func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal {
input := trimQuery(opts.Query)
var delay time.Duration
if opts.Tac {
@@ -736,6 +736,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
previewLabel: nil,
previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit,
+ executor: executor,
paused: opts.Phony,
cycle: opts.Cycle,
headerVisible: true,
@@ -2522,6 +2523,7 @@ type replacePlaceholderParams struct {
allItems []*Item
lastAction actionType
prompt string
+ executor *util.Executor
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
@@ -2535,6 +2537,7 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
allItems: list,
lastAction: t.lastAction,
prompt: t.promptString,
+ executor: t.executor,
})
}
@@ -2595,7 +2598,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case escaped:
return match
case match == "{q}" || match == "{fzf:query}":
- return quoteEntry(params.query)
+ return params.executor.QuoteEntry(params.query)
case match == "{}":
replace = func(item *Item) string {
switch {
@@ -2608,13 +2611,13 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case flags.file:
return item.AsString(params.stripAnsi)
default:
- return quoteEntry(item.AsString(params.stripAnsi))
+ return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
}
}
case match == "{fzf:action}":
return params.lastAction.Name()
case match == "{fzf:prompt}":
- return quoteEntry(params.prompt)
+ return params.executor.QuoteEntry(params.prompt)
default:
// token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
@@ -2648,7 +2651,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
str = strings.TrimSpace(str)
}
if !flags.file {
- str = quoteEntry(str)
+ str = params.executor.QuoteEntry(str)
}
return str
}
@@ -2688,7 +2691,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
return line
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
- cmd := util.ExecCommand(command, false)
+ cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
if !background {
@@ -2965,7 +2968,7 @@ func (t *Terminal) Loop() {
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
- cmd := util.ExecCommand(command, true)
+ cmd := t.executor.ExecCommand(command, true)
env := t.environ()
if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
@@ -3372,27 +3375,21 @@ func (t *Terminal) Loop() {
valid, list := t.buildPlusList(a.a, false)
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
- shell := os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = "sh"
- }
- shellPath, err := exec.LookPath(shell)
- if err == nil {
- t.tui.Close()
- if t.history != nil {
- t.history.append(string(t.input))
- }
- /*
- FIXME: It is not at all clear why this is required.
- The following command will report 'not a tty', unless we open
- /dev/tty *twice* after closing the standard input for 'reload'
- in Reader.terminate().
- : | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
- */
- tui.TtyIn()
- util.SetStdin(tui.TtyIn())
- syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
+ t.tui.Close()
+ if t.history != nil {
+ t.history.append(string(t.input))
}
+
+ /*
+ FIXME: It is not at all clear why this is required.
+ The following command will report 'not a tty', unless we open
+ /dev/tty *twice* after closing the standard input for 'reload'
+ in Reader.terminate().
+
+ while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
+ */
+ tui.TtyIn()
+ t.executor.Become(tui.TtyIn(), t.environ(), command)
}
case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
diff --git a/src/terminal_test.go b/src/terminal_test.go
index e7d3e751..9fc53919 100644
--- a/src/terminal_test.go
+++ b/src/terminal_test.go
@@ -23,6 +23,7 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
+ executor: util.NewExecutor(""),
})
}
@@ -244,6 +245,7 @@ func TestQuoteEntry(t *testing.T) {
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes
+ exec := util.NewExecutor("")
if util.IsWindows() {
effectiveStyle = windowsStyle
@@ -278,7 +280,7 @@ func TestQuoteEntry(t *testing.T) {
}
for input, expected := range tests {
- escaped := quoteEntry(input)
+ escaped := exec.QuoteEntry(input)
expected = templateToString(expected, effectiveStyle)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
diff --git a/src/terminal_unix.go b/src/terminal_unix.go
index c7fa7f12..d0b00f2f 100644
--- a/src/terminal_unix.go
+++ b/src/terminal_unix.go
@@ -5,26 +5,11 @@ package fzf
import (
"os"
"os/signal"
- "strings"
"syscall"
"golang.org/x/sys/unix"
)
-var escaper *strings.Replacer
-
-func init() {
- tokens := strings.Split(os.Getenv("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("'", "'\\''")
- }
-}
-
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}
@@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}
-
-func quoteEntry(entry string) string {
- return "'" + escaper.Replace(entry) + "'"
-}
diff --git a/src/terminal_windows.go b/src/terminal_windows.go
index a1ea7a22..112cd68d 100644
--- a/src/terminal_windows.go
+++ b/src/terminal_windows.go
@@ -4,8 +4,6 @@ package fzf
import (
"os"
- "regexp"
- "strings"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}
-
-func quoteEntry(entry string) string {
- shell := os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = "cmd"
- }
-
- if strings.Contains(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(shell, "pwsh") || strings.Contains(shell, "powershell") {
- escaped := strings.Replace(entry, `"`, `\"`, -1)
- return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
- } else {
- return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
- }
-}
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
diff --git a/test/test_go.rb b/test/test_go.rb
index b08ac72b..f58b789e 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1974,7 +1974,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.item_count }
end
- def test_reload_should_terminate_stadard_input_stream
+ def test_reload_should_terminate_standard_input_stream
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
end