CDPATH
2020-05-30
Let’s say we have a directory somewhere that we access often; maybe the directory that contains all our projects, or the parent directory of a bunch of services. And that directory has an annoyingly long path, potentially buried deep in some GOPATH
, for example. To get there, we have to type something like
cd ~/go/src/github.com/bewuethr/awesome-project
every time. Unacceptable, right?
We can use an alias! That’s what I did for a long time. I added
alias cdap='cd "$HOME/go/src/github.com/bewuethr/awesome-project"'
to my ~/.bashrc
1, and was able to navigate like
cdap
$ pwd
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project
ls
$ bar baz foo go.mod go.sum
cd bar
$ pwd
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project/bar
Great!
CDPATH
I almost always want to access a subdirectory of awesome-project
, rarely the directory itself. Bash (or any POSIX conformant shell) has a shell variable called CDPATH
: a colon-separated list of directories for cd
to use as a search path.
So, instead of my alias, I’d stick this into my .bashrc
:
export CDPATH='/home/benjamin/go/src/github.com/bewuethr/awesome-project'
Now, from anywhere in my file system, I can just do this:
pwd
$ /path/to/some/random/directory
cd foo
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project/foo
And sure enough, Bash even helpfully tells me that it took me there! Excellent.
Or is it? Let’s assume I want to change into a directory foo
within that random directory:
pwd
$ /path/to/some/random/directory
ls
$ errcheck foo lint vet
cd foo
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project/foo
That took me to the foo
from CDPATH
! Definitely not what I wanted. I could do this instead:
cd ./foo
$ pwd
$ /path/to/some/random/directory/foo
But in reality, that’d more likely look like
cd foo # Oops, uses CDPATH!
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project/foo
cd - # Go to previous directory...
$ /path/to/some/random/directory
cd ./foo # Be explicit with ./ $
and gone is any advantage CDPATH
would have had over an alias. It tried using it like this for a while, but it got too annoying and I gave up, went back to my alias.
CDPATH
done rightUntil one day, I really wanted to figure out if there isn’t a better way. I took a closer look at the Bash manual; from the description of the cd
builtin:
If the shell variable
CDPATH
exists, it is used as a search path: each directory name inCDPATH
is searched for directory, with alternative directory names inCDPATH
separated by a colon (:
).
And a bit later:
If a non-empty directory name from
CDPATH
is used, or if-
is the first argument, and the directory change is successful, the absolute pathname of the new working directory is written to the standard output.
That was the first hint: “non-empty directory name”!
In the POSIX spec for cd
, we can read this:
Starting with the first pathname in the <colon>-separated pathnames of CDPATH […] if the pathname is null, test if the concatenation of dot, a <slash> character, and the operand names a directory.
Even more explicit. A null pathname in CDPATH
is replaced with ./
, which is exactly what I want. The fix is really a one character change:
export CDPATH=':/home/benjamin/go/src/github.com/bewuethr/awesome-project'
# └── this one!
This prepends a null directory to CDPATH
.
Now I can do this:
pwd
$ /path/to/some/random/directory
ls
$ errcheck foo lint vet
cd foo # Does cd ./foo
$ pwd
$ /path/to/some/random/directory/foo
ls
$ foo.go
cd foo # Uses non-null CDPATH path
$ /home/benjamin/go/src/github.com/bewuethr/awesome-project/foo
Much better!
As the POSIX spec tells us, CDPATH
has been around since the System V shell. And indeed, the manual for System V Release 3.5 has this to say:
A nice side effect when using the (pretty standard) bash-completion package is that it is fully CDPATH
aware (see the implementation of _cd
), so I can do
pwd
$ /home/benjamin/.vim
ls
$ after autoload bundle colors ftdetect
cd f<tab>
$ foo/ ftdetect/
and get both local and CDPATH
directories.
Really to a separate file sourced from my ~/.bashrc
, but I’m getting ahead of myself. The post about my (perfect) dotfile setup is still in the making.↩︎