On-Demand Loading for your .zshrc or .bashrc

Recently, I’ve been trying to make my coding environment snappier, and one thing I was never happy with was how slow my .zshrc is.

Now, don’t get me wrong, I’m not one of those people using oh-my-zsh with a ton of plugins and seeing 15-second waits for my shell to start… but I do want a new tab to be ready in a second or less.

So, I slapped zmodload zsh/zprof onto the top of my .zshrc, opened a new tab, and ran zprof | less …and 50% of the wait was in sourcing virtualenvwrapper, which I don’t feel like reinventing.

Time to take a lesson from the improvements I’ve been making to my .vimrc. Specifically, the { 'on': ['CommandA', 'CommandB'] } option hanging off the end of various lines for my plugin loader.

A little experimentation later and I came up with this construct:

function init_virtualenvwrapper {
    # Don't do anything if it's already loaded
    type virtualenvwrapper_workon_help &>/dev/null && return

    # ------------------------------------------------
    # normal stuff to load virtualenvwrapper goes here
    # ------------------------------------------------
}

for cmd in workon mkproject mkvirtualenv; do
    function $cmd {
        unset -f "$0"
        init_virtualenvwrapper
        "$0" "$@"
    }
done

For those not familiar with shell scripting, I’ll clarify.

For each shell function or command that I want to trigger deferred loading, I create a function with the same name that does the following:

  1. “Delete” itself so it won’t interfere with what virtualenvwrapper is going to set up. (You want to do this first to avoid removing what virtualenvwrapper just created)
  2. Call the virtualenvwrapper setup code to load the real command.
  3. init_virtualenvwrapper starts by checking for some side-effect of having been run before and exits early if that’s the case. (This keeps mkproject from re-doing what workon already did, or vice-versa.)
  4. Call the actual command and pass through any arguments.

Doing this means that:

  1. Your .zshrc or .bashrc startup time only pays the price for declaring a few shell functions. (And, if that gets too heavy for some reason, you could move init_virtualenvwrapper into another file and source it on demand.)
  2. Your first call to a wrapped command like workon will take longer. (eg. if it was adding two seconds to your shell start time, then your first call to it will take two seconds longer.)
  3. Subsequent calls to that or any other command sharing the same init_virtualenvwrapper will be as quick as usual.

Unfortunately, this design is actually Zsh-specific, which sucks for me because this is a file I share between .zshrc and .bashrc:

  1. Bash doesn’t support using a variable for a function name, so you can’t use a for loop. You’ll just get `$cmd': not a valid identifier.
  2. In my testing, functions didn’t set $0 in bash, so this will actually execute bash "$@", bringing you back to where you started, while zsh doesn’t set the FUNCNAME array variable that bash uses.

So, if you want to support both, here’s the most concise form I was able to put together:

function init_virtualenvwrapper {
    local _cmdname="$1"
    shift
    unset -f "$_cmdname"

    # Don't do anything if it's already loaded
    if ! type virtualenvwrapper_workon_help &>/dev/null; then
            # ----------------------------------------
            # normal stuff to load virtualenvwrapper
            # ----------------------------------------
    fi

    "$_cmdname" "$@"
}
# }}}

function workon {
    init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}
function mkproject {
    init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}
function mkvirtualenv {
    init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}

Anyway, I hope this helps to inspire anyone else who’s suffering from slow shell startup times.

UPDATE: And now, shortly after writing that, I discover that someone else went to the trouble of using eval to provide a nice API on top of this trick and put it up on GitHub as sandboxd. From that name, I can see why i didn’t find it before.

CC BY-SA 4.0 On-Demand Loading for your .zshrc or .bashrc by Stephan Sokolow is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

This entry was posted in Geek Stuff. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution under the same terms as the associated post.       Also, please be aware that non-constructive comments will have their URL field erased before being approved in order to combat SEO spam.