Fast Navigation in Terminal: Coming Full Circle

A small list of methods for fast file system navigation from the command line. While I tried all of them and finally settled on a minimal approach described at the end, the article doesn’t advocate for one method over the others. Different users have different needs and should choose the approach most suitable in their circumstances.

Spring

At the beginning, just after learning to use the command line, my terminal navigation included a lot of cd and a lot of ls. I jumped around folders, learned shortcuts like, cd ~, cd -, and started becoming familiar with the shell. Having multi-argument commands for quick file manipulation felt powerful and fresh, or so I remember.

After going to command line you no longer have to open folders in a graphical window manager, scan their names, use <ctrl + mouse click> to select them, and then drag the mouse to move those selected folders to another place. You do 'mv pattern* place/' instead. All is nice.

But there was one problem.

Summer

Along the way I started organizing my folders using a structure like this:

└── work
    ├── bitbucket
    │   └── user1
    │       ├── repo1
    │       └── repo2
    ├── github
    │   ├── user1
    │   │   ├── repo1
    │   │   └── repo2
    │   ├── user2
    │   │   ├── repo1
    │   │   └── repo2
    │   ├── user3
    │   │   ├── repo1
    │   │   ├── repo2
    │   │   ├── repo3
    │   │   └── repo4
    │   └── user4
    │       └── repo1
    ├── teaching
    │   ├── class1
    │   │   ├── lecture1
    │   │   └── lecture2
    │   └── class2
    │       ├── lecture1
    │       └── lecture2
    ├─── clients
    │   ├── client1
    │   │   ├── project1
    │   │   ├── project2
    │   │   └── project3
    │   ├── client2
    │   │   ├── project1
    │   │   ├── project2
    │   │   └── project3
    │   └── client3
    │      └── project1
    └─── ...

I often had to go in and out of various project folders and my cd + ls navigation became tedious. Reaching a project involved navigating a big tree of directories. On top of that a lot of directories had similar names which got in the way of tab completion. This is where I found “z” - a command line utility that tracks your most visited folders based on frequency and recency.

“z” keeps a database of your most frequently used folders and allows to quickly jump to these folders even if you don’t remember their full name. Instead of doing a lot of cd + <tab> all you have to do is type 'z proj' and z will take you to your most frequently/recently accessed folder that has proj somewhere in its name. It takes a short amount of time for “z” to learn about your most visited places but after that it becomes quick and convenient.

But there was one problem.

Autumn

“z” didn’t always take me where I wanted to be. It worked, I would say, around 95% of the time or more. But those other times it took me to an unrelated directory and distracted from the work at hand forcing to navigate my way back using the old cd. Moreover, sometimes I moved and renamed various folders which likely contributed to its disorientation.

After this experience I decided that the terminal should be dumb and deterministic without any fuzziness or smart guessing. And here I found marks - a short and sweet command line script for keeping manual directory bookmarks. It consisted of 4 tiny functions: one for creating a mark, one for removing, one for listing all the marks, and one for jumping to the bookmarked folder:

export MARKPATH=$HOME/.marks

function mark {
  mkdir -p "$MARKPATH"; ln -s "$(pwd)" "$MARKPATH/$1"
}

function unmark {
  rm -i "$MARKPATH/$1"
}

function marks {
  ls -l "$MARKPATH" | sed 's/  / /g' | cut -d' ' -f9- | sed 's/ -/\t-/g' && echo
}

function jump {
  cd -P "$MARKPATH/$1" 2>/dev/null || echo "No such mark: $1"
}

As well as a function generating completion suggestions after pressing <tab>:

_completemarks() {
  local curw=${COMP_WORDS[COMP_CWORD]}
  local wordlist=$(find $MARKPATH -type l -printf "%f\n")
    COMPREPLY=($(compgen -W '${wordlist[@]}' -- "$curw"))
    return 0
}

complete -F _completemarks jump unmark

The workflow of marks is manual but convenient. You have to go into the directory you want to bookmark and execute the mark command followed by the custom name, like 'mark myproject'. After that you can quickly jump back to this bookmarked folder using 'jump myproject' and when the bookmark is no longer relevant you can get rid of it with 'unmark myproject'.

With this approach the user is in control. There is no secret smart behaviour which means no auto-magic and no surprises. And after moving a project to another place the bookmark can be easily adjusted to point to its new location immediately, without waiting for a hidden automatic process to update its location, frequency, and recency.

But there was one problem.

Winter

Neither of those jump nor z commands could take me everywhere I needed to go. So the typical workflow would start with jumping to a project via the jump command but then switching to the old navigation via cd once inside it. Which meant that for navigating the file system I always had to keep using both: jump and cd. And having two distinct commands for doing the same thing felt a bit weird. This is where I learned about the $CDPATH variable and rolled out my own solution.

All you have to do is add this to your .bashrc:

export CDPATH=.:~/.marks/

Then, to add a bookmark called @name pointing to a "dir" directory:

ln -sr dir ~/.marks/@name

To delete a bookmark:

rm ~/.marks/@name

To jump to its location:

cd @name

And to list all available bookmarks:

cd @<tab>

The $CDPATH solution works best when all the bookmarks are started with a special symbol which in my case was @. This solves a couple of problems. One, the bookmarked directories, if prefixed with @ symbol, will not interfere with directories in the current folder. Second, all the available bookmarks will be displayed by typing 'cd @<tab>'. And third, all bookmarks are now part of cd and so they gain tab completion for free. You can even use tab completion for jumping directly to folders nested within bookmarks by doing 'cd @bookmark/subfolder/'.

Using this method you no longer have to maintain a short list of custom bookmark functions and their autocompletion in your .bashrc. And command for “change directory” can always be done with the same old and familiar cd command, independant of context.

But there was one problem.

Spring again

After working with the $CDPATH approach for a while I began noticing something peculiar. At no point in time did I have a list with a dozen or more bookmarks. Instead I found myself constantly going to the same 2 or 3 projects, finishing them, removing them from ~/.marks/ folder, adding new ones, and repeating the cycle again. Why would someone keep bookmarks for 3 directories?

This time, instead of changing the command, I tried to do something different and changed the folder structure. My projects moved from the state of being relevant to being archived so why not create an archive directory and organize folders based not on their name or type but on state? Thus the "zzz" folder was born. And now my project structure looks something like this:

└── work
    ├── active_project1
    ├── active_project2
    └── zzz
        ├── bitbucket
        │   └── user1
        │       ├── repo1
        │       └── repo2
        ├── github
        │   ├── user1
        │   │   ├── repo1
        │   │   └── repo2
        │   ├── user2
        │   │   ├── repo1
        │   │   └── repo2
        │   ├── user3
        │   │   ├── repo1
        │   │   ├── repo2
        │   │   ├── repo3
        │   │   └── repo4
        │   └── user4
        │       └── repo1
        ├── teaching
        │   ├── class1
        │   │   ├── lecture1
        │   │   └── lecture2
        │   └── class2
        │       ├── lecture1
        │       └── lecture2
        ├─── clients
        │   ├── client1
        │   │   ├── project1
        │   │   ├── project2
        │   │   └── project3
        │   ├── client2
        │   │   ├── project1
        │   │   ├── project2
        │   │   └── project3
        │   └── client3
        │      └── project1
        └─── ...

With this structure the bookmarking systems no longer provide big benefits as most frequent and recent folders are always under ~/work/some_project. And since at any timepoint only a few of them are visible the tab completion will do its job: 'cd ~/w<tab>/s<tab>'.

The name "zzz" might seem strange but is convenient: the letters “zzz” symbolically mark the projects within as being in a “sleeping” state while the 3 “z” letters in a row make sure that this folder is always placed at the very end of your active project list. A nice folder structure is still maintained in the archive but for the ease of access all active folders are layed flat under the ~/work/ directory. When the project is paused or completed it is moved to its place within the "zzz" hierarchy, maintaining the previous structure. There is one extra benefit - contents of the "work" directory act as a reminder about all the projects that are in progress. If necessary a to-do list with concrete details might be placed at the same level.

And just like that, like a newbie, I navigate the file system with cd and ls again.


  1. ◦  “z” by rupa on GitHub

  2. ◦  “Quickly navigate your filesystem from the command line” by Jeroen Janssens

  3. ◦  “Bourne shell variables” from the Bash manual