Add execute-silent action

Close #823
This commit is contained in:
Junegunn Choi 2017-01-27 17:46:56 +09:00
parent ed57dcb924
commit 421b9b271a
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 93 additions and 55 deletions

View File

@ -8,6 +8,10 @@ CHANGELOG
- Placeholder expression used in `--preview` and `execute` action can - Placeholder expression used in `--preview` and `execute` action can
optionally take `+` flag to be used with multiple selections optionally take `+` flag to be used with multiple selections
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` - 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 0.16.2
------ ------

View File

@ -469,6 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \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) \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-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. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command fzf switches to the alternate screen when executing a command. However, if the
with the selected entries when multi-select is enabled (\fB--multi\fR). With process is expected to complete quickly, and you are not interested in its
this action, \fB{}\fR is replaced with the quoted strings of the selected output, you might want to use \fBexecute-silent\fR instead, which silently
entries separated by spaces. executes the command without switching.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@ -581,18 +581,25 @@ const (
escapedPlus = 2 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) { 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 { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if src[len(":execute")] == '-' { prefix := ":execute"
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" 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{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -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) errorExit("unknown action: " + spec)
} else { } else {
var offset int var offset int
if t == actExecuteMulti { switch t {
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi") offset = len("execute-multi")
} else { default:
offset = len("execute") offset = len("execute")
} }
if spec[offset] == ':' { if spec[offset] == ':' {
@ -752,23 +762,21 @@ func parseKeymap(keymap map[int][]action, str string) {
} }
func isExecuteAction(str string) actionType { func isExecuteAction(str string) actionType {
t := actExecute matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { if matches == nil || len(matches) != 1 {
return actIgnore return actIgnore
} }
prefix := matches[0][1]
b := str[len("execute")] if len(prefix) == 0 {
if strings.HasPrefix(str, "execute-multi") { prefix = matches[0][2]
if len(str) < len("execute-multi(") {
return actIgnore
}
t = actExecuteMulti
b = str[len("execute-multi")]
} }
e := str[len(str)-1] switch prefix {
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || case "execute":
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { return actExecute
return t case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
} }
return actIgnore return actIgnore
} }

View File

@ -204,7 +204,8 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti actExecuteSilent
actExecuteMulti // Deprecated
) )
func toActions(types ...actionType) []action { 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) valid, list := t.buildPlusList(template, forcePlus)
if !valid { if !valid {
return return
} }
command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin if !background {
cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout
t.tui.Pause() cmd.Stderr = os.Stderr
cmd.Run() t.tui.Pause()
if t.tui.Resume() { cmd.Run()
t.printAll() if t.tui.Resume() {
t.printAll()
}
t.refresh()
} else {
cmd.Run()
} }
t.refresh()
} }
func (t *Terminal) hasPreviewer() bool { func (t *Terminal) hasPreviewer() bool {
@ -1390,10 +1395,10 @@ func (t *Terminal) Loop() {
doAction = func(a action, mapkey int) bool { doAction = func(a action, mapkey int) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false) t.executeCommand(a.a, false, a.t == actExecuteSilent)
case actExecuteMulti: case actExecuteMulti:
t.executeCommand(a.a, true) t.executeCommand(a.a, true, false)
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false

View File

@ -14,8 +14,10 @@ func newItem(str string) *Item {
} }
func TestReplacePlaceholder(t *testing.T) { 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{ items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"), 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 // {}, 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'") check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, 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'") check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi // {..}, 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'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'") check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items // {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'") 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}/''") 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}/'' ''") 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 // String delimiter
delim := "'" 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'") check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter // Regex delimiter
regex := regexp.MustCompile("[oa]+") regex := regexp.MustCompile("[oa]+")
// foo'bar baz // 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'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }