Pierre Prinetti

Bash style notes

My personal Bash styleguide.

Headers

The shebang tells our operating system what interpreter to use to execute the script.

#!/usr/bin/env bash

These options make the execution of our script more predictable:

set -o errtrace
set -o errexit
set -o nounset
set -o pipefail

# or more concisely:
set -Eeuo pipefail

When a command in a script fails, the failure is ignored by default. With -E and -e, errors stop the execution of the script.

Undeclared variables are typically treated as empty variables. With -u, they stop the execution of the script.

From man builtin:

Pipes

The concatenation operator modify the behaviour of the next command. For this reason, I find them more readable when positioned on the new line.

curl -sSL https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64 \
	| tee /tmp/yq \
	| sha256sum -c <(echo -n 'e70e482e7ddb9cf83b52f5e83b694a19e3aaf36acf6b82512cbe66e41d569201  -') \
	&& chmod +x /tmp/yq

When executing a piped command in an interactive shell, redirect to TTY to inspect what is being sent through the pipe:

openstack port list -f value -c id \
	| tee /dev/tty \
	| xargs -r openstack port delete

In a script, redirecting to stderr can be useful for logging:

openstack port list -f value -c id \
	| tee /dev/stderr \
	| xargs -r openstack port delete

Functions and Scope blocks

There is one true brace style.

Functions wrapped in curly braces {} do not define a scope. To avoid polluting the global context, explicitly declare the scope of our variables:

set_var() {
	declare var='hey'      # 'var' does not exist outside the block
	declare -r target="$1" # 'target' does not exist outside the block, and can't be changed inside the block
	leak='ugh'             # 'leak' is available outside the block after the function returns. Don't do that.
}

Blocks wrapped in parenthesis () execute in a subshell and confine any declared environment.

restic-backup() (
	source <(pass show restic/env) # source a script exporting credentials
	restic backup ~/Documents      # use the credentials
)                                      # Credentials are no longer available after the function returns

Tests

We can’t assume that a variable has been exported outside our script. This substitution works with set -u even when the variable was never declared.

MAYBE_SET_EXTERNALLY="${MAYBE_SET_EXTERNALLY:-default}"

Since we’re using Bash, we can happily use the power of double brackets.

if [[ -r "$myfile" ]]; then
	command
else
	dnammoc
fi

Move content around

From file to variable :

data="$(<"$data_file")"

From variable to file:

cat <<< "$data" > "$data_file"

Command output to variable:

data="$(command arg1 arg2)"

string to command stdin (herestring):

python3 <<< 'print("ciao")' # A newline always gets appended to a herestring

multiline string to command stdin (heredoc):

python3 <<-EOF
	if __name__ == '__main__':
	    print(f'Hello, {"$(echo -n 'world')"}!')
	EOF

The - before the delimiter (EOF in this case) tells Bash to remove any leading tab from the output.

To prevent any substitution in the heredoc, wrap the delimiter in single quotes:

cat <<'EOF' > out.txt
This text $will $be $printed $(as-is) into 'out.txt'.
EOF