This post shows how to run a command with a time limit in Bash, considering several shell options.

tl;dr

See this code snipet.

The timeout command

For this purpose, we use GNU’s timeout command.

The manual page says:

If the command times out, and –preserve-status is not set, then exit with status 124.

This can be problematic when you have enabled the errexit option in Bash, and you want to continue your script after the command has timed out.

Shell options in Bash

Bash has a variety of built-in shell options.

  • The Set Builtin - Bash Manual
  • Usage:
    • to enable an option: set [--abefhkmnptuvxBCEHPT] [-o option-name] [argument …]
    • to disable an option: set [+abefhkmnptuvxBCEHPT] [+o option-name] [argument …]

Here is a list of the options.

Name Short Default Description
allexport a Off Each variable or function that is created or modified is given the export attribute and marked for export to the environment of subsequent commands.
braceexpand B On The shell will perform brace expansion.
emacs   On Use an emacs-style line editing interface.
errexit e Off Exit immediately if a pipeline returns a non-zero status.
errtrace E Off If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment.
functrace T Off If set, any trap on DEBUG and RETURN are inherited by shell functions, command substitutions, and commands executed in a subshell environment.
hashall h On Locate and remember (hash) commands as they are looked up for execution.
histexpand H On Enable ! style history substitution.
history   On (*) Enable command history. *This option is on by default in interactive shells.
ignoreeof   Off An interactive shell will not exit upon reading EOF.
keyword k Off All arguments in the form of assignment statements are placed in the environment for a command, not just those that precede the command name.
monitor m On Job control is enabled. All processes run in a separate process group. When a background job completes, the shell prints a line containing its exit status.
noclobber C Off Prevent output redirection using >, >&, and <> from overwriting existing files.
noexec n Off(*) Read commands but do not execute them. This may be used to check a script for syntax errors. *This option is ignored by interactive shells.
noglob f Off Disable filename expansion (globbing).
nolog   Off Currently ignored.
notify b Off Cause the status of terminated background jobs to be reported immediately, rather than before printing the next primary prompt.
nounset u Off Treat unset variables and parameters other than the special parameters @ or * as an error when performing parameter expansion. An error message will be written to the standard error, and a non-interactive shell will exit.
onecmd t Off Exit after reading and executing one command.
physical P Off If set, do not resolve symbolic links when performing commands such as cd which change the current directory. The physical directory is used instead. By default, Bash follows the logical chain of directories when performing commands which change the current directory.
pipefail   Off If set, the return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully.
posix   Off Change the behavior of Bash where the default operation differs from the POSIX standard to match the standard.
privileged p Off Turn on privileged mode.
verbose v Off Print shell input lines as they are read.
vi   Off Use a vi-style line editing interface.
xtrace x Off Print a trace of simple commands, for commands, case commands, select commands, and arithmetic for commands and their arguments or associated word lists after they are expanded and before they are executed.

For example, you can enable the errexit option by either set -e or set -o errexit, and you can disable that option by either set +e or set +o errexit. The shell variable $- represents the currently enabled options.

bash-3.2$ echo $-
himBH
bash-3.2$ set -e
bash-3.2$ echo $-
ehimBH
bash-3.2$ set +e
bash-3.2$ echo $-
himBH
bash-3.2$ set -o errexit
bash-3.2$ echo $-
ehimBH
bash-3.2$ set +o errexit
bash-3.2$ echo $-
himBH

Wrapping the timeout command

So, if the errexit option is set, we want to temporarily disable it and then run the timeout command.

Here is an example wrapper for such purposes.

TIMEOUT=timeout

run_with_timeout() {
    oldopt=$-
    set +e
    $TIMEOUT $@
    ret=$?
    if [[ $ret -eq 124 ]]; then
        ret=0
    fi
    set -$oldopt
    return $ret
}

Note that we overwrite the return code with 0 if the command times out (in that case, timeout returns status code 124). Also, we back up the current shell options and restore them before exiting the function.

We can test this function with the following code.

set -e

run_with_timeout 1s true
echo "Success within time limit: rc=$?"

run_with_timeout 1s sleep 10
echo "Timeout: rc=$?"

run_with_timeout 1s false
echo "*THIS SHOULD NOT BE PRINTED* Failure within time limit: rc=$?"

The output should be like this:

Success within time limit: rc=0
Timeout: rc=0