Comprehensive reference for bash scripting reference covering essential concepts, practical examples, and production best practices.
Bash scripting is the foundation of system automation, deployment, and maintenance across Linux environments. This reference provides precise, actionable details for writing robust, production-grade bash scripts: from variable scoping and command substitution to error handling, debugging, and idiomatic patterns that prevent silent failures.
Variables in bash are untyped, dynamically scoped, and accessible via $VARIABLE or ${VARIABLE} syntax.
name="Alice"
echo "Hello, $name"
echo "Hello, ${name}!" # Equivalent, but ${} enables nesting and modifiers
Common expansions:
${VAR:-default}: use default if VAR is unset or empty.${VAR:=default}: assign default to VAR if unset or empty.${VAR:+value}: expand to value if VAR is non-empty.${VAR:offset:length}: substring extraction.path="/home/alice/projects"
echo "${path:6:5}" # "alice"
echo "${path##*/}" # "projects" (remove longest * prefix)
echo "${path%/*}" # "/home/alice" (remove shortest * suffix)
Key failure mode: unquoted variables. Using echo $PATH vs echo "$PATH":
$PATH expands to a single word: PATH becomes bin:local:usr:bin → bin local usr bin"$PATH" preserves spaces and splits: bin:local:usr:bin.Use "$VAR" liberally to avoid word splitting and glob expansion.
Use $(command) or backticks ` command . Prefer $(...)` for nesting.
files=$(ls *.txt)
date_str=$(date '+%Y-%m-%d %H:%M:%S')
log_dir="/var/log/$date_str"
Critical detail: $(command) runs in a subshell, while command runs in current shell.
count=0
count=$((count + 1)) # arithmetic expansion
count=$((count + $((2 * 3)))) # nested arithmetic
Use set -e to exit on any command failure:
#!/bin/bash
set -e
set -u # exit on undefined variable
set -o pipefail # fail if any command in a pipeline fails
echo "Starting backup..."
tar -czf backup.tar.gz /data
rsync -av /data/ user@server:/backup/
Silent failure: set -e does not catch failures in if, while, or until conditions unless explicitly wrapped.
if ! command; then
echo "Command failed"
fi
Use if ! command; then ... fi consistently.
Define functions with function name { ... } or name() { ... }.
function greet {
local name="$1"
local greeting="${2:-Hello}"
echo "${greeting}, ${name}!"
}
greet "Bob" "Hi"
local declares variables scoped to the function. Without local, variables are global.
Failure mode: local only works in functions; local in scripts applies to the main script scope.
Pass arrays with args=("${@}") and access via ${array[@]} and ${array[i]}.
process_files() {
local files=("$@")
for file in "${files[@]}"; do
echo "Processing $file"
done
}
process_files *.log *.txt
Idiom: use args=("$@") to preserve arguments across function calls.
Enable debugging with set -x or set -v:
set -x: print each command before execution.set -v: print each line as it’s read.set -euo pipefail
set -x
main() {
local input_dir="$1"
local output_dir="$2"
for file in "$input_dir"/*.csv; do
echo "Processing $file"
process_csv "$file" "$output_dir"
done
}
main "$@"
Critical: set -u catches undefined variables early.
echo $missing_var # no error if $missing_var not defined
echo "${missing_var}" # fails if $missing_var not defined
Best practice: always use set -euo pipefail at the top of scripts.
Use trap for cleanup and error handling:
trap 'echo "Error on line $LINENO"' ERR
trap 'echo "Cleaning up..."; rm -f temp.*' EXIT
EXIT runs on script exit; ERR runs on any command failure.
Advanced: use trap 'command' ERR for per-command error handling.
Use [[ ]] for conditional expressions (superior to [ ]):
if [[ -f "$file" && -r "$file" ]]; then
echo "File exists and is readable"
fi
if [[ "$name" == "Alice" ]]; then
echo "Name is Alice"
fi
if [[ $age -gt 18 ]]; then
echo "Adult"
fi
[[ ]] supports regex matching:
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi
Failure mode: [[ ]] does not expand variables in =~ without $.
if [[ $var =~ pattern ]]; then # pattern is literal string
Use while IFS= read -r line for reliable line-by-line processing:
while IFS= read -r line; do
echo "$line"
done < input.txt
-r prevents backslash interpretation; IFS= preserves leading/trailing whitespace.
Idiom: use while IFS= read -r line as the canonical way to read lines.
Standard arrays:
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # "apple"
echo "${fruits[@]}" # "apple banana cherry"
echo "${#fruits[@]}" # 3 (length)
fruits+=("date") # append
Associative arrays:
declare -A config
config[host]="localhost"
config[port]="8080"
config[debug]="true"
for key in "${!config[@]}"; do
echo "$key: ${config[$key]}"
done
Failure mode: declare -A must be used *before* assignment; config[key]=value without declare -A creates indexed arrays.
Use keys=("${!array[@]}") to extract keys from associative arrays.
Use case for multi-branch matching:
case "$mode" in
dev|development)
echo "Running in development mode"
;;
prod|production)
echo "Running in production mode"
;;
*)
echo "Unknown mode: $mode"
;;
esac
case supports glob patterns and regex-like matching.
Use eval for dynamic command construction:
cmd="echo \"Hello, $name\""
eval "$cmd"
# Or:
eval "echo $name"
Idiom: eval is essential for building complex command strings.
Use mapfile (or readarray) to read entire files into arrays:
mapfile -t lines < input.txt
for line in "${lines[@]}"; do
echo "$line"
done
-t removes trailing newlines.
Failure mode: mapfile without -t leaves newline characters in array elements.
Bootstrapping: ensure script runs with correct environment.
#!/bin/bash
set -euo pipefail
# Ensure script is run with correct interpreter
[[ "${BASH_SOURCE[0]}" == "${0}" ]] || echo "Script sourced, not executed"
# Ensure script is in correct directory
cd "$(dirname "${BASH_SOURCE[0]}")"
# Load configuration
if [[ -f .env ]]; then
set -a
source .env
set +a
fi
Idiom: source .env with set -a (export all variables) and set +a to restore.
Idiom: use [[ "${BASH_SOURCE[0]}" == "${0}" ]] to detect if script is run or sourced.
Bash returns exit codes from commands. Use exit 1 or exit $code.
if ! command; then
echo "Command failed" >&2
exit 1
fi
Use exit 0 for success; exit 1 for general failure; exit 2 for configuration issues.
Idiom: exit 1 is the canonical failure exit code.
Use set -e to propagate failures through pipelines:
grep "error" log.txt | awk '{print $1}' | sort | uniq -c
If grep fails, the entire pipeline fails.
echo $PATH → bin local usr bin, echo "$PATH" → bin:local:usr:binset -e doesn't handle if conditions: use if ! command; then ... fi[[ ]] not expanding $ in =~: use [[ $var =~ $pattern ]]while IFS= read -r line not preserving whitespace: use IFS= and -rmapfile leaves newlines: use mapfile -tdeclare -A not creating associative arrays: use declare -A before assignment[[ "${BASH_SOURCE[0]}" == "${0}" ]] detects script vs source| Command | Flag | Meaning |
|---|---|---|
set | -e | Exit on error |
set | -u | Exit on undefined variable |
set | -o pipefail | Pipeline fails if any command fails |
set | -x | Print commands as they execute |
while | IFS= read -r line | Reliable line-by-line input |
case | case "$var" in ... esac | Multi-branch conditional |
eval | eval "$cmd" | Execute dynamic command string |
mapfile | mapfile -t array < file | Read file into array, trim newlines |
This reference is not a tutorial: it is a catalog of known, tested, production-tested patterns and pitfalls in bash scripting. Use it to write scripts that do not fail silently, and to debug when they do.
This page was rewritten on 10 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy