TL;DR The following snippet added to you ~/.zshrc will recognize project folders you changed to, so that when you create new shells (i.e. through opening a terminal) it changes to the last used project automatically. Please find the complete snippet at the end of this post.

Some projects have a lot of processes. While there are tools for orchestrating the startup of applications that require multiple processes, sometimes it just more convenient to open terminals for each of those processes. But having opened multiple terminals, it would be cumbersome to have to change to the project's directory on each of those shells. And more generally, it would be nice to have a shell setup which is aware of the project I'm working on and thus could automatically change the directory to the current project as I spawn new terminals.

Let's make a list of what we need to make that happen:

  • a persistent storage
    • to store the location of the current project to
    • and to read the location of the current project from
  • a way to distinguish project directories from other directories
  • a hook that hooks into the event of changing directories
  • a way to automatically change the directory of a new shell
  • optionally, a way to revert the last recognition of a project

Storage

A flat file will do for storage. This file storage will be required throughout the upcoming code. Let's put it in a variable WD(for "working directory").

WD=~/.wd

We can easily save the current working directory:

pwd > $WD

And read it back:

CURRENT_PROJECT=`cat $WD`

But since we're planning to be able to revert to the last detected project, we'll actually use it as a stack, and thus instead of overwriting the file we'll append to it.

pwd >> $WD

And when reading from the stack instead of reading the whole file we'll just read the last line.

CURRENT_PROJECT=`tail -1 $WD`

So reading and writing to the storage is set, let's move on.

Project or Nonproject Directory

Distinguishing project from nonproject directories is a tricky one and might depend on the tools you're using. Since I'm using git in almost all of my projects, I settle with the presence of a .git directory as an indicator for a project directory.

if [[ -d .git ]]; then
  # ...
fi

If you are using other VCSs, you need to change that, obviously. A good indicator might also be project settings files that are written by your editor or IDE or dependency/project automation files (like Gemfile for Ruby, package.json for JavaScript or project.clj for Clojure).

Hooking into cd

Hooking into changing directories is fairly easy with zsh as it provides chpwd among its so called "Hook Functions". But it is a good practice to use add-zsh-hook, which lets you register multiple functions to a hook.

autoload -U add-zsh-hook

add-zsh-hook chpwd recognize-project

recognize-project is a function that we still need to write as of yet.

Other shells than zsh provide similar functionality. In some cases like bash you get away by wrapping the builtin cd command in a function, that call the builtin but also runs you own code.

Automate cd

Automatically changing to the last location stored is as easy as calling

cd `tail -1 $WD`

Adding this to you ~/.zshrc will run it automatically for each new shell. Just be aware that as long as ~/.wd is empty or doesn't exist this will throw an error.

In Practice

Putting it together:

#!/usr/bin/zsh

autoload -U add-zsh-hook

WD=~/.wd

recognize-project() {
  if [[ -d .git ]]; then
    pwd >> $WD
  fi
}

add-zsh-hook chpwd recognize-project

cd `tail -1 $WD`

Used in practice, this quickly reveals some weaknesses.

Undo

Sometimes, while working on project A we just want to have one shell in project B to look something up, but we quickly release that the location of project B has been stored when opening the next shell and we would like to have the means of undoing that. In that case we just need to remove the last line from the storage (pop the stack), read the location before that and change to it.

previous-project() {
  sed -i '$ d' $WD
  cd `tail -1 $WD`
}

alias pp=previous-project

I like to give functions expressive names, but I don't want to type these so I aliased previous-project here to pp.

Unique

Another weakness is that your our stack will quickly collect multiple consecutive equal lines. This is of no much use and in fact renders the just added undo feature useless. So to get rid of duplicate consecutive lines in our stack we'll use some sed magic:

sed -i '$!N; /^\(.*\)\n\1$/!P; D' $WD

This reads: If it's not the last line read the line and see if it is equal to the next line, if that is not the case print and in any case delete it. This will effectively remove duplicate consecutive lines and this keep our stack usable.

Finally

Ok, let's put everything together! This gives us the complete snippet:

#!/usr/bin/zsh

autoload -U add-zsh-hook

WD=~/.wd

recognize-project() {
  if [[ -d .git ]]; then
    pwd >> $WD
    # delete consecutive duplicate lines
    sed -i '$!N; /^\(.*\)\n\1$/!P; D' $WD
  fi
}

add-zsh-hook chpwd recognize-project

previous-project() {
  # delete last line
  sed -i '$ d' $WD
  cd `tail -1 $WD`
}

alias pp=previous-project

cd `tail -1 $WD`