Fuzzy bookmarks for your shell

I do a lot of things in shell, and I'd like to have an easy and convenient way to keep a list of shortcuts/bookmarks. I wasn't satisfied by all of the existing solutions I found, but eventually I've come across a great, universal tool: command-line fuzzy finder by Junegunn Choi. Great thanks to Junegunn for that shell-life-changing tool.

It primarily allows you to “fuzzy-find” files (check the rich gif animation by the link above), but it also allows to feed arbitrary text data to it and filter this data. So, the shortcuts idea is simple: all we need is to maintain a file with paths (which are shortcuts), and fuzzy-filter this file. Here's how it looks: we type cdg command (from “cd global”, if you like), get a list of our bookmarks, pick the needed one in just a few keystrokes, and press Enter. Working directory is changed to the picked item:

So, the first step is to install the aforementioned fzf: go to its page and follow its installation procedure.

After that, we need to have a simple mechanism for feeding our bookmarks to fzf. There are definitely much more than one way to do that, so you can come up with your better ideas, but I've done it in this rather simple way:

It's probably convenient to have two files with bookmarks: the system-wide one, and the second one for each user. Format of these files is like this:

/path/to/first_bookmark    # probably some comment
/path/to/second_bookmark
 
/path/to/third_bookmark

First of all, we create helper script that echoes contents of given file with comments and empty lines stripped. Let's name it cdscuts_list_echo and put to /usr/bin:

/usr/bin/cdscuts_list_echo
#!/bin/bash
 
cat $1 | sed 's/#.*//g' | sed '/^\s*$/d'

And then make it executable:

$ sudo chmod a+x /usr/bin/cdscuts_list_echo

Now, let's write script /usr/bin/cdscuts_glob_echo that reads predefined files with bookmarks and echoes their contents together:

/usr/bin/cdscuts_glob_echo
#!/bin/bash
 
system_wide_filelist=''
user_filelist=''
 
if [ -r /etc/cdg_paths ]; then
   system_wide_filelist=$(cdscuts_list_echo /etc/cdg_paths)
fi
if [ -r ~/.cdg_paths ]; then
   user_filelist=$(cdscuts_list_echo ~/.cdg_paths)
fi
 
echo -e "$system_wide_filelist\n$user_filelist" | sed '/^\s*$/d'

As you see, the system-wide bookmarks are stored in /etc/cdg_paths, and user's bookmarks are in ~/.cdg_paths. Again, mark this script as executable:

$ sudo chmod a+x /usr/bin/cdscuts_glob_echo

Now, we can actually create our bookmark file ~/.cdg_paths with the following test contents:

/path/to/first_bookmark    # probably some comment
/path/to/second_bookmark
 
/path/to/third_bookmark

And check that cdscuts_glob_echo successfully echoes it without comments and empty lines:

$ cdscuts_glob_echo ~/.cdg_paths
/path/to/first_bookmark    
/path/to/second_bookmark
/path/to/third_bookmark

And if we feed that to fzf, we can already fuzzy-find in that list! Try it:

$ cdscuts_glob_echo ~/.cdg_paths | fzf

But, well, when we actually pick an item from the list, it is just printed on the console. It's surely very nice, but we would like to cd to it instead.

The cd operation cannot be implemented in standalone script (because each process has its own working directory, and therefore cd is a built-in shell command). So, what we need is to write a shell function. Let's name it cdg. For bash, open your ~/.bashrc (and for zsh, open your ~/.zshrc), and add the following:

# Setup cdg function
# ------------------
unalias cdg 2> /dev/null
cdg() {
   local dest_dir=$(cdscuts_glob_echo | fzf )
   if [[ $dest_dir != '' ]]; then
      cd "$dest_dir"
   fi
}
export -f cdg > /dev/null

Then, open new shell session.

And that's it! Now, you can type:

$ cdg

Then pick an item from your list, and when you select it (by pressing Enter), your shell will try to change directory there. You can now fill your ~/.cdg_paths with real paths and enjoy it.

You can find a lot of interesting stuff you can do with fzf, check its page for details; but the very minimum you should know for bookmarks is that you can type not only letters to filter the list, but also arrows to navigate currently displayed list. And, if you're like me, you don't like to leave the home row, so, fzf provides vim-like keybingins as well: Ctrl+j behaves like down arrow, and Ctrl+k behaves like up arrow.

Have fun!