Exit code of a compound command and errexit

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

Indirect variable references in Bash 4.3

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

Backup/restore Bash options including 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

BUG in the "time" Bash built-in?

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

Dynamic allocation of file descriptors in Bash

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

Arduino-based DIY DJ MIDI controller

Controller outside Controller inside

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.

.dockerignore

Put .git in it.

More Best Practices When It Comes to Writing Docker Related Files

UTC

UTC is enough for everyone …right?

Scaleway

Scaleway has introduced the NextGen Start NVMe Cloud Servers (yes, NVME SSD) starting at €1.99/month!

Portable one-liner to enable the Puppet noop mode

puppet resource augeas setnoop \
  "context=/files/etc/puppetlabs/puppet/puppet.conf/agent" \
  "changes=set noop true" \
&& { systemctl try-restart puppet || service puppet condrestart; }