Setting up the FileSystem

My key requirement for a file system layout is:

Minimize the time navigating the file system takes.

Obvious right?

There are a few implications of that basic rule.

  1. It should be instinctively obvious where any file in the system is (after an initial acclimatisation period).
  2. There should be minimal typing required to get to any directory
  3. The system should scale without additional effort.
  4. Directories used less frequently can be (slightly) harder to get to.


The Tools

My shell of choice is zsh, you may prefer bash or fish. There are some great features in zsh to help you get around the system.

Note: As a general rule fish has at least the same features as zsh, and bash has fewer. So anything covered here for zsh should exist in fish, and might have an equivalent in bash. zsh is 99% compatible with Bash, and thus has almost no breaking changes from the original Bourne Shell (/bin/sh), so it's a lot easier to use in parallel with other shells.

The autocd option

AUTO_CD example

This is a great zsh feature. If you type a string into the terminal that zsh doesn't recognise as a valid command, then it will try to parse it as a file path and cd to it.

This means instead of typing cd foo you can just type foo (as long as that's not also a command on your system, if it is just type foo/). It works for absolute paths and handles variables as well, so you can type ~/foo, /foo, or $XDG_CONFIG_HOME/foo, and they'll all work fine.

You can read more about it in the manual here, or in man zshoptions.


CDPATH example

When you pass cd a path that isn't an absolute path (starting with /), or a relative path (starting with ./ or ../), cd will assume it's a relative path starting from the current directory ($PWD). However you can also tell cd to look in other directories if it's not in the current directory, by setting the CDPATH variable. Like $PATH, $CDPATH is a colon-separated list of paths. However whereas $PATH is the list of paths to check for binaries, $CDPATH is the list of paths to check for arguments to cd.

Note that the $CDPATH variable is different in zsh and bash. In bash you need to include the current directory at the beginning, otherwise cd will prefer ~/foo to ./foo (which is rarely what you want in my experience). So I set CDPATH=~ in zsh, and CDPATH=:~ in bash.

It's worth emphasizing that the CDPATH only works for cd (surprisingly!), so you won't be able to take advantage of it for other operations like cp foo ~/tmp. This is one of the reasons I make sparing use of it, and only use it for ~. Adding an extra ~/ is only two extra characters, so it's not the end of the world. The Tab-Completion shortcuts I'll cover next work everywhere.

Tab-Complete for Paths

path tabcomplete example

This is the killer feature of zsh for me, and combined with the features above it really shines. This is easiest to explain with an example. If I want to get to ~/code/rust/xi-editor, I can just type c/r/x and then press Tab. If there is only one possible combination with those paths then it will directly complete to the full path, if there are multiple it will prompt you to choose. This means it's suddenly very easy to cd into deeply nested directories, and that in each directory it's best to have every subdirectory start with a different letter.

Run command on chpwd

chpwd ls example

Allows you to run arbitrary commands whenever you change a directory. I just use it to run ls -A every time I change directories, so I always know what's in the directory I'm in.

More info here.

History for cd

cd history example

I set these zsh options:

setopt autopushd autocd pushd_silent pushdignoredups pushdminus

Lots of detail here, but the key ones are autopushd and pushd_silent. autopushd makes cd work like pushd, which keeps a history of the directories you have cd'd to. You can go back by typing popd, or better by typing cd - Tab. This gives you an interactive menu for which directory you want to go to. See the picture below for more info.

pushd_silent suppresses pushd's annoying habit of printing the path it is cding to. However it doesn't stop the printing of the path that comes from $CDPATH. What this means specifically (with my $CDPATH setup) is that it prints the path whenever it's non-obvious where you're cding to, e.g. when foo takes you to ~/foo not ./foo. This is the ideal balance between explicit and minimalism in my (infallible) opinion.

Advanced Tab-Complete

Advanced Tab-Complete example

It's quite complex to get this stuff to work, but you can do some fancy things with the completion widget. I tell zsh to treat .,_- as completion boundaries, but much more advanced completion is possible.

Laying out your file system

Given these features, it's important to lay out your filesystem to take advantage of them. The first thing is to put everything in your home directory (~/), which is good practice in general.

You then probably want one directory to put your code in, which doesn't share a first letter with anything else in your home directory. I use ~/code for personal stuff, and ~/wrk for work stuff, but you can use anything.

Within those directories you want to keep the pre-tab-complete paths as short as possible, so to get to ~/code/node you only want to have to type c/nTab. If you also have ~/code/npm then it won't work, but if you have a ~/code/node-gyp it will (it completes to the shortest substring in all possible completions, which in this case is ~/code/node, which is what you want).

Basically if something you access rarely conflicts with something common, either change the name to something with a different first letter (that you can remember), or put it in a subdirectory (e.g. ~/code/other/npm), which can then be accessed quickly (c/o/n).


With all these features you should never need to spend more than a second typing out a path.