diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd6e9c..f8a5bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ CHANGELOG - Placeholder expression used in `--preview` and `execute` action can optionally take `+` flag to be used with multiple selections - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` +- Added `execute-silent` action for executing a command silently without + switching to the alternate screen. This is useful when the process is + short-lived and you're not interested in its output. + - e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'` 0.16.2 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index e4321ba..95d0380 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -469,6 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) + \fBexecute-silent(...)\fR (see below for the details) \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR @@ -538,10 +539,10 @@ the closing character. The catch is that it should be the last one in the comma-separated list of key-action pairs. .RE -\fBexecute-multi(...)\fR is an alternative action that executes the command -with the selected entries when multi-select is enabled (\fB--multi\fR). With -this action, \fB{}\fR is replaced with the quoted strings of the selected -entries separated by spaces. +fzf switches to the alternate screen when executing a command. However, if the +process is expected to complete quickly, and you are not interested in its +output, you might want to use \fBexecute-silent\fR instead, which silently +executes the command without switching. .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/src/options.go b/src/options.go index 9d8bb89..d29f8bf 100644 --- a/src/options.go +++ b/src/options.go @@ -581,18 +581,25 @@ const ( escapedPlus = 2 ) +func init() { + // Backreferences are not supported. + // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') + executeRegexp = regexp.MustCompile( + "(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") +} + func parseKeymap(keymap map[int][]action, str string) { - if executeRegexp == nil { - // Backreferences are not supported. - // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') - executeRegexp = regexp.MustCompile( - "(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") - } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - if src[len(":execute")] == '-' { - return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" + prefix := ":execute" + if src[len(prefix)] == '-' { + c := src[len(prefix)+1] + if c == 's' || c == 'S' { + prefix += "-silent" + } else { + prefix += "-multi" + } } - return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" + return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) @@ -728,9 +735,12 @@ func parseKeymap(keymap map[int][]action, str string) { errorExit("unknown action: " + spec) } else { var offset int - if t == actExecuteMulti { + switch t { + case actExecuteSilent: + offset = len("execute-silent") + case actExecuteMulti: offset = len("execute-multi") - } else { + default: offset = len("execute") } if spec[offset] == ':' { @@ -752,23 +762,21 @@ func parseKeymap(keymap map[int][]action, str string) { } func isExecuteAction(str string) actionType { - t := actExecute - if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { + matches := executeRegexp.FindAllStringSubmatch(":"+str, -1) + if matches == nil || len(matches) != 1 { return actIgnore } - - b := str[len("execute")] - if strings.HasPrefix(str, "execute-multi") { - if len(str) < len("execute-multi(") { - return actIgnore - } - t = actExecuteMulti - b = str[len("execute-multi")] + prefix := matches[0][1] + if len(prefix) == 0 { + prefix = matches[0][2] } - e := str[len(str)-1] - if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || - b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { - return t + switch prefix { + case "execute": + return actExecute + case "execute-silent": + return actExecuteSilent + case "execute-multi": + return actExecuteMulti } return actIgnore } diff --git a/src/terminal.go b/src/terminal.go index 43d21d8..ee678f5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -204,7 +204,8 @@ const ( actPreviousHistory actNextHistory actExecute - actExecuteMulti + actExecuteSilent + actExecuteMulti // Deprecated ) func toActions(types ...actionType) []action { @@ -1126,22 +1127,26 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo }) } -func (t *Terminal) executeCommand(template string, forcePlus bool) { +func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) { valid, list := t.buildPlusList(template, forcePlus) if !valid { return } command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) cmd := util.ExecCommand(command) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - t.tui.Pause() - cmd.Run() - if t.tui.Resume() { - t.printAll() + if !background { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + t.tui.Pause() + cmd.Run() + if t.tui.Resume() { + t.printAll() + } + t.refresh() + } else { + cmd.Run() } - t.refresh() } func (t *Terminal) hasPreviewer() bool { @@ -1390,10 +1395,10 @@ func (t *Terminal) Loop() { doAction = func(a action, mapkey int) bool { switch a.t { case actIgnore: - case actExecute: - t.executeCommand(a.a, false) + case actExecute, actExecuteSilent: + t.executeCommand(a.a, false, a.t == actExecuteSilent) case actExecuteMulti: - t.executeCommand(a.a, true) + t.executeCommand(a.a, true, false) case actInvalid: t.mutex.Unlock() return false diff --git a/src/terminal_test.go b/src/terminal_test.go index 5afafaa..41941ee 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -14,8 +14,10 @@ func newItem(str string) *Item { } func TestReplacePlaceholder(t *testing.T) { - items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} + item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") + items1 := []*Item{item1, item1} items2 := []*Item{ + newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} @@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) { } // {}, preserve ansi - result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") // {}, strip ansi - result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar baz'") // {}, with multiple items - result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) - check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") + result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2) + check("echo 'foo'\\''bar baz'") // {..}, strip leading whitespaces, preserve ansi - result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") // {..}, strip leading whitespaces, strip ansi - result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar baz'") // {q} - result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar baz' 'query'") // {q}, multiple items - result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2) + result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2) check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") - result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2) + check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") + + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2) + check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") + + result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") + // forcePlus + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2) + check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") + + // No match + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) + check("echo /") + + // No match, but with selections + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1}) + check("echo /' foo'\\''bar baz'") + // String delimiter delim := "'" - result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1) + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1) check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") // Regex delimiter regex := regexp.MustCompile("[oa]+") // foo'bar baz - result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1) + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") }