TS-32: Bash
This technical standard extends TS-31: Unix Shells and POSIX Standards with guidelines for using the Bash shell specifically. Everything in TS-31 applies here, too; TS-32 extends TS-31.
See also TS-62: Make for guidance on Makefiles, which commonly wrap Bash scripts and recipes covered by this standard.
Choosing Bash
Bash is the RECOMMENDED shell for scripts that will be used only internally within an organization. It is RECOMMENDED to standardize on Bash for all Unix scripting across an organization, to promote code reuse.
For scripts that will be distributed outside of an organization, or otherwise to environments in which the shell is unknown or can’t be controlled, it is RECOMMENDED to stick to POSIX-compliant syntax – as covered by the TS-31: Unix Shells and POSIX Standards technical standard.
File name extensions
Bash file names may have the .sh extension or no extension. Bash file names
MUST NOT have other extensions such as .bash. Instead, use the shebang (see
below) to identify the target interpreter.
Shebang (#!)
The first line of a Bash file MUST be the shebang (#!, also known as a
hashbang). This indicates to the shell which interpreter to use. The classic
Bash shebang is:
#!/bin/bash
But the env-style shebang is RECOMMENDED:
#!/usr/bin/env bash
The difference between the two is that the classic shebang bets everything on one path, while the env shebang looks for the Bash binary in more places, and thus the script has a greater chance of being successfully run across different environments. The trade-off is ever-so-slightly slower startup time, as the PATH environment variable needs to be searched to find the Bash binary.
Tip
If you’re using the classic Bash shebang, and if you get an error similar to
"command not found" when running the script, then almost certainly the path to
the Bash interpreter is wrong for the current system. Use whereis bash or
find ./ -name bash to find the path to the bash executable. Some possible
locations include:
/bin/bash/sbin/bash/usr/local/bin/bash/usr/bin/bash/usr/sbin/bash/usr/local/sbin/bash
If a script is written to be POSIX-compliant, use either of the following
shebangs instead. This will trip the Bash program to executing the script in
POSIX-compliant mode, equivalent to passing the --posix option to bash on
the command line (which in turn is equivalent to using the sh command to run
Bash in POSIX-compliant mode).
#!/bin/sh #!/bin/env sh
Again, the second env-style shebang will have wider compatibility and is therefore RECOMMENDED.
Options
It is RECOMMENDED to enable "strict mode" in all Bash scripts, by including the following line after the shebang:
set -euo pipefail
The -o pipefail option is a Bashism that extends POSIX strict mode (-e) to
pipelines. It causes a pipeline to return a failure status if any command in the
pipeline fails, rather than just the last command. This is useful for catching
errors in pipelines that would otherwise pass by uncaught by the set -e exit
mode.
The -e, -u, and -o pipefail options, used in combination, mean that the
script will exit whenever any command returns a non-zero exit code (indicating
an error), even if the command is executed within a pipeline, and when unset
variables are encountered. This configuration is RECOMMENDED for most Bash
script use cases, as it will help catch many common classes of bugs.
Sourcing
Use the POSIX-compliant dot notation for sourcing files. The alternative
source keyword is more explicit but less portable (it is a Bash extension).
# ❌ source functions.sh # ✅ . functions.sh
In Bash, sourced files are not truly modular. Sourcing merely provides a way of breaking up long scripts into smaller chunks, for easier code management. When sourcing a file, all Bash does is copy the contents of the sourced file and implant them into the current script at runtime.
Bash authors need to be careful about doing things like change directory (cd)
within a child script, as doing so may break the relative paths for subsequent
imports in the parent script. Best practice is for every sourced file to return
the session to its pre-existing state. For example, if a source file uses cd,
be sure to have the script cd back to the prior working directory before
execution passes back to the parent script.
Bash authors also must be careful about mutating variables, declared in the parent script, in sourced scripts. Any variables that are defined in the parent script and referenced in the child script are handled as though all the code had been written in a single file. This is a common source of bugs – when two distinct values are accidentally assigned to variables of the same name, but declared in different files.
A good solution is to pass variables to source files, like this:
hello="helloworld" . install.sh $hello
In install.sh:
# Prints "helloworld". echo $1
The ability to source a script with arguments is a Bashism. In other shells such
as Dash, positional parameters like $1 parameters will not be defined in this
case.
Variables
In Bash, local variables can be used to scope data to the functions the variables are declared in.
hello="hello"
hello() {
local hello="world"
echo $hello # → "world"
}
echo $hello # → "hello" (not modified).This syntax is Bash-specific and it is RECOMMENDED to use it for all variable declarations within functions, so as not to unnecessarily pollute the global namespace and to prevent unexpected overwrites of global variables. Pay particular attention to declaring loop variables as local, too – this is a common source of bugs:
local i
for i in {1..5}; do
echo $i
doneWhere the local keyword is omitted, include a comment explaining why it is not
used. There will be many use cases where functions are designed to update global
variables – that’s fine, just be sure to leave a comment to be explicit in your
intent.
Variable declaration and assignment MAY be on different lines, but try to always initialize variables with meaningful default values.
some_function() {
local code=0
# …
code=2
return ${code}
}Arrays
Arrays, which are a Bash-specific shell extension, are used to store lists of elements. This data structure is RECOMMENDED for safely expanding lists into individual elements, such as arguments given to a command or elements given to a loop. Arrays are perfect for these use cases – this is their intended purpose in the language design.
Arrays SHOULD NOT be used as a basic for constructing complex data structures – this is not their intended use case.
declare -a flags
# ✅
flags=(--foo --bar='baz') # Initial assignment of flags.
flags+=(--greeting="Hello ${name}") # Append to the list of flags.
mybinary "${flags[@]}" # Expand the flags to an arguments list.
# ❌ This won't work as expected.
flags='--foo --bar=baz'
flags+=' --greeting="Hello world"'
mybinary ${flags}The names of variables that hold array structures SHOULD be pluralized. In loops, the singular form of the array name SHOULD be used for the iteration variable.
for zone in "${zones[@]}"; do
# Do something with "${zone}".
doneFunctions
Prefer the POSIX-compliant function declaration syntax. It is cleaner, more readable, and more portable.
# ❌ Explicit function declaration.
function some_function () {
return 0
}
# ✅ Implicit function declaration.
some_function() {
return 0
}Returning values via namerefs
As covered in TS-31: Unix Shells and POSIX Standards → Functions, the RECOMMENDED way to return non-integer data from a function is to write it to stdout and capture it with command substitution. That approach runs the function in a subshell, which has a performance cost and means the function cannot set a variable directly in the caller’s scope.
Where that overhead matters, or where a function needs to populate a variable
in the caller’s environment directly – for example, an array or another
structured value that would be awkward to serialize through stdout – use a
Bash nameref (declare -n). The caller passes the name of a variable it owns
as an argument, and the function assigns to that name as if it were a local
alias for the caller’s variable:
get_config_value() {
local -n result_ref="$1"
local key="$2"
result_ref="$(grep "^${key}=" config.env | cut -d '=' -f2-)"
}
get_config_value value "API_KEY"
echo "${value}"Namerefs avoid the subshell entirely, so they are useful in loops or performance-sensitive code paths where repeated command substitution would be costly. They are a Bash-specific extension and MUST NOT be used in scripts that need to remain POSIX-compliant.
Control flows
For conditional expressions, it is RECOMMENDED to use the Bash-specific
double-bracket syntax, [[ … ]], over [ … ], test, and /usr/bin/[.
In older versions of Bash, using single bracket syntax with && or || could
cause syntax issues. Using the double bracket syntax is better, therefore, for
backwards compatibility with older implementations of Bash.
The double bracket syntax is preferred for other reasons, too. It prevents pathname expansion and word splitting, which eliminates a common class of bugs in shell scripts. It also allows for regular expression matching, which the single bracket syntax does not support.
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then echo "Match" fi if [[ "filename" == "f*" ]]; then echo "Match" fi # For comparison, this gives a "too many arguments" error as # f* is expanded to the contents of the current directory. if [ "filename" == f* ]; then echo "Match" fi
For clarity, use == for equality rather than =, even though both work. The
former requires the use of the preferred Bash-specific [[ … ]] syntax. The
latter can be confused with an assignment.
# ✅
if [[ "${my_var}" == "val" ]]; then
do_something
fi
# ❌
if [[ "${my_var}" = "val" ]]; then
do_something
fiBe careful when using < and > in [[ … ]], which performs a lexicographical
comparison. Use … or -lt and -gt for numerical comparison.
# ✅
if (( my_var > 3 )); then
do_something
fi
# ✅
if [[ "${my_var}" -gt 3 ]]; then
do_something
fi
# ❌ Probably unintended lexicographical comparison.
# True for 4, false for 22.
if [[ "${my_var}" > 3 ]]; then
do_something
fiPrefer to use -z and -n to test for zero-length and non-empty strings
respectively. Alternatively you can do an equality check against a literal ""
value, but if you do ensure that you quote on the empty side.
# ✅
if [[ -z "${my_var}" ]]; then
do_something
fi
# ✅
if [[ -n "${my_var}" ]]; then
do_something
fi
# ✅ This is okay, but ensure quotes on the empty side.
if [[ "${my_var}" == "" ]]; then
do_something
fi
# ❌ Be explicit and use `-n` here.
if [[ "${my_var}" ]]; then
do_something
fi
# ❌ Do not use filler characters, like this.
if [[ "${my_var}X" == "some_stringX" ]]; then
do_something
fiBe careful about porting Bash scripts, which use the double-bracket [[ … ]]
syntax, to other shells. Other shells have adopted this syntax, too, but the
behavior is not consistent across all of them. Thus, the behavior of a script
using this syntax could be inconsistent if executed in different shells.
In loops, Bash supports omitting the in "$@" part of a for loop. But it is
RECOMMENDED to maintain this, for clarity.
for arg in "$@"; do
echo "${arg}"
doneProcess substitution
Process substitution, <(command) and >(command), lets the output (or
input) of a command be treated as if it were a file. It is RECOMMENDED as a
lightweight alternative to mktemp for one-off cases where a command’s
output only needs to be read once, avoiding the need to create and clean up a
temporary file.
# ❌ Requires creating and cleaning up a temporary file.
tmp="$(mktemp)"
first-stage > "${tmp}"
second-stage --input "${tmp}"
rm -f "${tmp}"
# ✅ No temporary file required.
second-stage --input <(first-stage)Prefer mktemp over process substitution where the same output needs to be
read multiple times, where the consuming command does not support reading from
a named pipe, or where the file needs to persist beyond the current command.
Extended globbing
The shopt -s extglob and shopt -s globstar options extend Bash’s pattern
matching. extglob enables extended pattern operators such as @(pattern-list)
and !(pattern-list), allowing more expressive filename matching within the
shell itself. globstar enables ** to match files recursively through
subdirectories.
shopt -s extglob globstar # Match any file ending in .adoc, at any depth. files=(**/*.adoc) # Match files matching either of two patterns. items=(**/@(foo|bar)*)
These options are RECOMMENDED where they can replace an external find
invocation for simple recursive or pattern-based file matching, avoiding the
overhead of spawning a subprocess. For more complex matching – conditions on
file type, permissions, modification time, and so on – find remains the
appropriate tool.
Arithmetic
Arithmetic expressions MUST be written using … or $…, and MUST
NOT use the $[ … ] syntax, the `expr command, or the let built-in.
# ❌ This Bash syntax is deprecated and non-portable. i=$[2 * 10] # ❌ Unquoted assignments using `let` are subject to globbing word-splitting. let i="2 + 2" # ❌ The expr utility is an external program, not a shell built-in, and is # many times slower than the built-in `(( … ))` syntax for arithmetic. There # are other issues with using `expr` too, such as inconsistencies with the # handling of quoting. i=$( expr 4 '*' 4 )
Numerical comparison MUST NOT be performed inside [[ … ]] expressions, either.
In this context, the < and > operators perform lexicographical comparison
instead. For all numeric comparisons use ` …`.
if (( a < b )); then … fi
Variables MAY be referenced verbatim inside $…. Bash knows to look up
var for you; you don’t need to write ${var} or even $var. Referencing
variables consistently throughout a script is RECOMMENDED, but inside $…
is the one exception where you MAY omit the ${…} wrapping syntax. This
produces slightly cleaner code and tends to read better in the context of
arithmetic expressions.
hr=2 min=5 sec=30 echo "$(( hr * 3600 + min * 60 + sec ))" # → 7530
In the following example, note the use of $… within a string.
echo "$(( 2 + 2 )) is 4"
It is recommended to avoid using … as a standalone statement. But it’s
okay sometimes. In the following example, the result of a calculation is
assigned to a variable.
(( i = 10 * j + 400 ))
References
- Bash reference manual
- Pure Bash bible by Dylan Araps
- Bash guide for beginners by Machtelt Garrels
- Advanced Bash scripting guide by Mendel Cooper
- BASH Programming - Introduction HOW-TO by Mike G
- BashGuide, written by various authors.
- The Bash guide by Maarten Billemont (work-in-progress)
- The Bash hackers wiki, community-maintained documentation (archived).
- Bash shell scripting, a Creative Commons wiki-book by various authors.
- Google’s shell style guide
- Bash scripting quirks and safety tips
by Julia Evans
- [ShellCheck](https://www.shellcheck.net/): Static analysis tool for shell scripts.