Skip to content
Bash Debugging: set -x, PS4, traps
step 1/7

Reading — step 1 of 7

Learn

~3 min readProcess Control and Debugging

Bash scripts fail in confusing ways. The toolkit for debugging is built-in — knowing it saves hours.

set -x — trace execution

set -x
name="Ada"
echo "hello $name"
set +x

With -x, every command is printed before execution, prefixed with +:

+ name=Ada
+ echo 'hello Ada'
hello Ada

See variable values, expansion, all of bash's machinery exposed. Toggle with set -x / set +x to scope to a section.

bash -x script.sh — trace from the start without modifying the script.

PS4 — customize the trace prefix

export PS4='+ ${BASH_SOURCE}:${LINENO} ${FUNCNAME[0]:-main}: '
set -x
# ... script ...

Now each trace line shows file, line number, function:

+ /home/me/script.sh:10 main: name=Ada
+ /home/me/script.sh:11 main: echo 'hello Ada'

Great for sourced libraries — you see WHICH file each line came from.

Microsecond timestamps (Bash 5+):

export PS4='+ $EPOCHREALTIME ${BASH_SOURCE}:${LINENO} '
set -x

Profile time spent per command.

set -v — verbose (print before expansion)

set -v
# echoes each line as written, before expansion

Less common than -x. Useful for showing the SOURCE rather than the expanded form.

ERR trap — log on failure

set -euo pipefail
trap 'echo "failed at $LINENO with exit $?"' ERR

false                        # triggers ERR before script exits

With set -e, the script exits on any failure. The ERR trap fires first — chance to log the location.

Detailed error logging:

on_error() {
    local line=$1
    local cmd=$2
    echo "FAIL line $line: $cmd" >&2
    echo "call stack:" >&2
    local i
    for ((i = ${#FUNCNAME[@]} - 1; i >= 0; i--)); do
        echo "  $i: ${FUNCNAME[$i]}() in ${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}" >&2
    done
}
trap 'on_error $LINENO "$BASH_COMMAND"' ERR

$BASH_COMMAND is the command currently being executed. BASH_SOURCE and BASH_LINENO arrays trace the call stack.

DEBUG trap — fires before EVERY command

trap 'echo "about to run: $BASH_COMMAND"' DEBUG

Very slow but extremely powerful for tracing complex scripts. Don't ship enabled — wraps every command.

ShellCheck

Not built-in but essential. Static analysis for shell scripts:

shellcheck script.sh

Catches:

  • Unquoted variables
  • Misused test operators
  • Subshell variable scope bugs
  • Word-splitting hazards
  • POSIX vs bash mismatches

Run on every commit. Free, open source, brilliantly accurate.

bash -n — syntax check without execution

bash -n script.sh            # parse only, don't run

Fast sanity check before deploying. Catches bracket mismatches, unfinished if/then.

Common debugging patterns

# Step-by-step
echo "DEBUG: x=$x" >&2

# Pause and inspect
read -p "press enter to continue"

# Conditional verbose
DEBUG=${DEBUG:-0}
[[ $DEBUG -eq 1 ]] && echo "debug info" >&2

# Run with: DEBUG=1 ./script.sh

IDE-style debuggers exist (bashdb) but the built-ins handle 95% of bugs.

Common mistakes

  • Forgetting to disable set -x — output noise floods. Toggle off after the suspect section.
  • Not setting custom PS4 — default trace doesn't show line numbers. Always set PS4 with line info.
  • Trapping ERR without set -e — ERR only fires when a failure would have triggered set -e behavior.
  • set -x in production logs — leaks command lines that may contain secrets. Disable for production runs.
  • Skipping shellcheck — it catches things you never thought to check. Use it.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…