Managing project-specific configurations in ZSH

A short post outlining how I implement project-specific shell configurations in Linux.

zsh quality of life

Featured image

I recently started working on the RTEMS kernel as my GSoC project which requires me to source some shell parameters to configure the workspace accordingly. A pretty common pattern to do this in software development is to create a new shell script which is sourced manually every time the project is worked upon.

The benefits of this approach, as compared to storing everything in the global shell config (~/.zshrc or ~/.bashrc), is modularity. As any sane Linux user would have it, I prefer not to pollute my shell environment with parameters I won’t need when I’m not working on the project. But one major drawback of doing this is that you have to manually source the config every time you open up a new shell as follows:

source <project-dir >/source.sh

Now, I have a pretty dynamic workflow, which involves spawning and closing the terminal emulator a whole bunch of times, and having to do this manually each time gets pretty annoying when you’re developing software. So let’s fix that, shall we?

Thinking of the solution #

zsh is my shell of choice and it supports adding hooks . Now, hooks are pretty awesome in the fact that they allow us to run functions based on certain events. The event of our interest is chpwd, which allows us to run a function every time the directory changes.

With the shell-specific details out of the way, we can move ahead to working out the implementation details of the hook callback itself. What I ended up with was an “upward search” approach. It involves reserving a name for the shell script to be sourced, .source.sh in my case, and to keep searching each directory up the file path until the file is found, or we land at ~. This allows the shell config to be sourced even if we land in a subdirectory of the project, which is a basic requirement for me. git also uses the same approach for determining the project root directory.

# Look for the project config by traversing up the path
function source-local-config() {
    local file=".source.sh"
    local parent="$PWD"

    while [[ "$parent" != "$HOME" ]]; do
        if [[ -f "$parent/$file" && -r "$parent/$file" ]]; then
            source "$parent/$file"
            return
        fi

        parent="${parent%/*}"
    done
}

It does not make sense to have ~/.source.sh as keeping those configurations in ~/.zshrc would be better.

Finalizing #

With the callback logic implemented, all we need to do is to define it in ~/.zshrc, as follows:

# Run once on startup and then add hook for directory change
source-local-config
autoload -U add-zsh-hook
add-zsh-hook chpwd source-local-config

Note that I also invoke the function on startup in addition to registering the hook. This is to handle the edge case where I might open up my terminal emulator directly in a project. Since the chpwd event only registers when we invoke cd, that is why we have this startup invocation to handle the shell spawning edge case as well.

That concludes the zsh setup. To add a project-specific config, I simply create a .source.sh file in the folder similar to the following:

#!/bin/sh

export RTEMS_DIR="$(realpath "$(dirname "$0")")"
export RTEMS_PREFIX="$RTEMS_DIR/build/v6"

export PATH="$RTEMS_PREFIX/bin:\
$RTEMS_DIR/rsb/source-builder:\
$PATH"

I have to admit this simple setup is a huge quality of life improvement for me, and I hope it helps you as well.

My .zshrc actually resides in ~/.config/zsh/ as I’ve set my $ZDOTDIR to $XDG_CONFIG_HOME/zsh according to the XDG spec. Therefore, the more apt choice would be to use $ZDOTDIR/.zshrc instead of ~/.zshrc.



Comments

Nothing yet.

Leave a reply