bash best practices


Script header

#!/usr/bin/env bash

set -o errexit -o nounset -o pipefail
shopt -s inherit_errexit lastpipe
#!/bin/sh -e

Arrays

Declaration

local -a xs=()
declare -a xs=()
local -A xs=()
declare -A xs=()
local -a xs
declare -a xs
local -A xs
declare -A xs

# Doesn't work with nounset:
echo "${#xs[@]}"

Expansion

func ${arr[@]+"${arr[@]}"}
# Doesn't work with nounset:
func "${arr[@]}"
# Expands to 0 arguments instead of 1:
declare -a arr=('')
func "${arr[@]+"${arr[@]}"}"

unset

unset -v 'arr[x]'
unset -v 'arr[$i]'
# May break due to globbing:
unset -v arr[x]
# In addition, possible quoting problem:
unset -v arr[$i]
# Doesn't work for some reason:
unset -v 'arr["x"]'
unset -v 'arr["]"]'
# Also rejected:
unset -v 'arr["$i"]'

# An insightful discussion on the topic:
# https://lists.gnu.org/archive/html/help-bash/2016-09/msg00020.html

errexit

Command substitution

shopt -s inherit_errexit

foo() { echo foo ; }
bar() { false ; echo bar >&2 ; }

output="$( bar )"
foo "$output"

# If inherit_errexit is unavailable, you can do
#output="$( set -e; bar )"
foo() { echo foo ; }
bar() { false ; echo bar >&2 ; }

# This will print both "foo" and "bar":
foo "$( bar )"
# This will also print "foo":
foo "$( false )"
foo() { echo foo ; }
bar() { false ; echo bar >&2 ; }

# This will still print both "foo" and "bar".
output="$( bar )"
foo "$output"

# This won't print anything.
output="$( false )"
foo "$output"

Process substitution

shopt -s lastpipe

result=()
cmd | while IFS= read -r line; do
    result+=("$( process_line "$line" )")
done
# Without lastpipe, the loop is executed is a subshell,
# and the array will be empty:
result=()
cmd | while IFS= read -r line; do
    result+=("$( process_line "$line" )")
done
# errexit doesn't work for <( cmd ) no matter what:
while IFS= read -r line; do
    process_line "$line"
done < <( cmd )
# This will be printed even if cmd fails:
echo 'should never see this'
# This breaks if $output contains the \0 byte:
output="$( cmd )"

while IFS= read -r line; do
    process_line "$line"
done <<< "$output"

Functions

foo() { false ; echo foo >&2 ; }

foo
echo ok
foo() { false ; echo foo >&2 ; }

# This will print "foo" no matter what.
if foo; then
    echo ok
fi

# Same below.
foo && echo ok
foo || echo fail

# It currently appears to be completely impossible to
# execute a function inside a conditional with errexit
# enabled. Therefore, you should try to avoid this
# whenever possible.