Sometimes you may need to capture an exit code of a command while keeping the errexit
option enabled during execution of that command.
|| rc=$?
or wrapping the command in an if
statement, does not work correctly for compound commands and functions as ||
, if
, while
, etc. will completely disable the errexit
option for that command, therefore failures in the middle of a function will not cause that function to fail.
Example:
set -euo pipefail
failing_fn () {
return 5
}
complex_fn () {
true
failing_fn
echo 'Should not see me'
}
rc=UNCHANGED
complex_fn || rc=$?
echo "$rc"
outputs
Should not see me
UNCHANGED
One could use the PIPESTATUS
trick, like complex_fn | cat; rc=${PIPESTATUS[0]}
but it does not work with -o pipefail
Here’s one of the ways to handle this:
set +e
(set -e; complex_fn)
rc=$?
set -e
echo "$rc"
outputs 5
This method also works with pipes and -o pipefail
:
set +e
(set -e; true | complex_fn | true)
rc=$?
set -e
echo "$rc"
also outputs 5
There is a way to declare (-n
) a variable in Bash as a reference to another variable (kind of pointer). This allows us to pass multiple arrays to functions or create utility functions like:
# Check if variable is set. Will return true even when the value is an empty string.
# Usage: is_set VAR_NAME
is_set () {
declare -n __var="$1"
[[ "${__var+set}" = 'set' ]]
}
or
# Translate characters in a string
# Usage: translate STR FROM_CHARS TO_CHARS
translate () {
declare str="$1"
declare -n __from="$2"
declare -n __to="$3"
declare i
[[ "${#__from[@]}" = "${#__to[@]}" ]] || return 1
for i in "${!__from[@]}"; do
str="${str//"${__from[$i]}"/"${__to[$i]}"}"
done
printf '%s\n' "$str"
}
$ declare -a from=(a b)
$ declare -a to=(c d)
$ translate "abc" from to
cdc
or
pushopts () {
declare -n __opts="$1"
readarray -t __opts < <(shopt -po)
}
popopts () {
declare -n __opts="$1"
declare cmd
for cmd in "${__opts[@]}"; do
eval "$cmd"
done
}
$ set -e
$ pushopts BACKUP
$ set +e
$ popopts BACKUP
$ shopt -po errexit
set -o errexit
errexit
is not propagated into command substitutions, so you cannot do this:
set -e
OPTS=$(shopt -po)
set +e
eval "$OPTS"
but it is propagated into process substitutions.
set -e
# Backup restore commands into an array
declare -a OPTS
readarray -t OPTS < <(shopt -po)
set +e
# Restore options
declare cmd
for cmd in "${OPTS[@]}"; do
eval "$cmd"
done
Check:
$ shopt -po errexit
set -o errexit
In Bash, time
is a built-in reserved word you can prepend to a pipeline to measure the duration of executed commands. It implements a subset of features of the original time
binary, but does not involve shelling-out.
time
built-in will output the measurements to STDERR. Under some circumstances (time
is used to measure a failing function executed in a subshell with errexit enabled), the STDERR of time
will leak into the executed command.
Example:
Let’s define a couple of functions first
$ failing_fn () { false; }
$ successful_fn () { true; }
The following will result in measurements written into testfile
(while it shouldn’t, redirect happens in the subshell)
$ time (set -e; failing_fn 2>./testfile)
$ cat testfile
real 0m0.000s
user 0m0.000s
sys 0m0.000s
This does not happen if the subshell command is not a function
$ time (set -e; false 2>./testfile)
real 0m0.000s
user 0m0.000s
sys 0m0.000s
$ cat testfile
it also doesn’t happen when the failing function does not fail the pipeline (errexit is not enabled)
$ time (set +e; failing_fn 2>./testfile)
real 0m0.000s
user 0m0.000s
sys 0m0.000s
$ cat testfile
or if the function is successful
$ time (set -e; successful_fn 2>./testfile)
real 0m0.000s
user 0m0.000s
sys 0m0.000s
$ cat testfile
or when we do not use time with the subshell syntax directly
$ time eval '(set -e; failing_fn 2>./testfile)'
real 0m0.001s
user 0m0.001s
sys 0m0.000s
$ cat testfile
Oddly enough, simply adding | cat
to the pipeline inside the subshell will result in measurements output being duplicated into both STDERR of the current shell and STDERR of the subshell function
$ time (set -e; failing_fn 2>./testfile | cat)
real 0m0.001s
user 0m0.001s
sys 0m0.001s
$ cat testfile
real 0m0.001s
user 0m0.000s
sys 0m0.000s
and it works properly in case | cat
and eval
are present in the subshell (found this one by trial-and-error)
$ time (set -e; eval failing_fn 2>./testfile | cat)
real 0m0.001s
user 0m0.001s
sys 0m0.001s
$ cat testfile
Since Bash version 4.1 it is possible to dynamically allocate file descriptors greater than 10. Useful for redirections
Example:
$ testfn () {
echo "This is STDOUT"
echo "This is STDERR" 1>&2
}
$ (
# Annotate output of the current subshell so it is visible
# which file descriptor it comes from
exec 2> >(sed -e "s/^/STDERR:/") > >(sed -e "s/^/STDOUT:/")
# Unmodified output
testfn
# The folowing command will swap STDOUT and STDERR from testfn()
{ testfn 2>&${stderrfd} >&${stdoutfd}; } {stderrfd}>&1 {stdoutfd}>&2
# Need to manually close the created descriptors after use
exec {stderrfd}>&- {stdoutfd}>&-
)
STDERR:This is STDERR
STDERR:This is STDOUT
STDOUT:This is STDOUT
STDOUT:This is STDERR
Output is out of order because annotating processes run in async subshells
A while ago my daughter asked me to show her how beat-matching is done. Unfortunately we had no DJing equipment and even the entry-level MIDI controllers on Amazon are quite expensive. After some googling we’ve discovered projects like Fliper DJ and Traktorino, but the former wasn’t quite what I’ve wanted and the later wasn’t available at the time, so we’ve decided to build one ourselves.
Few knobs, buttons, wires, Arduino Uno (+ CD74HC4067 multiplexer) and here we go:
Project repository on GitHub contains the code and Traktor 2 mappings.
Put .git
in it.
More Best Practices When It Comes to Writing Docker Related Files
Scaleway has introduced the NextGen Start NVMe Cloud Servers (yes, NVME SSD) starting at €1.99/month!
puppet resource augeas setnoop \
"context=/files/etc/puppetlabs/puppet/puppet.conf/agent" \
"changes=set noop true" \
&& { systemctl try-restart puppet || service puppet condrestart; }