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:
- “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)
- Call the virtualenvwrapper setup code to load the real command.
init_virtualenvwrapper
starts by checking for some side-effect of having been run before and exits early if that’s the case. (This keepsmkproject
from re-doing whatworkon
already did, or vice-versa.)- Call the actual command and pass through any arguments.
Doing this means that:
- 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 moveinit_virtualenvwrapper
into another file andsource
it on demand.) - 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.) - 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
:
- 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
. - In my testing, functions didn’t set
$0
in bash, so this will actually executebash "$@"
, bringing you back to where you started, while zsh doesn’t set theFUNCNAME
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.
On-Demand Loading for your .zshrc or .bashrc by Stephan Sokolow is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.