Repository: junegunn/fzf
Pull request #1733 (2019-10-24 20:05:40)

Add 'f' placeholder option for preview pane
If present the contents of the selection will be placed in a temporary file, and the filename will be placed into the string instead.

Addresses comments in #1719

junegunn: Thanks, let me finish up with a few changes and a test case. Please allow my commits to your branch.

https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork
srfraser: > Thanks, let me finish up with a few changes and a test case. Please allow my commits to your branch.

Already set! ;) Thanks!

junegunn: All looks good now. Is there anything else you want to address? I'll update the man page when I release the new version.
srfraser: It all looks good to me. Thank you for your good feedback, I've learned a lot

Patch:
diff --git a/src/options.go b/src/options.go
index 3449f3dc..bd2a038e 100644
--- a/src/options.go
+++ b/src/options.go
@@ -189,6 +189,7 @@ type Options struct {
 	PrintQuery  bool
 	ReadZero    bool
 	Printer     func(string)
+	PrintSep    string
 	Sync        bool
 	History     *History
 	Header      []string
@@ -240,6 +241,7 @@ func defaultOptions() *Options {
 		PrintQuery:  false,
 		ReadZero:    false,
 		Printer:     func(str string) { fmt.Println(str) },
+		PrintSep:    "\n",
 		Sync:        false,
 		History:     nil,
 		Header:      make([]string, 0),
@@ -1106,8 +1108,10 @@ func parseOptions(opts *Options, allArgs []string) {
 			opts.ReadZero = false
 		case "--print0":
 			opts.Printer = func(str string) { fmt.Print(str, "\x00") }
+			opts.PrintSep = "\x00"
 		case "--no-print0":
 			opts.Printer = func(str string) { fmt.Println(str) }
+			opts.PrintSep = "\n"
 		case "--print-query":
 			opts.PrintQuery = true
 		case "--no-print-query":
diff --git a/src/terminal.go b/src/terminal.go
index 1b44e141..bb29d1d5 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"os/signal"
 	"regexp"
@@ -22,9 +23,11 @@ import (
 // import "github.com/pkg/profile"
 
 var placeholder *regexp.Regexp
+var activeTempFiles []string
 
 func init() {
-	placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q}|{\\+?n})")
+	placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})")
+	activeTempFiles = []string{}
 }
 
 type jumpMode int
@@ -103,6 +106,7 @@ type Terminal struct {
 	jumping    jumpMode
 	jumpLabels string
 	printer    func(string)
+	printsep   string
 	merger     *Merger
 	selected   map[int32]selectedItem
 	version    int64
@@ -231,6 +235,7 @@ type placeholderFlags struct {
 	preserveSpace bool
 	number        bool
 	query         bool
+	file          bool
 }
 
 func toActions(types ...actionType) []action {
@@ -407,6 +412,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 		jumping:    jumpDisabled,
 		jumpLabels: opts.JumpLabels,
 		printer:    opts.Printer,
+		printsep:   opts.PrintSep,
 		merger:     EmptyMerger,
 		selected:   make(map[int32]selectedItem),
 		reqBox:     util.NewEventBox(),
@@ -1207,6 +1213,9 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
 		case 'n':
 			flags.number = true
 			skipChars++
+		case 'f':
+			flags.file = true
+			skipChars++
 		case 'q':
 			flags.query = true
 		default:
@@ -1232,7 +1241,27 @@ func hasPreviewFlags(template string) (plus bool, query bool) {
 	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]
 	selected := allItems[1:]
 	if current[0] == nil {
@@ -1269,10 +1298,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
 					} else {
 						replacements[idx] = strconv.Itoa(n)
 					}
+				} else if flags.file {
+					replacements[idx] = item.AsString(stripAnsi)
 				} else {
 					replacements[idx] = quoteEntry(item.AsString(stripAnsi))
 				}
 			}
+			if flags.file {
+				return writeTemporaryFile(replacements, printsep)
+			}
 			return strings.Join(replacements, " ")
 		}
 
@@ -1302,7 +1336,13 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
 			if !flags.preserveSpace {
 				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, " ")
 	})
@@ -1319,7 +1359,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
 	if !valid {
 		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)
 	if !background {
 		cmd.Stdin = os.Stdin
@@ -1335,6 +1375,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
 		cmd.Run()
 		t.tui.Resume(false)
 	}
+	cleanTemporaryFiles()
 }
 
 func (t *Terminal) hasPreviewer() bool {
@@ -1492,7 +1533,7 @@ func (t *Terminal) Loop() {
 				// We don't display preview window if no match
 				if request[0] != nil {
 					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)
 					if t.pwindow != nil {
 						env := os.Environ()
@@ -1534,6 +1575,7 @@ func (t *Terminal) Loop() {
 					if out.Len() > 0 || !<-updateChan {
 						t.reqBox.Set(reqPreviewDisplay, out.String())
 					}
+					cleanTemporaryFiles()
 				} else {
 					t.reqBox.Set(reqPreviewDisplay, "")
 				}


Test patch:
diff --git a/src/terminal_test.go b/src/terminal_test.go
index 62b20c45..8b828af4 100644
--- a/src/terminal_test.go
+++ b/src/terminal_test.go
@@ -30,92 +30,92 @@ func TestReplacePlaceholder(t *testing.T) {
 			t.Errorf("expected: %s, actual: %s", expected, result)
 		}
 	}
-
+	printsep := "\n"
 	// {}, 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'")
 
 	// {}, strip ansi
-	result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
+	result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
 	check("echo '  foo'\\''bar baz'")
 
 	// {}, 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'")
 
 	// {..}, 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'")
 
 	// {..}, 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'")
 
 	// {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'")
 
 	// {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'")
 
-	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'")
 
-	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}/''")
 
-	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}/''")
 
-	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}/'' ''")
 
 	// 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}/'' ''")
 
 	// 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'")
 
-	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'")
 
-	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'")
 
-	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'")
 
 	// Whitespace preserving flag with regex delimiter
 	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 '  '")
 
-	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 ''\\'''")
 
-	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 ' '")
 
 	// 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 /")
 
 	// 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'")
 
 	// 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'")
 
 	// Regex delimiter
 	regex = regexp.MustCompile("[oa]+")
 	// 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'")
 }
 
diff --git a/test/test_go.rb b/test/test_go.rb
index 4d3fa647..724d847c 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1417,6 +1417,15 @@ class TestGoFZF < TestBase
     tmux.until { |lines| lines[1].include?('{//1 10/1   10  /123//0 9}') }
   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
     tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter
     tmux.until { |lines| lines.match_count == 0 }