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:

  • errtrace: If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.
  • errexit: Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command […] exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return value is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately […], and may cause subshells to exit before executing all the commands in the subshell.
  • nounset: Treat unset variables and parameters other than the special parameters @ and * as an error when performing parameter expansion. If expansion is attempted on an unset variable or parameter, the shell prints an error message, and, if not interactive, exits with a non-zero status.
  • -o pipefail: If set, the return value of a pipeline is the value of the last (right‐most) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully […].

Pipes

Concatenation operators 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

Comments

Use backtick command expansion for commenting out a line in multiline commands:

command \
    first argument \
    `# comment` \
    second argument