mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 03:19:01 +00:00
Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
This commit is contained in:
parent
b86a967ee2
commit
a4391aeedd
13
CHANGELOG.md
13
CHANGELOG.md
@ -3,10 +3,23 @@ CHANGELOG
|
|||||||
|
|
||||||
0.51.0
|
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
|
- Added `change-multi` action for dynamically changing `--multi` option
|
||||||
- `change-multi` - enable multi-select mode with no limit
|
- `change-multi` - enable multi-select mode with no limit
|
||||||
- `change-multi(NUM)` - enable multi-select mode with a limit
|
- `change-multi(NUM)` - enable multi-select mode with a limit
|
||||||
- `change-multi(0)` - disable multi-select mode
|
- `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
|
- Bug fixes and improvements
|
||||||
|
|
||||||
0.50.0
|
0.50.0
|
||||||
|
@ -818,6 +818,16 @@ the finder only after the input stream is complete.
|
|||||||
e.g. \fBfzf --multi | fzf --sync\fR
|
e.g. \fBfzf --multi | fzf --sync\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.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]"
|
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
|
||||||
Start HTTP server and listen on the given address. It allows external processes
|
Start HTTP server and listen on the given address. It allows external processes
|
||||||
to send actions to perform via POST method.
|
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
|
||||||
.BR 2 " Error"
|
.BR 2 " Error"
|
||||||
.br
|
.br
|
||||||
|
.BR 127 " Invalid shell command for \fBbecome\fR action"
|
||||||
|
.br
|
||||||
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
|
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
|
||||||
|
|
||||||
.SH FIELD INDEX EXPRESSION
|
.SH FIELD INDEX EXPRESSION
|
||||||
@ -1441,8 +1453,6 @@ call.
|
|||||||
|
|
||||||
\fBfzf --bind "enter:become(vim {})"\fR
|
\fBfzf --bind "enter:become(vim {})"\fR
|
||||||
|
|
||||||
\fBbecome(...)\fR is not supported on Windows.
|
|
||||||
|
|
||||||
.SS RELOAD INPUT
|
.SS RELOAD INPUT
|
||||||
|
|
||||||
\fBreload(...)\fR action is used to dynamically update the input list
|
\fBreload(...)\fR action is used to dynamically update the input list
|
||||||
|
@ -121,13 +121,16 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process executor
|
||||||
|
executor := util.NewExecutor(opts.WithShell)
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||||
var reader *Reader
|
var reader *Reader
|
||||||
if !streamingFilter {
|
if !streamingFilter {
|
||||||
reader = NewReader(func(data []byte) bool {
|
reader = NewReader(func(data []byte) bool {
|
||||||
return chunkList.Push(data)
|
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)
|
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +181,7 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, eventBox, opts.ReadZero, false)
|
}, eventBox, executor, opts.ReadZero, false)
|
||||||
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||||
} else {
|
} else {
|
||||||
eventBox.Unwatch(EvtReadNew)
|
eventBox.Unwatch(EvtReadNew)
|
||||||
@ -209,7 +212,7 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
go matcher.Loop()
|
go matcher.Loop()
|
||||||
|
|
||||||
// Terminal I/O
|
// Terminal I/O
|
||||||
terminal := NewTerminal(opts, eventBox)
|
terminal := NewTerminal(opts, eventBox, executor)
|
||||||
maxFit := 0 // Maximum number of items that can fit on screen
|
maxFit := 0 // Maximum number of items that can fit on screen
|
||||||
padHeight := 0
|
padHeight := 0
|
||||||
heightUnknown := opts.Height.auto
|
heightUnknown := opts.Height.auto
|
||||||
|
@ -120,6 +120,7 @@ const usage = `usage: fzf [options]
|
|||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
--print0 Print output delimited by ASCII NUL characters
|
--print0 Print output delimited by ASCII NUL characters
|
||||||
--sync Synchronous search for multi-staged filtering
|
--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 /)
|
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||||
(To allow remote process execution, use --listen-unsafe)
|
(To allow remote process execution, use --listen-unsafe)
|
||||||
--version Display version information and exit
|
--version Display version information and exit
|
||||||
@ -356,6 +357,7 @@ type Options struct {
|
|||||||
Unicode bool
|
Unicode bool
|
||||||
Ambidouble bool
|
Ambidouble bool
|
||||||
Tabstop int
|
Tabstop int
|
||||||
|
WithShell string
|
||||||
ListenAddr *listenAddress
|
ListenAddr *listenAddress
|
||||||
Unsafe bool
|
Unsafe bool
|
||||||
ClearOnExit bool
|
ClearOnExit bool
|
||||||
@ -1327,10 +1329,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
actions = append(actions, &action{t: t, a: actionArg})
|
actions = append(actions, &action{t: t, a: actionArg})
|
||||||
}
|
}
|
||||||
switch t {
|
switch t {
|
||||||
case actBecome:
|
|
||||||
if util.IsWindows() {
|
|
||||||
exit("become action is not supported on Windows")
|
|
||||||
}
|
|
||||||
case actUnbind, actRebind:
|
case actUnbind, actRebind:
|
||||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||||
case actChangePreviewWindow:
|
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)"))
|
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||||
case "--tabstop":
|
case "--tabstop":
|
||||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
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":
|
case "--listen", "--listen-unsafe":
|
||||||
given, str := optionalNextString(allArgs, &i)
|
given, str := optionalNextString(allArgs, &i)
|
||||||
addr := defaultListenAddr
|
addr := defaultListenAddr
|
||||||
@ -2073,6 +2073,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Padding = parseMargin("padding", value)
|
opts.Padding = parseMargin("padding", value)
|
||||||
} else if match, value := optString(arg, "--tabstop="); match {
|
} else if match, value := optString(arg, "--tabstop="); match {
|
||||||
opts.Tabstop = atoi(value)
|
opts.Tabstop = atoi(value)
|
||||||
|
} else if match, value := optString(arg, "--with-shell="); match {
|
||||||
|
opts.WithShell = value
|
||||||
} else if match, value := optString(arg, "--listen="); match {
|
} else if match, value := optString(arg, "--listen="); match {
|
||||||
addr, err := parseListenAddress(value)
|
addr, err := parseListenAddress(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
// Reader reads from command or standard input
|
// Reader reads from command or standard input
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
pusher func([]byte) bool
|
pusher func([]byte) bool
|
||||||
|
executor *util.Executor
|
||||||
eventBox *util.EventBox
|
eventBox *util.EventBox
|
||||||
delimNil bool
|
delimNil bool
|
||||||
event int32
|
event int32
|
||||||
@ -30,8 +31,8 @@ type Reader struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewReader returns new Reader object
|
// NewReader returns new Reader object
|
||||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
|
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||||
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) startEventPoller() {
|
func (r *Reader) startEventPoller() {
|
||||||
@ -242,7 +243,7 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
|
|||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
r.killed = false
|
r.killed = false
|
||||||
r.command = &command
|
r.command = &command
|
||||||
r.exec = util.ExecCommand(command, true)
|
r.exec = r.executor.ExecCommand(command, true)
|
||||||
if environ != nil {
|
if environ != nil {
|
||||||
r.exec.Env = environ
|
r.exec.Env = environ
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ import (
|
|||||||
func TestReadFromCommand(t *testing.T) {
|
func TestReadFromCommand(t *testing.T) {
|
||||||
strs := []string{}
|
strs := []string{}
|
||||||
eb := util.NewEventBox()
|
eb := util.NewEventBox()
|
||||||
|
exec := util.NewExecutor("")
|
||||||
reader := NewReader(
|
reader := NewReader(
|
||||||
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
||||||
eb, false, true)
|
eb, exec, false, true)
|
||||||
|
|
||||||
reader.startEventPoller()
|
reader.startEventPoller()
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@ -245,6 +244,7 @@ type Terminal struct {
|
|||||||
listenUnsafe bool
|
listenUnsafe bool
|
||||||
borderShape tui.BorderShape
|
borderShape tui.BorderShape
|
||||||
cleanExit bool
|
cleanExit bool
|
||||||
|
executor *util.Executor
|
||||||
paused bool
|
paused bool
|
||||||
border tui.Window
|
border tui.Window
|
||||||
window tui.Window
|
window tui.Window
|
||||||
@ -640,7 +640,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewTerminal returns new Terminal object
|
// 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)
|
input := trimQuery(opts.Query)
|
||||||
var delay time.Duration
|
var delay time.Duration
|
||||||
if opts.Tac {
|
if opts.Tac {
|
||||||
@ -736,6 +736,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
previewLabel: nil,
|
previewLabel: nil,
|
||||||
previewLabelOpts: opts.PreviewLabel,
|
previewLabelOpts: opts.PreviewLabel,
|
||||||
cleanExit: opts.ClearOnExit,
|
cleanExit: opts.ClearOnExit,
|
||||||
|
executor: executor,
|
||||||
paused: opts.Phony,
|
paused: opts.Phony,
|
||||||
cycle: opts.Cycle,
|
cycle: opts.Cycle,
|
||||||
headerVisible: true,
|
headerVisible: true,
|
||||||
@ -2522,6 +2523,7 @@ type replacePlaceholderParams struct {
|
|||||||
allItems []*Item
|
allItems []*Item
|
||||||
lastAction actionType
|
lastAction actionType
|
||||||
prompt string
|
prompt string
|
||||||
|
executor *util.Executor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
|
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,
|
allItems: list,
|
||||||
lastAction: t.lastAction,
|
lastAction: t.lastAction,
|
||||||
prompt: t.promptString,
|
prompt: t.promptString,
|
||||||
|
executor: t.executor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2595,7 +2598,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
|||||||
case escaped:
|
case escaped:
|
||||||
return match
|
return match
|
||||||
case match == "{q}" || match == "{fzf:query}":
|
case match == "{q}" || match == "{fzf:query}":
|
||||||
return quoteEntry(params.query)
|
return params.executor.QuoteEntry(params.query)
|
||||||
case match == "{}":
|
case match == "{}":
|
||||||
replace = func(item *Item) string {
|
replace = func(item *Item) string {
|
||||||
switch {
|
switch {
|
||||||
@ -2608,13 +2611,13 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
|||||||
case flags.file:
|
case flags.file:
|
||||||
return item.AsString(params.stripAnsi)
|
return item.AsString(params.stripAnsi)
|
||||||
default:
|
default:
|
||||||
return quoteEntry(item.AsString(params.stripAnsi))
|
return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case match == "{fzf:action}":
|
case match == "{fzf:action}":
|
||||||
return params.lastAction.Name()
|
return params.lastAction.Name()
|
||||||
case match == "{fzf:prompt}":
|
case match == "{fzf:prompt}":
|
||||||
return quoteEntry(params.prompt)
|
return params.executor.QuoteEntry(params.prompt)
|
||||||
default:
|
default:
|
||||||
// token type and also failover (below)
|
// token type and also failover (below)
|
||||||
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||||
@ -2648,7 +2651,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
|||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
}
|
}
|
||||||
if !flags.file {
|
if !flags.file {
|
||||||
str = quoteEntry(str)
|
str = params.executor.QuoteEntry(str)
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
@ -2688,7 +2691,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
|||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||||
cmd := util.ExecCommand(command, false)
|
cmd := t.executor.ExecCommand(command, false)
|
||||||
cmd.Env = t.environ()
|
cmd.Env = t.environ()
|
||||||
t.executing.Set(true)
|
t.executing.Set(true)
|
||||||
if !background {
|
if !background {
|
||||||
@ -2965,7 +2968,7 @@ func (t *Terminal) Loop() {
|
|||||||
if items[0] != nil {
|
if items[0] != nil {
|
||||||
_, query := t.Input()
|
_, query := t.Input()
|
||||||
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
||||||
cmd := util.ExecCommand(command, true)
|
cmd := t.executor.ExecCommand(command, true)
|
||||||
env := t.environ()
|
env := t.environ()
|
||||||
if pwindowSize.Lines > 0 {
|
if pwindowSize.Lines > 0 {
|
||||||
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
|
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
|
||||||
@ -3372,27 +3375,21 @@ func (t *Terminal) Loop() {
|
|||||||
valid, list := t.buildPlusList(a.a, false)
|
valid, list := t.buildPlusList(a.a, false)
|
||||||
if valid {
|
if valid {
|
||||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
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()
|
t.tui.Close()
|
||||||
if t.history != nil {
|
if t.history != nil {
|
||||||
t.history.append(string(t.input))
|
t.history.append(string(t.input))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FIXME: It is not at all clear why this is required.
|
FIXME: It is not at all clear why this is required.
|
||||||
The following command will report 'not a tty', unless we open
|
The following command will report 'not a tty', unless we open
|
||||||
/dev/tty *twice* after closing the standard input for 'reload'
|
/dev/tty *twice* after closing the standard input for 'reload'
|
||||||
in Reader.terminate().
|
in Reader.terminate().
|
||||||
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
|
|
||||||
|
while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
|
||||||
*/
|
*/
|
||||||
tui.TtyIn()
|
tui.TtyIn()
|
||||||
util.SetStdin(tui.TtyIn())
|
t.executor.Become(tui.TtyIn(), t.environ(), command)
|
||||||
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case actExecute, actExecuteSilent:
|
case actExecute, actExecuteSilent:
|
||||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
||||||
|
@ -23,6 +23,7 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
|||||||
allItems: allItems,
|
allItems: allItems,
|
||||||
lastAction: actBackwardDeleteCharEof,
|
lastAction: actBackwardDeleteCharEof,
|
||||||
prompt: "prompt",
|
prompt: "prompt",
|
||||||
|
executor: util.NewExecutor(""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +245,7 @@ func TestQuoteEntry(t *testing.T) {
|
|||||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||||
var effectiveStyle quotes
|
var effectiveStyle quotes
|
||||||
|
exec := util.NewExecutor("")
|
||||||
|
|
||||||
if util.IsWindows() {
|
if util.IsWindows() {
|
||||||
effectiveStyle = windowsStyle
|
effectiveStyle = windowsStyle
|
||||||
@ -278,7 +280,7 @@ func TestQuoteEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for input, expected := range tests {
|
for input, expected := range tests {
|
||||||
escaped := quoteEntry(input)
|
escaped := exec.QuoteEntry(input)
|
||||||
expected = templateToString(expected, effectiveStyle)
|
expected = templateToString(expected, effectiveStyle)
|
||||||
if escaped != expected {
|
if escaped != expected {
|
||||||
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||||
|
@ -5,26 +5,11 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"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) {
|
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||||
signal.Notify(resizeChan, syscall.SIGWINCH)
|
signal.Notify(resizeChan, syscall.SIGWINCH)
|
||||||
}
|
}
|
||||||
@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
|
|||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||||
}
|
}
|
||||||
|
|
||||||
func quoteEntry(entry string) string {
|
|
||||||
return "'" + escaper.Replace(entry) + "'"
|
|
||||||
}
|
|
||||||
|
@ -4,8 +4,6 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||||
@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
|
|||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
// NOOP
|
// 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) + "'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -3,31 +3,71 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes the given command with $SHELL
|
type Executor struct {
|
||||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
shell string
|
||||||
|
args []string
|
||||||
|
escaper *strings.Replacer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecutor(withShell string) *Executor {
|
||||||
shell := os.Getenv("SHELL")
|
shell := os.Getenv("SHELL")
|
||||||
|
args := strings.Fields(withShell)
|
||||||
|
if len(args) > 0 {
|
||||||
|
shell = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
} else {
|
||||||
if len(shell) == 0 {
|
if len(shell) == 0 {
|
||||||
shell = "sh"
|
shell = "sh"
|
||||||
}
|
}
|
||||||
return ExecCommandWith(shell, command, setpgid)
|
args = []string{"-c"}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
cmd := exec.Command(shell, "-c", command)
|
cmd := exec.Command(x.shell, append(x.args, command)...)
|
||||||
if setpgid {
|
if setpgid {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
}
|
}
|
||||||
return cmd
|
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
|
// KillCommand kills the process for the given command
|
||||||
func KillCommand(cmd *exec.Cmd) error {
|
func KillCommand(cmd *exec.Cmd) error {
|
||||||
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||||
|
@ -6,60 +6,102 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"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
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||||
var shell string
|
// can kill preview process with its child processes at once.
|
||||||
if cached := shellPath.Load(); cached != nil {
|
// 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)
|
shell = cached.(string)
|
||||||
} else {
|
} else {
|
||||||
shell = os.Getenv("SHELL")
|
if strings.Contains(shell, "/") {
|
||||||
if len(shell) == 0 {
|
|
||||||
shell = "cmd"
|
|
||||||
} else if strings.Contains(shell, "/") {
|
|
||||||
out, err := exec.Command("cygpath", "-w", shell).Output()
|
out, err := exec.Command("cygpath", "-w", shell).Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
shell = strings.Trim(string(out), "\n")
|
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
|
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
|
||||||
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
cmd := x.ExecCommand(command, false)
|
||||||
// can kill preview process with its child processes at once.
|
cmd.Stdin = stdin
|
||||||
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
cmd.Stdout = os.Stdout
|
||||||
// but it is left as is now because no adverse effect has been observed.
|
cmd.Stderr = os.Stderr
|
||||||
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
cmd.Env = environ
|
||||||
var cmd *exec.Cmd
|
err := cmd.Start()
|
||||||
if strings.Contains(shell, "cmd") {
|
if err != nil {
|
||||||
cmd = exec.Command(shell)
|
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
Exit(127)
|
||||||
HideWindow: false,
|
|
||||||
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
|
||||||
CreationFlags: 0,
|
|
||||||
}
|
}
|
||||||
return cmd
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
Exit(exitError.ExitCode())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
func (x *Executor) QuoteEntry(entry string) string {
|
||||||
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
|
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 {
|
} else {
|
||||||
cmd = exec.Command(shell, "-c", command)
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
}
|
}
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
HideWindow: false,
|
|
||||||
CreationFlags: 0,
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KillCommand kills the process for the given command
|
// KillCommand kills the process for the given command
|
||||||
|
@ -1974,7 +1974,7 @@ class TestGoFZF < TestBase
|
|||||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
end
|
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.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 }
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user