Bash Scripting Reference

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.

Variable Usage and Expansion

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:

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":

Use "$VAR" liberally to avoid word splitting and glob expansion.

Command Substitution and Execution

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.

Functions and Scope

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.

Error Handling and Debugging

Enable debugging with set -x or set -v:

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.

File and Path 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.

Arrays and Associative Arrays

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.

Advanced Features and Idioms

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.

Common Script Patterns

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.

Error and Exit Codes

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.

Common Failures and Debugging Tips

Summary of Essential Flags and Options

CommandFlagMeaning
set-eExit on error
set-uExit on undefined variable
set-o pipefailPipeline fails if any command fails
set-xPrint commands as they execute
whileIFS= read -r lineReliable line-by-line input
casecase "$var" in ... esacMulti-branch conditional
evaleval "$cmd"Execute dynamic command string
mapfilemapfile -t array < fileRead 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.