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!

Discussion

royaso, 2015/09/29 09:29

great!

my bash function to bookmark the dir

bm(){
echo 0112wd0032>>~/.mybookmarks
}
Jochen, 2015/10/12 22:00

Good idea. I made some small changes:

Fuzzy search also in comments

In cdscuts_list_echo the line

cat $1 | sed 's/#.*//g' | sed '/^\s*$/d'

can be changed into

cat $1 | sed '/^\s*$/d'

This doesn't break any function (cd <directory> doesnt care about # and characters behind), only shows also the comments (and make them seekable in fzf).

Adding a directory to ~/.cdg_paths

cdg-add () {
        echo "${PWD} # $*" >> ~/.cdg_paths
}
simon, 2016/02/02 17:06

Maybe a little addition to cdg-add: do not allow the same directory multiple times:

cdg-add () {
    local curr_dir="${PWD} # $*"
    if ! grep -Fxq "$curr_dir" ~/.cdg_paths; then
        echo "$curr_dir" >> ~/.cdg_paths
    fi
}
simon, 2016/02/02 17:41

For the your first adaptation for cdscuts_list_echo: if you use this adaptation, also adapt the following line in cdg(): cd “$dest_dir” to cd $dest_dir (without the quotes). Because else it interprets the whole string with the # as new path, which gives an error.

Dmitry Frank, 2016/03/19 20:11

Simon, thanks for your comments! Sorry for not publishing them for so long; there must be some issue with email notifications on my side…

maz, 2015/10/24 13:04

Thank you, very usefull.

Dmitry Frank, 2015/10/26 20:44

Glad it helps, thanks for comment!

Jens Kohl, 2018/08/07 14:01

I glued it together with the awesome autojump tool like so:

unalias cdg 2> /dev/null
cdg() {
  # get the autopath directory list with the following command:
  # j -s | grep "^data:" | cut -d ' ' -f 2
  local autojump_list=/Users/USERNAME/Library/autojump/autojump.txt
  local dest_dir=$(cat $autojump_list | cut -d$'\t' -f 2 | fzf )
  if [[ $dest_dir != '' ]]; then
    echo $dest_dir
    cd "$dest_dir"
  fi
}
Tonus, 2022/03/09 01:00

Thank you very much for all this !

I did my changes to have all in the .bash_function_rc that I source in the traditionnal .bashrc

I tried to improve the “no dupes” and hability to match the comments. Might not be the cleaner code but thought it could be usable.

<blockquote>

# cdb introduce bookmarks in cli

function cdb_read() {

    system_wide_filelist=''
    user_filelist=''
    if [ -r /etc/cdb_paths ]; then
       system_wide_filelist=$(cat /etc/cdb_paths | sed '/^\s*$/d')
    fi
    if [ -r ~/.config/cdb_paths ]; then
       user_filelist=$(cat ~/.config/cdb_paths | sed '/^\s*$/d')
    fi

echo -e “$system_wide_filelist\n$user_filelist” | sed '/^\s*$/d'

}

function cdb-add () {

  if [ -z "$( cat ~/.config/cdb_paths | sed 's/#.*//' | sed 's/ $//' | grep -x "$PWD")" ]; then
      echo "$PWD" >> ~/.config/cdb_paths
      echo "$PWD added to bookmarks."
      else
echo "$PWD already bookmarked !"
  fi

}

unalias cdb 2> /dev/null

function cdb() {

# $CWD=$HOME

 local dest_dir=$(cdb_read | fzf )
 if [[ $dest_dir != '' ]]; then
    dest_dir=$(echo "$dest_dir" | sed 's/#.*//')
    cd $dest_dir
 fi

}

export -f cdb > /dev/null

</blockquote>

Nhát Cuồng, 2023/06/20 05:23

This works for me.

# Bookmarks for shell with fzf: https://dmitryfrank.com/articles/shell_shortcuts

# https://zzzcode.ai/answer-question?id=b4a52602-b06b-4694-9dc3-c300d1d5eb42

function cdg_read() {

  user_filelist=''
  if [ -r ~/.cdg_paths ]; then
     user_filelist=$(cat ~/.cdg_paths | sed '/^\s*$/d')
  fi
  echo -e "$user_filelist" | tr -d '"' | sed '/^\s*$/d'

}

function cdg-add () {

if [ -z "$( cat ~/.cdg_paths | sed 's/#.*//' | sed 's/ $//' | grep -x "$PWD")" ]; then
    echo "$PWD" >> ~/.cdg_paths
    echo "$PWD added to bookmarks."
else
  echo "$PWD already bookmarked !"
fi

}

unalias cdg 2> /dev/null

function cdg() {

echo "Reading directories from file..."
local dest_dir=$(cdg_read | fzf )
echo "Selected directory: \"$dest_dir\""
if [[ $dest_dir != '' ]]; then
  dest_dir=$(echo "$dest_dir" | sed 's/#.*//;s/^[[:space:]]*//;s/[[:space:]]*$//')
  if [[ -d "$dest_dir" ]]; then
    echo "Changing directory to: $dest_dir"
    builtin cd "$dest_dir"
  else
    echo "Directory does not exist: $dest_dir\""
  fi
fi

}

Enter your comment (please, English only). Wiki syntax is allowed:
  _   __  __  __   ____ __  __   _  __
 | | / / / / / /  / __/ \ \/ /  / |/ /
 | |/ / / /_/ /  / _/    \  /  /    / 
 |___/  \____/  /_/      /_/  /_/|_/
 
articles/shell_shortcuts.txt · Last modified: 2023/03/09 08:36 by dfrank
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0