diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 200464aa..50de48e8 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -223,6 +223,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-multi(...)\fR (see below for the details) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR @@ -276,6 +277,12 @@ This is the special form that frees you from parse errors as it does not expect the closing character. The catch is that it should be the last one in the comma-separated list. .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 double-quoted strings of the +selected entries separated by spaces. + .RE .TP .BI "--history=" "HISTORY_FILE" diff --git a/src/options.go b/src/options.go index 42b27f34..d1f82014 100644 --- a/src/options.go +++ b/src/options.go @@ -466,10 +466,13 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" + if strings.HasPrefix(src, ":execute-multi") { + return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" + } + return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) @@ -565,11 +568,18 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b toggleSort = true default: if isExecuteAction(actLower) { - keymap[key] = actExecute - if act[7] == ':' { - execmap[key] = act[8:] + var offset int + if strings.HasPrefix(actLower, "execute-multi") { + keymap[key] = actExecuteMulti + offset = len("execute-multi") } else { - execmap[key] = act[8 : len(act)-1] + keymap[key] = actExecute + offset = len("execute") + } + if act[offset] == ':' { + execmap[key] = act[offset+1:] + } else { + execmap[key] = act[offset+1 : len(act)-1] } } else { errorExit("unknown action: " + act) @@ -580,10 +590,16 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b } func isExecuteAction(str string) bool { - if !strings.HasPrefix(str, "execute") || len(str) < 9 { + if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { return false } - b := str[7] + b := str[len("execute")] + if strings.HasPrefix(str, "execute-multi") { + if len(str) < len("execute-multi()") { + return false + } + b = str[len("execute-multi")] + } e := str[len(str)-1] if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { diff --git a/src/terminal.go b/src/terminal.go index eb82bd85..764459ff 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -132,6 +132,7 @@ const ( actPreviousHistory actNextHistory actExecute + actExecuteMulti ) func defaultKeymap() map[int]actionType { @@ -305,18 +306,22 @@ func (t *Terminal) output() bool { found = true } } else { - sels := make([]selectedItem, 0, len(t.selected)) - for _, sel := range t.selected { - sels = append(sels, sel) - } - sort.Sort(byTimeOrder(sels)) - for _, sel := range sels { + for _, sel := range t.sortSelected() { fmt.Println(*sel.text) } } return found } +func (t *Terminal) sortSelected() []selectedItem { + sels := make([]selectedItem, 0, len(t.selected)) + for _, sel := range t.selected { + sels = append(sels, sel) + } + sort.Sort(byTimeOrder(sels)) + return sels +} + func runeWidth(r rune, prefixWidth int) int { if r == '\t' { return 8 - prefixWidth%8 @@ -698,8 +703,12 @@ func keyMatch(key int, event C.Event) bool { return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ } -func executeCommand(template string, current string) { - command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1) +func quoteEntry(entry string) string { + return fmt.Sprintf("%q", entry) +} + +func executeCommand(template string, replacement string) { + command := strings.Replace(template, "{}", replacement, -1) cmd := exec.Command("sh", "-c", command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -858,7 +867,17 @@ func (t *Terminal) Loop() { case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) + executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) + } + case actExecuteMulti: + if len(t.selected) > 0 { + sels := make([]string, len(t.selected)) + for i, sel := range t.sortSelected() { + sels[i] = quoteEntry(*sel.text) + } + executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) + } else { + return doAction(actExecute, mapkey) } case actInvalid: t.mutex.Unlock() diff --git a/test/test_go.rb b/test/test_go.rb index 7143d369..e3b52747 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -713,6 +713,24 @@ class TestGoFZF < TestBase File.unlink output rescue nil end + def test_execute_multi + output = '/tmp/fzf-test-execute-multi' + opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"] + tmux.send_keys "seq 100 | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys :Escape, :a + tmux.send_keys :BTab, :BTab, :BTab + tmux.send_keys :Escape, :a + tmux.send_keys :Tab, :Tab + tmux.send_keys :Escape, :a + tmux.send_keys :Enter + readonce + assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], + File.readlines(output).map(&:chomp) + ensure + File.unlink output rescue nil + end + def test_cycle tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.until { |lines| lines[-2].include? '8/8' }