There are various command line utilities that allow interactively choosing a file and then print the user's selection to stdout, such as fzf or ranger. This feature allows for example running the cat "$(fzf)"
command to first choose a file and then display its contents. However, a downside of doing this is that the command is stored in its unexpanded form in the command history and therefore it is not possible to see what file was chosen or to run it again. Additionally, there is no way to double-check the exact command that will be run, which would be especially beneficial with multiple arguments, for example in cp "$(fzf)" "$(fzf)"
.
To get around this issue, I would like to bind a hotkey to open a file picker and insert the selected filename directly into the command line being edited.
This is the best I could come up with (here I use date
instead of fzf
or ranger
for a demonstration because it does not need to be installed so everyone can try):
bind -x '"\e\C-s":"selection=$(date)"'
bind '"\e\C-e": shell-expand-line'
bind '"\ef":" \"$selection\"\e\C-s\e\C-e"'
Example usage: Type echo
then press Alt-F. The command line becomes echo Sun Jul 30 19:55:16 CEST 2023
.
Explanation:
selection
to the value printed by the command date
(or fzf
or ranger
). "$selection"
into the command line then presses the hot keys for the first two bindings, thereby setting a value for the selection
variable and replacing it with that value.What I don't like in this solution:
Three separate bindings are necessary, because a single binding can only contain either a shell command, a readline command or a keypress macro.
If a shell variable named selection
is used by the user, it gets overwritten. (This could be mitigated by picking a less frequent name. It would still pollute the variable space though, unless I add one more binding to unset it.)
It has the undesired side effect that the shell expansion is applied to the entire command line, not just the $selection
variable reference.
The most severe problem though is that the inserted filename is not properly escaped, so if it contains special characters, the command line will be incorrect and potentially dangerous. (In the bindings above, I enclosed the $selection
variable reference in quotes. I hoped that this would result in echo "Sun Jul 30 19:55:16 CEST 2023"
, or echo Sun\ Jul\ 30\ 19:55:16\ CEST\ 2023
, but it does not.)
Is there a better way?
I think,instead of using multiple bindings and a shell variable, better to use a custom shell function, so first of all define a shell function in your shell configuration file ~/.bashrc
!
pick_file() {
local file
file=$(fzf --preview "cat {}")
if [ -n "$file" ]; then
file=$(printf "%q" "$file") #for escape special characters in the filename
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$file${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$((READLINE_POINT + ${#file}))
fi
}
then bind a hotkey to the pick_file
like this :
bind -x '"\C-x\C-f": pick_file'
reload your shell configuration like this:
source ~/.bashrc
now,easily press the assigned hotkey(Ctrl+X
and Ctrl+F
) to open the file picker, and after selecting a file, it will be inserted at the current cursor position in the command line!