There are little habits or structures you can add to your day which improve your productivity significantly over time. In the beginning, these changes may seem profound; yet over time you find yourself relying on them implicitly.

For me, both a Linux and command line junkie, one such change is the decision to auto-reload my bashrc whenever there are changes to that file, in all of my open terminals. The profundity is that I no longer need to interrupt my workflow to access a new alias, function or environment variable tweak: I no longer need to either close and reopen my terminal or paste the same code a dozen places for each tweak I want to make!

I don’t find myself tweaking my bashrc all that often; but when I do, I need to make a million tweak to get it just right; and, ultimately, all of this tweaking is a distraction from the real work I have to get done.

Now, I have the confidence to iterate quickly, with minimal fuss; then get around to using those changes without having to rebuild my tmux sessions one-by-one.

Here’s the gist:

  1. Separate your existing bashrc code into two categories: “re-entrant” and non re-entrant. Basically, if you reload your bashrc a dozen times, it would be bad to run PATH+=... a dozen times, so that’s non re-entrant.
  2. Track updates to the bashrc file. You need to generate a hash of the file contents and/or modification date, then store that every time you load your bashrc.
  3. Choose a reload trigger. Mine checks for updates whenever a new command prompt is printed.

The net effect is, I can reload my bashrc into a terminal by pressing Enter on an empty line, or by executing any single command.

How’d I do it?

At this point, the adventurous among you have already closed this blog post and started tinkering with your bashrc file. Bon voyage! For the rest of you, here’s how I did it.

Even though you need tons of bash experience to even care to read this far, I’m going to explain everything relevant. I have no idea which stuff is basic and which is dark magic. I’ve gone too far down the rabbit hole to remember that!

Reorganizing my code

First, I moved all the non re-entrant code — the stuff I don’t want to execute again on every reload — together near the top. Then, I wrapped it all in one big guard clause, like so:

if _BASHRC_WAS_RUN 2>/dev/null; then
    :;
else
    alias _BASHRC_WAS_RUN=true
    PATH+="..."
    [ -e "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
    # etc ...
fi

The :; syntax is just my idiom for ignoring the then branch when I only care about the else branch. I could have as easily said,

_BASHRC_WAS_RUN 2>/dev/null || {
    alias _BASHRC_WAS_RUN=true
    PATH+="..."
    # etc ...
}

The 2>/dev/null is to hide the command not found error that otherwise appears when you load your bashrc for the first time. An alternative is to conditionally define the alias to false unless it already exists.

Inside the guard, I alias the _BASHRC_WAS_RUN command to true, which is a system builtin guaranteed to always succeed. Since conditional statements in bash are considered true whenever the return code is 0, this block of code will be skipped on each reload.

The rest should be self-explanatory. It’s just code that I don’t want to run more than once. While this specific code is particular to me, the PATH line should at least look familiar. If you redefine your PATH from scratch each time, you won’t even need to add it here!

Below that guard, I have placed my re-entrant code. This is stuff I plan to tweak on a semi-regular basis. For me, I can’t predict that, so I tried to include as much as I could. This motivated me to rewrite my bashrc, which was already due for it.

# Use Neovim if available
if [[ -x "$(which nvim)"]]; then
  VIM=nvim
else
  VIM=vim
fi
EDITOR="$VIM"
alias vi="$VIM"
alias vim="$VIM"

alias ll='ls -alF'
# etc ...

Everything here is opinionated, and none of it is required for your bashrc. The only requirement is that the code can be safely redefined, over and over again. I have to keep this requirement in mind every time I add new code, and decide whether the code needs to go inside the guard clause or not.

Has it changed?

Now that I’ve organized my code and stand to gain some value from this, how do I detect changes to the bashrc?

I decided to use the file modification timestamp as my checksum. I set a reference timestamp toward the bottom of my bashrc, and outside the guard clause.

_BASHRC_TIMESTAMP=$(date +%s)

This will set _BASHRC_TIMESTAMP to the seconds since the universe began on Jan 1st, 1970. If you want to think about the race condition (i.e you save changes to your bashrc while it is currently running in some terminal, causing the update not to be detected), go ahead. Meanwhile, I’m going to say, “Eff!” Then I’ll do a double take and resave my file. Now, I’ll move on with life.

Next, I need to extract the file modification timestamp in the same format. After much tweaking, I came up with this:

getFileTimestamp ()
{
    ls --color=tty -1 --time-style=+%s -l "$@" | cut -f6 -d" "
}

The only thing I’m going to explain is that "$@" expands to the individual command line arguments, properly and individually quoted, and that, yes, it works for multiple arguments just fine — although we won’t need that feature today.

This is a great chance for you to explore the man ls command, as well as to experiment with it (or any alternative) on your own. Go ahead! I promise it won’t steal your Facebook password or delete your hard drive. ;-)

Can I get it?

Armed with that voodoo, I moved on to the trigger point. I needed a way to get my changes into my existing terminal session as quickly as possible. I was already familiar with the PROMPT_COMMAND trigger, which runs after every command that the user types at the prompt.

In fact, I was already using it:

prompt_cmd () {
    err=$?
    if [ $err -gt 0 ]; then
        echo >&2 -e "Error code: ${COLOR[red]}$err${COLOR[normal]}"
    fi
}
PROMPT_COMMAND=prompt_cmd

This lovely script checks the error code of the last command that ran interactively and outputs the error code as text if it didn’t succeed. I use the ANSI code for red color, defined elsewhere. Conveniently, bash preserves the return code for the next command prompt, even though I’m executing stuff in-between like this.

The user experience (so far) is

[nix-shell:~/.../src/posts/published]$ false
Error code: 1

[nix-shell:~/.../src/posts/published]$ echo $?
1

[nix-shell:~/.../src/posts/published]$

Inserting my trigger here will make it pretty quick! I’ll only have to execute one command to hit the trigger. So that means I can reload my bashrc as soon as soon as I start interacting with a shell session again.

Now, the only change I needed to make was to wrap up the timestamp check into a function and tie it with a bow! Don’t say I didn’t get you something for Christmas!

Here’s the full trigger script:

# Reload .bashrc if it's been updated since the last time it ran
chk_bashrc_timestamp () {
    if [[ "$_BASHRC_TIMESTAMP" -lt "$(getFileTimestamp "$HOME/.bashrc")" ]]; then
        echo >&2 "Reloading .bashrc..."
        . ~/.bashrc
    fi
}
_BASHRC_TIMESTAMP=$(date +%s)

prompt_cmd () {
    err=$?
    if [ $err -gt 0 ]; then
        echo >&2 -e "Error code: ${COLOR[red]}$err${COLOR[normal]}"
    fi
    chk_bashrc_timestamp
}
PROMPT_COMMAND=prompt_cmd

And here’s what it looks like in action.

[nix-shell:~/.../src/posts/published]$ touch ~/.bashrc
Reloading .bashrc...
[01:39] gnu/linux ~/.../src/posts/published
$ touch ~/.bashrc && false
Error code: 1
Reloading .bashrc...
[01:39] gnu/linux ~/.../src/posts/published