􀀂􀀟􀀍􀀆 􀀂􀀖􀀌􀀆 Shell startup precomputation

A month ago, Thorsten Ball wrote How fast is your shell?, encouraging users to profile their shells to make sure they start up quickly. His results are about 70ms - better than mine, but I still think mine are OK.

> /usr/bin/time zsh -i -c exit
        0.18 real         0.05 user         0.04 sys


  • Thorsten is using zsh’s time builtin, which shows real elapsed time on the right; I’m using the POSIX command, which shows real elapsed time on the left.
  • Oddly, zsh’s time is not documented with its builtins.
  • I first ran the test in a loop like i=1; while [ $i -le 5 ]; do /usr/bin/time zsh -i -c exit; i=$((i + 1)); done;, but the first iteration always took over ~170ms, while subsequent ones took 60ms. I take from this that 60ms is execution time and 140ms is executable and/or config file loading time.

A couple of days later, Laurence Tratt wrote Faster Shell Startup With Shell Switching, which suggests using different shells for scripts and interactive use. (A real callback to arguments1 about the C shell2 on Usenet!)

This post is about my suggestion for improving shell startup time: precomputation.

I have a script called dhd-shdetect that detects a bunch of stuff I want to account for in my shell that rarely changes, caching it in the file $HOME/.shdetect_dhd.sh. Anything that it can precompute is code that doesn’t have to run at every shell startup. Then, as much as possible, .zshrc and .profile files use the precomputed values rather than running code themselves. I’m not fanatic about this; if the best thing to do is run code at shell startup, then I do.

A few examples:

  1. I have a script called pathsetup which takes a list of paths that may exist, and returns all those in the list that actually do exist. It takes a while to check all these paths, so dhd-shdetect caches the result.

    # Stuff that we want to use, only if it exists on the filesystem
    ... dozens more entries
    # Stuff we want to ignore even if it does exist
    export PATH=$("$DHD/opt/bin/pathsetup" "$POSSIBLE_PATHS" "$IGNORE_PATHS")

    This is saved directly to the $HOME/.shdetect_dhd.sh file, which is dot-sourced in the .profile script.

  2. Detecting the ls(1) implementation in dhd-shdetect requires actually running the binary:

    if ls --version 2>/dev/null | grep -q GNU; then
    elif ls -h -G >/dev/null 2>&1; then

    This means that in .zshrc, we only have to read the cached value:

    case "$DHD_LS_TYPE" in
        gnu) alias ls="ls -FhN --color=always";;
        bsd) alias ls="ls -FhG";;
        *) alias ls="ls -F";;
  3. dhd-shdetect looks for specific commands that the profile scripts use if they are available. It saves them to a variable called $DHD_CMDAVAIL, and provides a function for fast lookups into that variable, avoiding the need to scan $PATH during shell startup.

    In dhd-shdetect:

    # Available commands
    # Can test for them with
    #    test "${DHD_CMDAVAIL#*cmd_name}" != "$DHD_CMDAVAIL"
    cmdavail fortune && DHD_CMDAVAIL="$DHD_CMDAVAIL fortune"
    cmdavail kubectl && DHD_CMDAVAIL="$DHD_CMDAVAIL kubectl"
    # ... several more

    The result saved to $HOME/.shdetect_dhd.sh:

    # ...
    # Very fast way to test precomputed command availability
    dhd_cmdavail() {
        test "\${DHD_CMDAVAIL#*\$1}" != "\$DHD_CMDAVAIL"
        return \$?

    Called in .zshrc:

    dhd_cmdavail kubectl && source <(kubectl completion zsh)

I can run as many programs as I want to influence my shell configuration, but I don’t have to pay the time cost every time my shell starts up. It’s a significant time savings, as running dhd-shdetect takes over a second:

> /usr/bin/time dhd-shdetect
Generated data saved to /Users/mrled/.shdetect_dhd.sh
Now log in again, or run the '.z' alias
        1.03 real         0.43 user         0.32 sys

That’s time I don’t have to spend waiting for new terminals ✨.

  1. The bad-boy of UNIX:

    This month begins a tutorial on the bad-boy of UNIX, lowest of the low, the shell of last resort. Yes, I am talking about the C shell. FAQ’s flame it. Experts have criticized it. Unfortunately, this puts UNIX novices in an awkward situation. Many people are given the C shell as their default shell. They aren’t familiar with it, but they have to learn enough to customize their environment. They need help, but get criticized every time they ask a question. Imagine the following conversation, initiated by a posting on USENET:

    Novice: How do I do XYZ using the C shell?

    Expert: You shouldn’t use the C shell. Use the Bourne shell.

    Novice: I try to, but I get syntax errors.

    Expert: That’s because you are using the C shell. Use the Bourne shell.

    Novice: I’ve now using the Bourne shell. How to I create aliases and do command-line editing in the Bourne shell?

    Expert: You can’t. use bash, ksh or tcsh.

    Novice: I don’t have these shells on all of the systems I use. What can I use?

    Expert: In that case, use the C shell.

    Novice: But you told me I shouldn’t use the C shell!?!

    Expert: Well, if you have to, you can use the C shell. It’s fine for interactive sessions. But you shouldn’t use it for scripts.

    Novice: It’s really confusing trying to learn two shells at once. I don’t know either shell very well, and I’m trying to learn JUST enough to customize my environment. I’d rather just learn one shell at a time.

    Expert: Well, it’s your funeral.

    Novice: How do I do XYZ using the C shell?

    Another Expert: You shouldn’t be using the C shell. Use the Bourne shell.

    Novice: @#%&!

  2. Csh Programming Considered Harmful:

    Resolved: The csh is a tool utterly inadequate for programming,
          and its use for such purposes should be strictly banned!


Comments are hosted on this site and powered by Remark42 (thanks!).

Webmentions are hosted on remote sites and syndicated via Webmention.io (thanks!).