Is the "callback" concept of programming existent in Bash?

In typical imperative programming, you write sequences of instructions and they are executed one after the other, with explicit control flow. For example:

if [ -f file1 ]; then   # If file1 exists ...
    cp file1 file2      # ... create file2 as a copy of a file1
fi

etc.

As can be seen from the example, in imperative programming you follow the execution flow quite easily, always working your way up from any given line of code to determine its execution context, knowing that any instructions you give will be executed as a result of their location in the flow (or their call sites’ locations, if you’re writing functions).

How callbacks change the flow

When you use callbacks, instead of placing the use of a set of instructions “geographically”, you describe when it should be called. Typical examples in other programming environments are cases such as “download this resource, and when the download is complete, call this callback”. Bash doesn’t have a generic callback construct of this kind, but it does have callbacks, for error-handling and a few other situations; for example (one has to first understand command substitution and Bash exit modes to understand that example):

#!/bin/bash

scripttmp=$(mktemp -d)           # Create a temporary directory (these will usually be created under /tmp or /var/tmp/)

cleanup() {                      # Declare a cleanup function
    rm -rf "${scripttmp}"        # ... which deletes the temporary directory we just created
}

trap cleanup EXIT                # Ask Bash to call cleanup on exit

If you want to try this out yourself, save the above in a file, say cleanUpOnExit.sh, make it executable and run it:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

My code here never explicitly calls the cleanup function; it tells Bash when to call it, using trap cleanup EXIT, i.e. “dear Bash, please run the cleanup command when you exit” (and cleanup happens to be a function I defined earlier, but it could be anything Bash understands). Bash supports this for all non-fatal signals, exits, command failures, and general debugging (you can specify a callback which is run before every command). The callback here is the cleanup function, which is “called back” by Bash just before the shell exits.

You can use Bash’s ability to evaluate shell parameters as commands, to build a callback-oriented framework; that’s somewhat beyond the scope of this answer, and would perhaps cause more confusion by suggesting that passing functions around always involves callbacks. See Bash: pass a function as parameter for some examples of the underlying functionality. The idea here, as with event-handling callbacks, is that functions can take data as parameters, but also other functions — this allows callers to provide behaviour as well as data. A simple example of this approach could look like

#!/bin/bash

doonall() {
    command="$1"
    shift
    for arg; do
        "${command}" "${arg}"
    done
}

backup() {
    mkdir -p ~/backup
    cp "$1" ~/backup
}

doonall backup "$@"

(I know this is a bit useless since cp can deal with multiple files, it’s only for illustration.)

Here we create a function, doonall, which takes another command, given as a parameter, and applies it to the rest of its parameters; then we use that to call the backup function on all the parameters given to the script. The result is a script which copies all its arguments, one by one, to a backup directory.

This kind of approach allows functions to be written with single responsibilities: doonall’s responsibility is to run something on all its arguments, one at a time; backup’s responsibility is to make a copy of its (sole) argument in a backup directory. Both doonall and backup can be used in other contexts, which allows more code re-use, better tests etc.

In this case the callback is the backup function, which we tell doonall to “call back” on each of its other arguments — we provide doonall with behaviour (its first argument) as well as data (the remaining arguments).

(Note that in the kind of use-case demonstrated in the second example, I wouldn’t use the term “callback” myself, but that’s perhaps a habit resulting from the languages I use. I think of this as passing functions or lambdas around, rather than registering callbacks in an event-oriented system.)


First it's important to note that what makes a function a callback function is how it's used, not what it does. A callback is when code that you write is called from code that you didn't write. You're asking the system to call you back when some particular event happens.

An example of a callback in shell programming is traps. A trap is a callback that isn't expressed as a function, but as a piece of code to evaluate. You're asking the shell to call your code when the shell receives a particular signal.

Another example of a callback is the -exec action of the find command. The job of the find command is to traverse directories recursively and process each file in turn. By default, the processing is to print the file name (implicit -print), but with -exec the processing is to run a command that you specifies. This fits the definition of a callback, although at callbacks go, it is not very flexible since the callback runs in a separate process.

If you implemented a find-like function, you could make it use a callback function to call on each file. Here's an ultra-simplified find-like function that takes a function name (or external command name) as argument and calls it on all regular files in the current directory and its subdirectories. The function is used as a callback which is called every time call_on_regular_files finds a regular file.

shopt -s globstar
call_on_regular_files () {
  declare callback="$1"
  declare file
  for file in **/*; do
    if [[ -f $file ]]; then
      "$callback" "$file"
    fi
  done
}

Callbacks aren't as common in shell programming as in some other environments because shells are primarily designed for simple programs. Callbacks are more common in environments where data and control flow are more likely to move back and forth between parts of the code that are written and distributed independently: the base system, various libraries, the application code.


"callbacks" are just functions passed as arguments to other functions.

At shell level, that simply means scripts / functions / commands passed as arguments to other scripts / functions / commands.

Now, for a simple example, consider the following script:

$ cat ~/w/bin/x
#! /bin/bash
cmd=$1; shift
case $1 in *%*) flt=${1//\%/\'%s\'};; *) flt="$1 '%s'";; esac; shift
q="'\\''"; f=${flt//\\/'\\'}; p=`printf "<($f) " "${@//\'/$q}"`
eval "$cmd" "$p"

having the synopsis

x command filter [file ...]

will apply filter to each file argument, then call command with the outputs of the filters as arguments.

For instance:

x diff zcat a.gz b.bz   # diff gzipped files
x diff3 zcat a.gz b.gz c.gz   # same with three-way diff
x diff hd a b  # hex diff of binary files
x diff 'zcat % | sort -u' a.gz b.gz  # first uncompress the files, then sort+uniq them, then compare them
x 'comm -12' sort a b  # find common lines in unsorted files

This is very close to what you can do in lisp (just kidding ;-))

Some people insist on limiting the "callback" term to "event handler" and/or "closure" (function + data/environment tuple); this is by no way the generally accepted meaning. And one reason why "callbacks" in those narrow senses aren't of much use in shell is because pipes + parallelism + dynamic programming capabilities are so much more powerful, and you're already paying for them in terms of performance, even if you try to use the shell as a clunky version of perl or python.

Tags:

Function

Bash