How to skip the fortune command when your shell is slow to start

A.K.A. How to get and compare timestamps without external commands in shell script (and without even invoking subshells in Zsh)

I love the fortune command. It’s a charming little addition to each new tab I open… until something (like a nightly backup) has blown away my disk cache or a runaway memory leak is causing thrashing. Then, it’s just a big delay in getting to what I want to do.

The obvious solution to any non-shell programmer is to time everything and invoke fortune only if it’s not already taking too long, but shell script complicates that by having so few builtins. We don’t want to invoke an external process, because that would defeat the point of making fortune conditional, and we don’t want to invoke a subshell because, if we’re thrashing because of memory contention, that’ll also make things worse.

It turns out that bash 4.2 and above can get us half-way there by using a subshell to invoke the printf builtin with the %(%s)T token, but Zsh has a clever little solution that even reuses code that we’re going to need anyway: prompt substitutions!

Here’s the gist of how to pull it off:

# Top of .zshrc
local start_time="${(%):-"%D{%s}"}"

# -- Do all my .zshrc stuff here

local end_time="${(%):-"%D{%s}"}"
if [[ $(( end_time - start_time )) < 2 ]]; then
    if (( $+commands[fortune] )); then
    echo "Skipping fortune (slow startup)"

This is a standard “subtract start time from end time to get how long it took, then compare it to a threshold” check, so the only part that should need to be changed in bash is using start_time="$(printf "%(%s)T")". Instead, let’s pick apart how the Zsh version works:

  1. We start with a bog standard ${VAR:-DEFAULT} parameter expansion however, unlike bash, Zsh does consider ${:-always default} to be valid syntax.
  2. The (%) on the left-hand side is a special magic flag, similar to the (?i) syntax used for inline flag-setting in some regular expression engines. It enables prompt expansion of both the (nonexistent) variable’s contents and the fallback value.
  3. %D{...} is Zsh’s prompt expansion placeholder for putting strftime (man strftime(3)) timestamps into your prompt.
  4. %s is the strftime token for “seconds since the epoch”
  5. You have to quote the %D{...} or the ${...} consumes the closing curly brace too eagerly.

That’s the big magic thing. A way to write an equivalent to time(2) from the C standard library in pure Zsh script with no use of $(...) or zmodload and, since we’re using prompt expansion to do it, the only thing we might not already have needed to load into memory is the code for the %D{...} expansion token.

(Unfortunately, there’s no way to get sub-second precision with this approach, so the only two useful threshold values for a well-optimized zshrc are probably “1 second” and “2 seconds”.)

Now for that odd (( $+commands[fortune] )) way of checking for the presence of the fortune command. What’s up with that?

Well, it’s actually a micro-optimization that I use in my zsh-specific scripts. According to this guy’s tests, it runs in half the time the other options take and, in my own tests using his test scripts, I found that, depending on the circumstances, that could go as far as one tenth of the others, and that the others vary wildly relative to each other. (On runs where $+commands is 7 to 10 times as fast as type and which, hash is sometimes twice as fast as type or which and sometimes half as fast.)

Normally, this would be a moot point because any of the portable ways of checking for the existence of a command via a subshell and a builtin would be far too quick for it to matter (ie. I do it just for the heck of it) but, in this case, it felt appropriate.

(Another unnecessary micro-optimization that I didn’t use here was preferring [[ ]] over [ ] in my zshrc scripts. My tests found a million runs of [[ "$PWD" == "$HOME" ]] to take about 1.4 seconds, while a million runs of [ "$PWD" = "$HOME" ] took about 4.2 seconds.)

CC BY-SA 4.0 How to skip the fortune command when your shell is slow to start 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.

All comments are moderated. If your comment is generic enough to apply to any post, it will be assumed to be spam. Borderline comments will have their URL field erased before being approved.