Sagar.BlogArticle
All posts
All posts
Bash

Debugging Bash Scripts — Techniques and Tools

Track down bugs in your Bash scripts with set -x trace output, LINENO, PS4, and common debugging patterns. Learn to read trace output and isolate failures.

February 2, 20266 min read
BashDebuggingset -xScripting

Debugging Bash scripts is different from debugging compiled code — you can't attach a debugger. Instead, you use the shell's own transparency: trace modes, verbose output, and strategic echo statements. With the right techniques, tracking down a bug takes minutes, not hours.

set -x — Execution Trace

#!/usr/bin/env bash
set -x    # enable trace

name="Alice"
echo "Hello, $name"

# Output:
# + name=Alice
# + echo 'Hello, Alice'
# Hello, Alice

# Enable/disable for just a section
set +x
sensitive_operation
set -x

Customising PS4 for Better Trace Output

PS4 is the prompt prefix for set -x trace lines (default is +). You can make it show the script name, line number, and function name:

#!/usr/bin/env bash

# Rich PS4: shows script, line, function
export PS4='+(${BASH_SOURCE##*/}:${LINENO})[${FUNCNAME[0]:-main}]: '

set -x

function greet {
    local name="$1"
    echo "Hello, $name"
}

greet "Alice"

# Trace output becomes:
# +(script.sh:8)[main]: greet Alice
# +(script.sh:4)[greet]: local name=Alice
# +(script.sh:5)[greet]: echo 'Hello, Alice'

Running a Script in Debug Mode

# Without modifying the script:
bash -x script.sh

# Check syntax without running:
bash -n script.sh

# Both:
bash -nx script.sh

# Trace only one line with subshell
(set -x; some_command)  # trace ends when () subshell exits

Common Bugs and How to Find Them

Top 5 Bash Bug Patterns

  1. Quoting: Unquoted $var splits on spaces. Always quote.
  2. Spaces around =: var = value runs var as a command.
  3. [ vs [[: Using == inside [ ] or >/< without escaping.
  4. Subshell scope: Setting variables in $(), pipes, or ( ) — changes not visible outside.
  5. Integer comparison with strings: [ "10" > "9" ] is lexicographic — use [[ 10 -gt 9 ]] or (( 10 > 9 )).
# Debug print helper — shows variable name and value
function dbg {
    printf '[DEBUG %s:%s] %s=%q
' "${BASH_SOURCE[1]##*/}" "$BASH_LINENO" "$1" "${!1}" >&2
}

name="Alice Smith"
count=42
dbg name     # [DEBUG script.sh:8] name='Alice Smith'
dbg count    # [DEBUG script.sh:9] count=42

# Pause and inspect (interactive debugging)
function breakpoint {
    echo "=== BREAKPOINT ===" >&2
    echo "Line: $BASH_LINENO" >&2
    read -rp "Press Enter to continue (or Ctrl+C to abort)..." >&2
}

Using ShellCheck

ShellCheck — static analysis for Bash

ShellCheck is the most useful Bash tool after the shell itself. It catches:

  • Unquoted variables that could split
  • Incorrect [ vs [[ usage
  • Pipes to while (subshell scope loss)
  • Incorrect return/exit usage
  • Dozens of other common mistakes

Install: brew install shellcheck or apt install shellcheck Run: shellcheck script.sh VS Code: install the ShellCheck extension for inline warnings.

Quick Check

What does `$BASH_LINENO` contain inside a function?

Exercise

This script has multiple bugs — find and fix them all:

#!/usr/bin/env bash
name = "Alice"
files=file1.txt file2.txt file3.txt

for f in $files; do
  if [ -f $f ]; then
    count=$(wc -l $f)
    echo $name has $count lines in $f
  fi
done

Hint: there are at least 4 bugs.