Add 'f' flag for placeholder expression (#1733)

If present the contents of the selection will be placed in a temporary file,
and the filename will be placed into the string instead.
This commit is contained in:
Simon Fraser 2019-10-27 14:50:12 +00:00 committed by Junegunn Choi
parent 0c6c76e081
commit 391669a451
4 changed files with 84 additions and 29 deletions

View File

@ -189,6 +189,7 @@ type Options struct {
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Printer func(string) Printer func(string)
PrintSep string
Sync bool Sync bool
History *History History *History
Header []string Header []string
@ -240,6 +241,7 @@ func defaultOptions() *Options {
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) }, Printer: func(str string) { fmt.Println(str) },
PrintSep: "\n",
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
@ -1106,8 +1108,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ReadZero = false opts.ReadZero = false
case "--print0": case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") } opts.Printer = func(str string) { fmt.Print(str, "\x00") }
opts.PrintSep = "\x00"
case "--no-print0": case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) } opts.Printer = func(str string) { fmt.Println(str) }
opts.PrintSep = "\n"
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":

View File

@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@ -22,9 +23,11 @@ import (
// import "github.com/pkg/profile" // import "github.com/pkg/profile"
var placeholder *regexp.Regexp var placeholder *regexp.Regexp
var activeTempFiles []string
func init() { func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q}|{\\+?n})") placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})")
activeTempFiles = []string{}
} }
type jumpMode int type jumpMode int
@ -103,6 +106,7 @@ type Terminal struct {
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
printer func(string) printer func(string)
printsep string
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
version int64 version int64
@ -231,6 +235,7 @@ type placeholderFlags struct {
preserveSpace bool preserveSpace bool
number bool number bool
query bool query bool
file bool
} }
func toActions(types ...actionType) []action { func toActions(types ...actionType) []action {
@ -407,6 +412,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
printer: opts.Printer, printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
@ -1207,6 +1213,9 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
case 'n': case 'n':
flags.number = true flags.number = true
skipChars++ skipChars++
case 'f':
flags.file = true
skipChars++
case 'q': case 'q':
flags.query = true flags.query = true
default: default:
@ -1232,7 +1241,27 @@ func hasPreviewFlags(template string) (plus bool, query bool) {
return return
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string { func writeTemporaryFile(data []string, printSep string) string {
f, err := ioutil.TempFile("", "fzf-preview-*")
if err != nil {
errorExit("Unable to create temporary file")
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
activeTempFiles = append(activeTempFiles, f.Name())
return f.Name()
}
func cleanTemporaryFiles() {
for _, filename := range activeTempFiles {
os.Remove(filename)
}
activeTempFiles = []string{}
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1] current := allItems[:1]
selected := allItems[1:] selected := allItems[1:]
if current[0] == nil { if current[0] == nil {
@ -1269,10 +1298,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
} else { } else {
replacements[idx] = strconv.Itoa(n) replacements[idx] = strconv.Itoa(n)
} }
} else if flags.file {
replacements[idx] = item.AsString(stripAnsi)
} else { } else {
replacements[idx] = quoteEntry(item.AsString(stripAnsi)) replacements[idx] = quoteEntry(item.AsString(stripAnsi))
} }
} }
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ") return strings.Join(replacements, " ")
} }
@ -1302,7 +1336,13 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
if !flags.preserveSpace { if !flags.preserveSpace {
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
} }
replacements[idx] = quoteEntry(str) if !flags.file {
str = quoteEntry(str)
}
replacements[idx] = str
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
} }
return strings.Join(replacements, " ") return strings.Join(replacements, " ")
}) })
@ -1319,7 +1359,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
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, t.printsep, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false) cmd := util.ExecCommand(command, false)
if !background { if !background {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@ -1335,6 +1375,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run() cmd.Run()
t.tui.Resume(false) t.tui.Resume(false)
} }
cleanTemporaryFiles()
} }
func (t *Terminal) hasPreviewer() bool { func (t *Terminal) hasPreviewer() bool {
@ -1492,7 +1533,7 @@ func (t *Terminal) Loop() {
// We don't display preview window if no match // We don't display preview window if no match
if request[0] != nil { if request[0] != nil {
command := replacePlaceholder(t.preview.command, command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, false, string(t.input), request) t.ansi, t.delimiter, t.printsep, false, string(t.input), request)
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if t.pwindow != nil { if t.pwindow != nil {
env := os.Environ() env := os.Environ()
@ -1534,6 +1575,7 @@ func (t *Terminal) Loop() {
if out.Len() > 0 || !<-updateChan { if out.Len() > 0 || !<-updateChan {
t.reqBox.Set(reqPreviewDisplay, out.String()) t.reqBox.Set(reqPreviewDisplay, out.String())
} }
cleanTemporaryFiles()
} else { } else {
t.reqBox.Set(reqPreviewDisplay, "") t.reqBox.Set(reqPreviewDisplay, "")
} }

View File

@ -30,92 +30,92 @@ func TestReplacePlaceholder(t *testing.T) {
t.Errorf("expected: %s, actual: %s", expected, result) t.Errorf("expected: %s, actual: %s", expected, result)
} }
} }
printsep := "\n"
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, 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{}, false, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, 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{}, false, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, 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{}, false, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, 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 {}{q}{}", true, Delimiter{}, false, "query 'string'", items2) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") 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) result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, 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{}, false, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") 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) result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, 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 // forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, 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}/'' ''") check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// Whitespace preserving flag with "'" delimiter // Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, false, "query", items1) result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'") check("echo ' foo'")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, false, "query", items1) result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo 'bar baz'") check("echo 'bar baz'")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, false, "query", items1) result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, false, "query", items1) result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// Whitespace preserving flag with regex delimiter // Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile("\\w+") regex = regexp.MustCompile("\\w+")
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, false, "query", items1) result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '") check("echo ' '")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, false, "query", items1) result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ''\\'''") check("echo ''\\'''")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, false, "query", items1) result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '") check("echo ' '")
// No match // No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'") check("echo /' foo'\\''bar baz'")
// String delimiter // String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, 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}, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }

View File

@ -1417,6 +1417,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') } tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') }
end end
def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar foobarfoo0101') }
end
def test_preview_q_no_match def test_preview_q_no_match
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter
tmux.until { |lines| lines.match_count == 0 } tmux.until { |lines| lines.match_count == 0 }