six demon bag
Wind, fire, all that kind of thing!
2020-03-23
Shell Patterns (3) - Structured Output
This is a short series describing some Bash constructs that I frequently use in my scripts.
On Linux (and many other operating systems) it's common to have regular and error output written to stdout and stderr respectively. In shell scripts you'd use the echo
or printf
commands for displaying messages, and redirect stdout to stderr for having the message displayed on stderr.
echo 'foo' # output goes to stdout
echo 'bar' 1>&2 # output goes to stderr
There may be different levels of information that you want to separate from each other, though, like having additional debug output that you don't want to pollute stdout or stderr. For that you can use the file descriptors 3 through 9.
echo 'debug message' 1>&3
However, for output on these file descriptors you need to tell Bash what to do with it, otherwise running the script will produce the following error:
Bad file descriptor
To avoid this error redirect the output to a file when running the script:
./script.sh 3> /var/tmp/debug.log
or redirect it from within the script via the exec
command:
#!/bin/bash
exec 3> /var/tmp/debug.log
echo 'debug message 1>&3
Alternatively you can merge the file descriptor into stdout to have the debug output displayed in the console.
#!/bin/bash
exec 3>&1
echo 'debug message 1>&3
I know it sounds silly to separate output streams only to merge them again right away, but bear with me here. Let's say, when you run the script interactively you want debug output to go to the console, but when you're running the script in the background you want debug output in a file. Using an environment variable you can discriminate between these two cases by sending debug output either to a path defined in that variable, or to the console if the variable is empty.
if [ -z "${DEBUG_LOG:-}" ]; then
exec 3>&1
else
exec 3>"$DEBUG_LOG"
fi
Beware that if you're using the above code in several scripts that invoke each other (e.g. because you put the code in a library script that every script dot-sources) each script will truncate the log file and thus destroy all output that has been written before! Check if the file descriptor is already being redirected to avoid that:
if [ -z "${DEBUG_REDIRECTION:-}" ]; then
if [ -z "${DEBUG_LOG:-}" ]; then
exec 3>&1
else
exec 3>"$DEBUG_LOG"
fi
export DEBUG_REDIRECTION="$(readlink /proc/$$/fd/3 | grep -v '^pipe:')"
fi
Wrap the statements for displaying output in functions, so you can use "speaking" names and also avoid cluttering your code with redirections:
msg() {
if [ -n "${1:-}" ]; then
echo -e "$1"
fi
}
warn() {
msg "${1:-}" 1>&2
}
Functions can also be used to make output conditional and/or decorate it:
debug() {
if [ "$DEBUG" = 'y' ]; then
echo "${1:+$(date +"$DATEFMT") DEBUG ${FUNCNAME[1]}()\t$1}" 1>&3
fi
}
Hence my scripts usually contain a section like this:
DEBUG="${DEBUG:-n}"
if [ "${DEBUG,,}" = 'y' ] && [ -z "${DEBUG_REDIRECTION:-}" ]; then
if [ -z "${DEBUG_LOG:-}" ]; then
exec 3>&1
else
exec 3>"$DEBUG_LOG"
fi
export DEBUG_REDIRECTION="$(readlink /proc/$$/fd/3 | grep -v '^pipe:' || true)"
fi
msg() {
if [ -n "${1:-}" ]; then
echo -e "$1"
fi
}
warn() {
msg "${1:-}" 1>&2
}
fail() {
warn "${1:+${FUNCNAME[1]}()\t$1}"
exit "${2:-1}"
}
debug() {
if [ "${DEBUG,,}" = 'y' ]; then
msg "${1:+$(date +"$DATEFMT") DEBUG ${FUNCNAME[1]}()\t$1}" 1>&3
fi
}
Posted 15:22 [permalink]