-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[fish] Fix for newlines and other characters #4334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
CTRL-T/ALT-C now works correctly when selecting files or directories that contain newlines in their names. When external commands defined by $FZF_CTRL_T_COMMAND/$FZF_ALT_C_COMMAND are used (for example the fd command with -0 switch), the --read0 option must also be set through $FZF_CTRL_T_OPTS/$FZF_ALT_C_OPTS.
Thank, I've been quite busy lately. Will review when I get some time. |
#4334 (comment) # Should display foo␊ echo -en "foo\n" | fzf --read0 --no-multi-line
Fixed in 0edb5d5.
Thanks, fixed in 9ffc2c7
It's very unlikely that fzf will see those characters, but why not? Sounds good to me. I'll think about it.
fzf manually processes tab characters to support a custom |
I've done some basic testing and the patch seems to work expected, thanks. That said, the changes are a bit dense for me since I'm not very familiar with fish scripting, so I’d like to take a bit more time to fully understand the details. |
Thanks for looking into this, and for the fixes. There is no rush, please take all the time you need, and let me know if any part of the code needs clarification. |
Could you share the test cases you used to validate the feature? The processing logic is fairly complex, so seeing some examples would help clarify the intent behind your code. One strange thing I noticed, |
This is a rewrite of __fzf_parse_commandline function, that fixes the following issues, when CTRL-T/ALT-C is used and current command line token contains: - Escaped newlines (\n): This never worked correctly, but after 282884a, the string would split, and the script would enter an infinite loop while trying to set $dir. - Escaped bell (\a, \cg), backspace (\b), form feed (\v, \cl), carriage return (\r), vertical tab (\v, \ck): walker-root would not set correctly for existing directories containing any of those characters. - Regular expression special characters (^, +, ? etc): $dir would not be be stripped from $fzf_query if it contained any of those characters. The lowest supported fish version is v3.1b. For optimal operation, the function uses more recent commands when supported by the running version. Specifically, for versions equal or newer than: - v3.2.0: Sets variables using PCRE2 capture groups of `string match --regex` when needing to preserve any trailing newlines and simultaneously omit the extra newline that is appended by `string collect -N`. - v3.5.0: Uses the builtin path command for path normalization, dirname extraction and existing directories check. - v4.0.0: Uses the --tokens-expanded option of commandline, for expansion and dealing with unbalanced quotes and incomplete escape sequences. It also uses the regex style of string-escape, to prepare variable contents for regex operations. This is not used in older versions, because they don't escape newlines.
The condition that strips Below, I give one example for each fix of the second commit:
ls foo\nbar # <ctrl-t>
mkdir foo\rbar
touch foo\rbar/baz
ls foo\rbar/ # <ctrl-t>
mkdir foo+bar
ls foo+bar/ # <ctrl-t> To make it easier to understand the code, I list below the function for fish v4+ only, and then explain what each part does and how it is adjusted for older versions: set -l fzf_query ''
set -l prefix ''
set -l dir '.'
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
string match -q -r -- '(?<prefix>^-[^\s=]+=)?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(commandline --current-token --tokens-expanded | string collect -N)
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
Please let me know if there are parts of the code that I should provide more information. |
Thank you for the detailed explanation. One suggestion. |
I like the suggestion, but what do you think of also adding some logic that disables the option prefix when cmd-a -ofilename # <ctrl-t> $prefix="-o" and $fzf_query="filename", as suggested
cmd-a -- -filename # <ctrl-t> $prefix="" and $fzf_query="-filename"
cmd-a -- -filename-a; cmd-b -ofilename-b # <ctrl-t> $prefix="-o" and $fzf_query="filename-b"
cmd-a -- -filename-a | cmd-b -ofilename-b # <ctrl-t> $prefix="-o" and $fzf_query="filename-b" It shouldn't need much code. |
Really? Then I'm all for it. |
Great! diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index 72b42079..fedc2c68 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -51,20 +51,26 @@ function fzf_key_bindings
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
+ set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
+ set -l -- prefix_regex ''
+
+ if string match -q -v -- '* -- *' (string sub -e (commandline -Cp) -- (commandline -p))
+ set -- prefix_regex '^-[^\s=]+=|^-[^\s]'
+ set -- match_regex '(?<prefix>'$prefix_regex')?'$match_regex
+ end
+
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
- string match -q -r -- '(?<prefix>^-[^\s=]+=)?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
- (commandline --current-token --tokens-expanded | string collect -N)
+ string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
- string match -q -r -- '(?<prefix>^-[^\s=]+=)?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
- (commandline --current-token --tokenize | string collect -N)
+ string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
- set -- prefix (string match -r -- '^-[^\s=]+=' $cl_token)
+ set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end |
Thanks. Please push a separate commit to this PR branch, and I'll merge without squashing. |
- Support single-letter options without = such as -fFILEPATH - fish v3.3.0 and newer: Disable option prefix if -- is preceded
I pushed the change, but modified the code for skipping the option prefix on |
These changes fix issues with newlines in names, and escaped newlines and other characters in command line (more details in commit messages). For the last fix, the
__fzf_parse_commandline
function is rewritten, which now takes advantage of commands/options present in newer fish versions, while maintaining compatibility with the oldest supported version, which is actually v3.1b. All fish versions were individually tested, and they all seem to work, even v3.1.1 despite the reported issues with eval. Dropping support for versions older than v3.2.0 would simplify the code a little, but I think it's better to keep it for now, because v3.1.2 is on Debian 11.I also have some observations relating the display of file/dir names by fzf:
--read0
option is not set, the  symbol is not shown for trailing newlines in file names, while it is shown for directory names (after 6fa8295, where the path separator is appended). I think it would be better to always show trailing newlines, so that filefoo
can be distinguished fromfoo\n
.foo\tbar
undistinguished fromfoo bar
(maybe it could be replaced by U+2409 ␉ instead, only for file/dir names).//
):fzf --walker{=dir,-root=/}