Matthew Bennett

Logo

I am a data scientist working on time series forecasting (using R and Python 3) at the London Ambulance Service NHS Trust. I earned my PhD in cognitive neuroscience at the University of Glasgow working with fmri data and neural networks. I favour linux machines, and working in the terminal with Vim as my editor of choice.

View my GitHub Profile

View my LinkedIn Profile

View my CV

Using FZF to select files for any program or command

The full code I’ve written so far can be found here.

I use fzf all the time in the terminal. One very useful feature is to search for and select files to be passed to some command (e.g. cat) or program (e.g. vlc). This is especially useful when fzf is configured to always search a set of default directories scattered around the file system, described in the previous post.

The Goal

Here I create a bash function to automate this procedure. The function is called with the command or program as an argument. The function launches fzf, you select your file(s) and hit enter. The selected files are passed to the command/program. Apart from being a little easier to type than 'vlc $(fzf)', the function returns control of the terminal to the user (e.g. when opening GUIs). The full command that was run after expansion will appear in your history just like any other.

Here we open a couple of python files in vim (the two marked with red circles), from two separate directories, and neither of which is in the current working directory. Note how few characters is needed to locate the files with fzf:

Loading multiple files in vim

The command is found in our history in a way we could re-execute (note that I wrapped the lines here so it would fit in the image, in reality the command appears as a single line:

The history looks good

A really useful case is to change directory to somewhere far away in the file system:

changing to a far away directory

Again, the command is found in our history in a way we could re-execute:

The history looks good

The usage is like this:
f cd [OPTION]... (hit enter, choose path)
f cat [OPTION]... (hit enter, choose files)
f vim [OPTION]... (hit enter, choose files)
f vlc [OPTION]... (hit enter, choose files)

The Code Implementation

The general method is to construct a command in ~/.bash_history taking the form: program + options + arguments. We use fzf to supply the file names which we'll use as arguments. Then we simply execute that command.

First we deal with the case of no arguments by just launching fzf and sorting any files that are selected:

#!/bin/bash

f() {

    # if no arguments passed, just lauch fzf
    if [ $# -eq 0 ]
    then
        fzf | sort
        return 0
    fi

Second we store the first argument as the program and shift it off the argument list. Any remaining arguments are taken as options to the program:

    program="$1"

    shift

    options="$@"

We launch fzf with the possibility of selecting multiple items and collect the arguments and store them in a variable. If no files are selected (e.g. if Esc pressed), we just return to terminal:

arguments=$(fzf --multi)

if [ -z "${arguments}" ]; then
    return 1
fi

Next we store the arguments passed to our program in a temporary variable and sanitise them for entry into ~/.bash_history. Specifically, we use sed to put all input arguments on one line and wrap each one with single quotes, also we put an extra single quote next to any pre-existing single quotes in the raw arguments (e.g. badly named files). This has the effect that the quote in the argument itself is respected as such:

for arg in "${arguments[@]}"; do
    arguments=$(echo "$arg" | sed "s/'/''/g; s/.*/'&'/g; s/\n//g")
done

In general, we want to launch GUI programs as background jobs so that we can still enter other commands into the terminal as they run. Non-GUI commands that run in the terminal such as 'vim', 'cat', 'head' etc should be run as foreground jobs (otherwise we won't see them). If the program is on the list of GUI programs, we append '&' on the end of the command:

if [[ "$program" =~ ^(nautilus|zathura|evince|vlc|eog|kolourpaint)$ ]]; then
    arguments="$arguments &"
fi

The command typed into the terminal could for example be 'f vlc'. The function will expand that into 'vlc file1.mp3 file2.mp3 &' and we want that command to show up in our bash history, rather than just seeing 'f vlc'. So we first write the shell's active history to the ~/.bash_history file. Then we append the sanitised commands to the ~/.bash_history, then reload the contents of the ~/.bash_history as our active history:

history -w

echo $program $options $arguments >> ~/.bash_history

history -r

Finally, we execute the command that we placed in ~/.bash_history:

fc -s -1
}

The full code I’ve written so far can be found here.

< Configuring FZF to search useful directories beyond the current working directory

back to home