Search code examples
bashcommand-linekey-bindings

How to bind a key to run a command and insert its output into the bash command line?


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:

  • The first line creates a binding for a shell command that sets a variable named selection to the value printed by the command date (or fzf or ranger).
  • The second line creates a binding for a readline function that does shell expansion, replacing variables with their values. (This is a default binding by the way, I only added this for completeness.)
  • The third line creates a binding for a macro that types the string "$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?


Solution

  • 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!