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 awhile
oruntil
keyword, part of the test following theif
orelif
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 onERR
, 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