Add 'r' flag (raw) for unquoted output

By default, placeholder expressions are automatically quoted to ensure
they are safely passed as arguments to external programs.

The r flag ({r}, {r1}, etc.) disables this behavior, outputting the
evaluated value without quotes.

For example,

  echo 'foo   bar' | fzf --preview 'echo {} {r}'

The preview command becomes:

  echo 'foo   bar' foo   bar

Since `{r}` expands to unquoted "foo   bar", 'foo' and 'bar' are passed
as separate arguments.

**Use with caution** Unquoted output can lead to broken commands.

  echo "let's go" | fzf --preview 'echo {r}'

Close #4330
This commit is contained in:
Junegunn Choi 2025-03-30 19:28:21 +09:00
parent ba6d1b8772
commit 31fd207ba2
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
3 changed files with 21 additions and 4 deletions

View File

@ -768,6 +768,12 @@ e.g.
\fBfzf \-\-multi \-\-preview='head \-10 {+}'
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
Each expression expands to a quoted string, so that it's safe to pass it as an
argument to an external command. So you should not manually add quotes around
the curly braces. But if you don't want this behavior, you can put
\fBr\fR flag (raw) in the expression (e.g. \fB{r}\fR, \fB{r1}\fR, etc).
Use it with caution as unquoted output can lead to broken commands.
When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.

View File

@ -38,7 +38,7 @@ As such it is not useful for validation, but rather to generate test
cases for example.
\\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type
{\+?s?f?r?RANGE(?:,RANGE)*} # token type
{q[:s?RANGE]} # query type
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
)
@ -65,7 +65,7 @@ const maxFocusEvents = 10000
const blockDuration = 1 * time.Second
func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
placeholder = regexp.MustCompile(`\\?(?:{[+sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@ -628,6 +628,7 @@ type placeholderFlags struct {
number bool
forceUpdate bool
file bool
raw bool
}
type searchRequest struct {
@ -3782,6 +3783,8 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags.number = true
case 'f':
flags.file = true
case 'r':
flags.raw = true
case 'q':
flags.forceUpdate = true
trimmed += string(char)
@ -3933,7 +3936,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
return "''"
}
return strconv.Itoa(int(n))
case flags.file:
case flags.file || flags.raw:
return item.AsString(params.stripAnsi)
default:
return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
@ -3975,7 +3978,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
if !flags.preserveSpace {
str = strings.TrimSpace(str)
}
if !flags.file {
if !flags.file && !flags.raw {
str = params.executor.QuoteEntry(str)
}
return str

View File

@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {r}, strip ansi
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {r..}, strip ansi
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo foo'bar baz")
// {}, with multiple items
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")