junegunn / fzf

:cherry_blossom: A command-line fuzzy finder
https://junegunn.github.io/fzf/
MIT License
61.81k stars 2.35k forks source link

[BUG] Multi-line commands surrounded by parentheses not showing up in history #3881

Closed FeralHedgehog closed 2 weeks ago

FeralHedgehog commented 2 weeks ago

Checklist

Output of fzf --version

0.53.0 (b9d15569)

OS

Shell

Problem / Steps to reproduce

Hello, thank you for this awesome tool! I have many multi-line commands in my Zsh history in the following format:

(
echo do stuff
echo do more stuff
)

The surrounding parentheses are to avoid escaping every newline.

After upgrading to v0.53.0 on an Arch install, fzf's history has trouble finding and listing them, only showing the last run command in this format.

A simple way to reproduce: Run:

(
echo Hello
echo World
)

Then:

ls

And then:

(
echo 1337
echo h4x
)

Hitting Ctrl+r will show ls and the third command, but not the first one - even when searching for "World".

I compared /usr/share/fzf/key-bindings.zsh with v0.52.1 and it looks like the problem is in the new Perl regex in fzf-history-widget(), but I don't know Perl and couldn't find the issue.

Tested both on current Arch package 0.53.0 (c4a9ccd6) and current master 0.53.0 (b9d15569) with same result.

LangLangBart commented 2 weeks ago

Thanks for the report. I can reproduce it as well.

The issue lies in the if part of the perl command.

https://github.com/junegunn/fzf/blob/b9d15569e88d5abc5cfeaaf64c5aed3f95ee71da/shell/key-bindings.zsh#L115

The . (dot) only captures until it encounters a newline, so only the first line of every command is being checked for uniqueness.

perl -0 -ne 'if (/^\s*[0-9]+\**\s+(.*)/) { print "$1\n" }' /tmp/perl_test
(
ls
(

Adding (?s) changes the . (dot) behavior

perl -0 -ne 'if (/^\s*[0-9]+\**\s+((?s).*)/) { print "$1\n" }' /tmp/perl_test
(
echo 1337
echo h4x
)
ls
(
echo Hello
echo World
)
man 1 perlrecharclass | less --pattern "the dot"
     The dot
         The dot (or period), "." is probably the most used, and certainly the
         most well-known character class. By default, a dot matches any
         character, except for the newline. That default can be changed to add
         matching the newline by using the single line modifier: either for the
         entire regular expression with the "/s" modifier, or locally with
         "(?s)".  (The "\N" backslash sequence, described below, matches any
         character except newline without regard to the single line modifier.)

Could you test it ?

--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -113,5 +113,5 @@ fzf-history-widget() {
   if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
     selected="$(printf '%1$s\t%2$s\000' "${(vk)history[@]}" |
-      perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++) { s/\n/\n\t/gm; print; }' |
+      perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\s+((?s).*)/, $1)}++) { s/\n/\n\t/gm; print; }' |
       FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
       FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
FeralHedgehog commented 2 weeks ago

Thanks for the quick and detailed reply! With the fix all commands show up as they should.