I was refactoring a bunch of initialisation scripts today and I found a bunch of places where I was retrying something with jittered arithmetic backoff. The details aren't all that important, except to say that I wanted to factor this out.

To do this, I needed to be able to pass a function to another function in bash. The language provides no direct facility for this, but it does have eval which allows us to implement this functionality.

# Jittered backoff
# 
# usage:
# backoff_retry CONDITION [OPERATION]
# 
# CONDITION should return success if we have succeeded in the
# operation.
# 
# OPERATION should be the operation to perform (if different from
# CONDITION).
# 
# If OPERATION is not present, the CONDITION is assumed to be the
# operation and is not tested again. This is to prevent inadvertant
# double retries.
# 
backoff_retry()
{
    count=0
    sleepy_time=0
    # The first parameter is our exit condition, so we extract it
    condition=$1
    shift

    # Try until we succeed
    while ! $condition; do
        echo "Trying '$@'"
        # Here's where we execute the code we want to retry
        eval "$@"
        # We skip the check if the condition is also the operation, otherwise
        # we end up trying everything twice
        if [ "$#" != "0" ]; then
            $condition && break
        fi
        # Add a random number of seconds (0-9) to the sleep time and
        # update the counter
        sleepy_time=$(($sleepy_time+$RANDOM*10/32768))
        count=$(($count+1))
        echo "Failed [$condition] running '$@' - try $count, sleeping $sleepy_time" "warn"
        sleep $sleepy_time
    done
}

The counter is purely informational, but it should be fairly trivial to add an option to limit the number of tries. Also, while you can pass raw commands rather than functions, it's probably worth writing a function for anything longer than a single command.

# Here are some examples of using the above retry function

# Using just CONDITION:
backoff_retry 'aptitude update'

# Using CONDITION and OPERATION:
backoff_retry '[ -f index.html ]' 'wget http://www.google.com/index.html'

# Using a function as the operation:
get_file()
{
    domain=`cat domain.txt`
    wget http://$domain/index.html
}

backoff_retry '[ -f index.html ]' 'get_file'

There are several more interesting things you can do with eval and functions:

# You can bind functions to arbitrary names at runtime, giving you the
# closest bash can probably get to lambdas and closures:

bindfun()
{
    name=$1
    shift
    eval "$name() { $@; }"
}

# You can bind different functions based on input:

foofun()
{
    if $1; then
        bindfun $2 'echo foo'
    else
        bindfun $2 'echo bar'
    fi
}

# You can reduce functions by currying:

add()
{
    echo $(( $1 + $2 ))
}

bindfun inc 'add 1 "$@"'
bindfun dec 'add -1 "$@"'