| |

LFCS 128 ๐Ÿง Shell Scripting โ€” Debugging

โฑ๏ธ Estimated read: 10 min | |

A script that fails silently or behaves unexpectedly is a script that needs debugging. The shell provides three mechanisms for this: set -x prints each command before executing it, set -v prints each line before parsing it, and the -n flag parses the script without executing it. Beyond these, there are the PS4 prompt customization, the trap DEBUG for per-command breakpoints, the BASH_XTRACEFD for redirecting trace output, and the shellcheck static analyzer for finding problems before the script runs. The LFCS exam tests the basic mechanisms and the syntax of the set flags. The critical point is that set -x is noisy and slows the script, so it should be enabled for a section of the script, not for the entire run, or redirected to a file for later analysis.

Key point: set -x prints each command before executing it. set +x disables tracing. bash -x script.sh traces the entire script from the command line. The PS4 variable controls the trace prompt. The trap DEBUG command runs a handler before each command, which is useful for logging. The set -n (or bash -n) parses the script without executing, which is the syntax check.


Why debugging requires multiple mechanisms

The silent failure problem. A script that exits with 0 but does not perform the expected operation is the hardest failure to diagnose. The set -x trace shows exactly which commands ran and which did not. The set -e strict mode turns silent failures into visible exits. The combination is the standard debugging workflow.

The trace noise problem. A script with a thousand commands produces a thousand trace lines. The output is unreadable and the relevant section is buried. The solution is to enable set -x for the specific function or block, disable it with set +x after, and use PS4 to add context to each trace line.

The parsing problem. A script with a syntax error does not run at all, but the error message from the shell is often unhelpful. The set -n or bash -n script.sh parses the script without executing, reporting syntax errors before the first command runs. This is the first step in any debugging session.

The static analysis problem. Some bugs are visible without running the script: unquoted variables, undefined variables, unused arguments, deprecated syntax. The shellcheck tool parses the script and reports these issues. It is the static analyzer for shell scripts, and it catches the common errors that the runtime trace does not reveal until the specific path is executed.

The trade-off. The runtime trace is precise but verbose. The static analysis is fast but does not catch logic errors. The trap DEBUG is useful but adds overhead. The exam tests the syntax and the use cases for each mechanism.


a. The set flags and the trace output

The set -x flag enables tracing. The set +x flag disables it. The trace output goes to stderr by default.

#!/bin/bash
echo "Start"
set -x
result=$((3 + 4))
echo "Result: $result"
set +x
echo "End"

The output shows each command prefixed with the PS4 prompt (default +).

Start
+ result=7
+ echo 'Result: 7'
Result: 7
End

The set -v flag prints each line before parsing, which is slightly different from set -x. The set -v shows the raw line, including comments and variable references before expansion. The set -x shows the command after expansion, which is what actually runs.

#!/bin/bash
set -v
echo "Hello, $USER"

The set -v output shows echo "Hello, $USER" (the raw line). The set -x output shows echo 'Hello, alice' (the expanded line). The difference matters when debugging variable expansion.

The bash -x script.sh command enables tracing for the entire script without modifying the script. The bash -n script.sh command parses without executing.

# Trace the entire script
bash -x script.sh

# Syntax check
bash -n script.sh

b. The PS4 prompt and the BASH_XTRACEFD

The PS4 variable controls the trace prompt. The default is +. It can be set to include the line number, the function name, the file name, and the timestamp.

#!/bin/bash
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
set -x
my_func() {
  echo "Inside"
}
my_func

The output shows the file, line, and function name for each traced command.

+script.sh:5:: my_func
+script.sh:3:my_func: echo Inside
Inside

The ${BASH_SOURCE} is the script file. The ${LINENO} is the current line. The ${FUNCNAME[0]} is the current function. The [-1] would be the bottom of the call stack, and [0] is the top.

The BASH_XTRACEFD variable redirects the trace output to a file descriptor. The default is 2 (stderr). Setting it to a custom file descriptor sends the trace to a separate file, which is useful for long-running scripts or for CI pipelines where the trace should not be mixed with the normal output.

#!/bin/bash
exec 5> /tmp/trace.log
export BASH_XTRACEFD=5
set -x
echo "This is not traced"
echo "This is traced"

The exec 5> /tmp/trace.log opens file descriptor 5 for writing. The BASH_XTRACEFD=5 sends the trace to that file descriptor. The echo "This is not traced" is executed before set -x and does not appear in the trace. The echo "This is traced" does.


c. The trap DEBUG and the shellcheck analyzer

The trap DEBUG command runs a handler before each command. The handler can log the command, inspect the variables, or implement a conditional breakpoint.

#!/bin/bash
trap 'echo "Executing: $BASH_COMMAND"' DEBUG

echo "First"
echo "Second"

The $BASH_COMMAND variable holds the command that is about to be executed. The handler runs before each command, including the echo commands. The output is:

Executing: echo "First"
First
Executing: echo "Second"
Second

The shellcheck tool is a static analyzer. It is installed separately (apt install shellcheck or brew install shellcheck). It reads the script and reports issues with line numbers and explanations.

shellcheck script.sh

The output is a list of findings. The most common findings are:

  • SC2086: Double quote to prevent globbing and word splitting.
  • SC2154: Variable is referenced but not assigned.
  • SC2164: Use cd ... || exit in case cd fails.
  • SC2046: Quote this to prevent word splitting.

The -s sh flag tells shellcheck to target POSIX shell instead of bash. The -e flag excludes specific codes. The -f flag sets the output format (gcc, json, checkstyle).

# POSIX shell
shellcheck -s sh script.sh

# Ignore a specific code
shellcheck -e SC2086 script.sh

The shellcheck is the static analysis, the set -x is the runtime trace, and the bash -n is the syntax check. Each catches a different class of problem.


Complete Example Session

# ============================================
# PART 1: SET -X TRACE
# ============================================

cat > trace.sh <<'EOF'
#!/bin/bash
echo "Start"
set -x
result=$((3 + 4))
echo "Result: $result"
set +x
echo "End"
EOF
chmod +x trace.sh
./trace.sh
# Output:
# Start
# + result=7
# + echo 'Result: 7'
# Result: 7
# End
# ============================================
# PART 2: BASH -X
# ============================================

bash -x trace.sh
# Traces the entire script.
# ============================================
# PART 3: SYNTAX CHECK
# ============================================

cat > broken.sh <<'EOF'
#!/bin/bash
if [ -f /etc/passwd ]; then
  echo "Found"
EOF

bash -n broken.sh
# Output: broken.sh: line 5: syntax error: unexpected end of file
# ============================================
# PART 4: PS4 PROMPT
# ============================================

cat > ps4.sh <<'EOF'
#!/bin/bash
export PS4='+${BASH_SOURCE}:${LINENO}: '
set -x
echo "Hello"
EOF
chmod +x ps4.sh
./ps4.sh
# Output:
# +ps4.sh:4: echo Hello
# Hello
# ============================================
# PART 5: TRACE TO FILE
# ============================================

cat > tracefile.sh <<'EOF'
#!/bin/bash
exec 5> /tmp/trace.log
export BASH_XTRACEFD=5
set -x
echo "Traced"
EOF
chmod +x tracefile.sh
./tracefile.sh
cat /tmp/trace.log
# Output: + echo Traced
# ============================================
# PART 6: TRAP DEBUG
# ============================================

cat > debugtrap.sh <<'EOF'
#!/bin/bash
trap 'echo "Executing: $BASH_COMMAND"' DEBUG
echo "First"
echo "Second"
EOF
chmod +x debugtrap.sh
./debugtrap.sh
# Output:
# Executing: echo "First"
# First
# Executing: echo "Second"
# Second
# ============================================
# PART 7: SHELLCHECK
# ============================================

cat > unquoted.sh <<'EOF'
#!/bin/bash
files="*.txt"
echo $files
EOF
shellcheck unquoted.sh
# Output: SC2086 (info): Double quote to prevent globbing and word splitting.
# ============================================
# PART 8: SHELLCHECK IGNORE
# ============================================

shellcheck -e SC2086 unquoted.sh
# No output: the warning is suppressed.
# ============================================
# PART 9: CONDITIONAL TRACE
# ============================================

cat > condtrace.sh <<'EOF'
#!/bin/bash
if [ "$1" = "--debug" ]; then
  set -x
fi
echo "Processing"
EOF
chmod +x condtrace.sh
./condtrace.sh --debug
# Output:
# + echo Processing
# Processing

The nine parts trace the debugging: set -x, bash -x, syntax check, PS4, trace to file, trap DEBUG, shellcheck, ignored codes, and conditional trace.


Quick Reference

set Flags

FlagPurpose
set -xTrace commands
set +xDisable trace
set -vTrace lines (raw)
set -nParse without executing

Command-Line Flags

FlagPurpose
bash -x script.shTrace entire script
bash -n script.shSyntax check

PS4 Components

ComponentMeaning
$BASH_SOURCEScript file
$LINENOLine number
$FUNCNAME[0]Current function
$BASH_COMMANDCommand being executed

Trace Redirect

VariablePurpose
BASH_XTRACEFDFile descriptor for trace
exec N> fileOpen file descriptor

shellcheck Flags

FlagPurpose
-s shPOSIX shell target
-e SC2086Ignore specific code
-f jsonOutput format

Best Practices

โœ… Do This:

set -x
<!-- Enable trace for the section that is failing --> # โœ…
bash -n script.sh
<!-- Syntax check before running -->                # โœ…
shellcheck script.sh
<!-- Static analysis in CI -->                       # โœ…

โŒ Don’t Do This:

# Enable set -x for the entire script
<!-- The output is unreadable -->                    # โŒ
# Ignore shellcheck findings
<!-- They are the common bugs -->                    # โŒ
# Assume no trace means no bug
<!-- Silent failures are invisible -->               # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Trace unreadableNo PS4 customizationAdd $LINENO and $FUNCNAME
Trace in stdoutBASH_XTRACEFD not setRedirect to file descriptor
Syntax error hiddenScript runs and fails silentlybash -n
Unquoted variable warningNo shellcheckRun shellcheck
Trace continues after errorset -e not setCombine set -ex

Real-World Examples

1. Trace Section

set -x
code_to_debug
set +x

2. Trace Entire Script

bash -x script.sh

3. Syntax Check

bash -n script.sh

4. Custom PS4

export PS4='+${BASH_SOURCE}:${LINENO}: '

5. Trace to File

exec 5> /tmp/trace.log
export BASH_XTRACEFD=5

6. Trap Debug

trap 'echo "$BASH_COMMAND"' DEBUG

7. Shellcheck

shellcheck script.sh

8. Ignore Code

shellcheck -e SC2086 script.sh

9. Conditional Trace

[ "$DEBUG" = "1" ] && set -x

10. Verbose Trace

set -v

Visual

Trace Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  set -x                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Each command printed to stderr              โ”‚
โ”‚  Prefixed with PS4 prompt                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  set +x                                      โ”‚
โ”‚  Trace stops                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Debugging Levels

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  bash -n script.sh                           โ”‚
โ”‚  โ†’ Syntax check                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  shellcheck script.sh                        โ”‚
โ”‚  โ†’ Static analysis                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  bash -x script.sh                           โ”‚
โ”‚  โ†’ Runtime trace                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Trace commandsset -x
Trace linesset -v
Disable traceset +x
Parse onlyset -n or bash -n
Trace scriptbash -x
Prompt controlPS4
Trace redirectBASH_XTRACEFD
Per-command traptrap DEBUG
Static analysisshellcheck

Key takeaways:

  • set -x is the runtime trace. It prints each command before executing. The set +x disables it. The trace goes to stderr by default.
  • bash -n is the syntax check. It parses the script without executing, reporting syntax errors before the first command runs.
  • PS4 and BASH_XTRACEFD make the trace readable. The PS4 includes the line number and function name. The BASH_XTRACEFD redirects the trace to a separate file.
  • trap DEBUG runs a handler before each command. The $BASH_COMMAND variable holds the command. The handler is useful for logging or conditional breakpoints.
  • shellcheck is the static analyzer. It catches unquoted variables, undefined variables, and other common mistakes before the script runs. It is the first step in any debugging session.

Remember: Debugging is a layered process. The syntax check is first. The static analysis is second. The runtime trace is third. The set -x shows what runs. The set -v shows what is parsed. The bash -n shows what is broken. The shellcheck shows what is wrong. The trap DEBUG shows what is next. The exam tests the flags and the syntax.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!