Guide Bash-Scripting Intermediate

Debugging

Debugging Bash scripts with set -x, bashdb-style tracing, shellcheck, and practical techniques for finding the exact line that's failing.

3 min read

set -x: Trace Every Command as It Runs

#!/usr/bin/env bash
set -x     # print each command (with expanded variables) before executing it

APP="myapp"
echo "Deploying $APP"
set +x       # turn tracing back off
bash -x deploy.sh          # trace an entire script without editing it
bash -x deploy.sh 2>trace.log     # capture the trace to a file for later review

Output looks like:

+ APP=myapp
+ echo 'Deploying myapp'
Deploying myapp

The + prefix marks trace lines, distinguishing them from the script’s actual output.

Tracing Only a Specific Section

echo "Before the tricky part"
set -x
complicated_function arg1 arg2
set +x
echo "After the tricky part"

Customizing the Trace Prefix (PS4)

export PS4='+ ${BASH_SOURCE}:${LINENO}: '
bash -x deploy.sh

This adds the filename and line number to every trace line — invaluable in larger scripts made of multiple sourced files, where a plain + gives no idea which file/line produced it.

Combining set -x With set -e for Real Debugging

#!/usr/bin/env bash
set -euxo pipefail    # -e (exit on error) + -u (undefined var error) + -x (trace) + pipefail

Running with all four flags during development, then dropping -x for production, is a common workflow.

Syntax Checking Without Running

bash -n script.sh      # parse only, checks syntax without executing anything

shellcheck: Static Analysis (Catches Real Bugs, Not Just Syntax)

shellcheck script.sh

shellcheck catches things a syntax check never will: unquoted variables, unreachable code, common quoting mistakes, and portability issues between bash and POSIX sh. It should be part of every CI pipeline that ships Bash scripts.

# Example finding from shellcheck:
# SC2086: Double quote to prevent globbing and word splitting.
rm $FILE          # flagged
rm "$FILE"          # correct

Printing Variables at Key Points

debug() {
    [ "${DEBUG:-false}" = "true" ] && echo "[DEBUG] $*" >&2
}

debug "APP_NAME is: $APP_NAME"
debug "Retrying, attempt $RETRY of $MAX_RETRIES"
DEBUG=true ./deploy.sh    # toggle debug output without editing the script

Using trap for Error Line Numbers

trap 'echo "Error on line $LINENO: command \"$BASH_COMMAND\" failed" >&2' ERR
set -e

This prints the exact line and command that caused a failure, without needing to run the whole script under -x.

Checking Variable Expansion Interactively

# Quickly test an expansion in an interactive shell before committing it to a script
VAR="test-value"
echo "${VAR^^}"        # verify uppercase expansion works as expected before using it

Production Considerations

  • Never ship a production script with set -x permanently enabled — it can leak secrets (API keys, passwords) passed as arguments or stored in variables directly into logs.
  • Run shellcheck in CI for every script change — most real-world Bash bugs (quoting, unset variables, wrong test operators) are caught by it before the script ever runs.
  • Use PS4 customization with file/line info for any script that sources other files — a bare set -x trace becomes nearly useless once a script grows beyond one file.

Common Debugging Workflow

bash -n script.sh              # 1. syntax check
shellcheck script.sh              # 2. static analysis
bash -x script.sh 2>trace.log       # 3. full trace if something's still wrong
grep -n "^+" trace.log                # 4. review just the traced commands

Quick Interview Answer

bash -x script.sh traces every command as it executes, which is the go-to first step for debugging. bash -n checks syntax without running anything, and shellcheck does static analysis, catching real bugs like unquoted variables that a syntax check would miss. Customizing PS4 to include ${BASH_SOURCE}:${LINENO} makes traces far more useful in multi-file scripts, and trap ... ERR with $LINENO pinpoints exactly where a failure happened without a full trace.”

Common Mistakes

  • Leaving set -x enabled in a production script, potentially leaking secrets into logs.
  • Skipping shellcheck entirely and only relying on manual code review, missing common quoting/scoping bugs.
  • Debugging blind with scattered echo statements instead of using set -x/trap ERR, which give more precise, structured information.

Add More Questions to This Guide

Know a question that should be here? Share it and help the community!

Open Google Form