Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 9 – Robust scripts: errors, traps and debugging

Debugging: bash -x, PS4, trap DEBUG and logs

Trace a bash script with set -x and a navigable PS4, send the trace to a log with BASH_XTRACEFD, and narrow bugs with bash -n and ShellCheck.

  • Intermediate
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Trace scripts with set -x and a PS4 that names file, line and function
  • Send the trace to a separate file with BASH_XTRACEFD so stderr stays clean
  • Find syntax errors fast with bash -n and the rest with ShellCheck
  • Isolate a bug to one line and one word, using a scratch script and a scratch input

Before you start

What you’ll learn · Before you start

What you will learn

  • Trace scripts with set -x and a PS4 that names file, line and function
  • Send the trace to a separate file with BASH_XTRACEFD so stderr stays clean
  • Find syntax errors fast with bash -n and the rest with ShellCheck
  • Isolate a bug to one line and one word, using a scratch script and a scratch input

Before you start

On this page

A loop meant to print one city, New Delhi, printed two lines, <New> and <Delhi>, and the script still exited with status 0, so no log said anything was wrong. Reading the code finds a slip like that slowly; a single run under bash -x, which shows each command after Bash has expanded it, shows the loop making one pass per word of the unquoted $name.

You will trace scripts with set -x and a navigable PS4, send the trace to its own log with BASH_XTRACEFD, catch syntax errors with bash -n, read a ShellCheck report, and narrow a bug to one line and one word by shrinking the sample input and the script together.

xtrace prints each command before it runs, already expanded

xtrace: Bash's set -x option (also set -o xtrace): the shell writes each simple command to stderr before running it, with its expansion done, prefixed by PS4 (default '+ '). set +x stops it; BASH_XTRACEFD=N sends the trace to file descriptor N instead. (also set -o xtrace) tells Bash to write each simple command to standard error before running it, with the expansion already done. set +x turns it back off, so a block of a script can be traced without the trace flooding the whole run. The one-shot form is bash -x script, which starts the shell with -x on.

buggy.sh with the default PS4, buggy_ps4.sh with a richer one, and a one-line loop under bash -xc Python · trace_demo_session.py
from session import run, write

# set -x and set +x bracket the block the user wants to trace. The default PS4 is "+ ", so every traced line reads
# "+ command". A richer PS4 names the file, the line and the function: PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '
write("buggy.sh", """#!/bin/bash
greet() {
    local who=${1-world}
    local msg="hello, $who"
    echo "$msg"
}

set -x
x=2
greet "errors-and-debugging"
set +x

echo "set -x block ended"
""")

write("buggy_ps4.sh", """#!/bin/bash
# A richer PS4 that names the file, the line and the function. Single-quote it so it expands at trace time.
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '

greet() {
    local who=${1-world}
    local msg="hello, $who"
    echo "$msg"
}

set -x
x=2
greet "errors-and-debugging"
set +x

echo "set -x block ended"
""")

run("""
chmod +x buggy.sh buggy_ps4.sh
# Default PS4 is just "+ ". Every traced command reads "+ command".
bash buggy.sh
echo "---with a richer PS4---"
bash buggy_ps4.sh
# A one-line trace with -c: the loop over the unquoted $name makes one pass per word.
bash -xc 'name="New Delhi"; for w in $name; do echo "<$w>"; done'
""")

Output

$ chmod +x buggy.sh buggy_ps4.sh
# Default PS4 is just "+ ". Every traced command reads "+ command".
$ bash buggy.sh
+ x=2
+ greet errors-and-debugging
+ local who=errors-and-debugging
+ local 'msg=hello, errors-and-debugging'
+ echo 'hello, errors-and-debugging'
hello, errors-and-debugging
+ set +x
set -x block ended
$ echo "---with a richer PS4---"
---with a richer PS4---
$ bash buggy_ps4.sh
+ buggy_ps4.sh:12:main: x=2
+ buggy_ps4.sh:13:main: greet errors-and-debugging
+ buggy_ps4.sh:6:greet: local who=errors-and-debugging
+ buggy_ps4.sh:7:greet: local 'msg=hello, errors-and-debugging'
+ buggy_ps4.sh:8:greet: echo 'hello, errors-and-debugging'
hello, errors-and-debugging
+ buggy_ps4.sh:14:main: set +x
set -x block ended
# A one-line trace with -c: the loop over the unquoted $name makes one pass per word.
$ bash -xc 'name="New Delhi"; for w in $name; do echo "<$w>"; done'
+ name='New Delhi'
+ for w in $name
+ echo '<New>'
<New>
+ for w in $name
+ echo '<Delhi>'
<Delhi>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_demo_session.py

The first two runs printed every expansion. + x=2 is the assignment to x. + greet errors-and-debugging is the call, with its argument already expanded. The three lines after it are inside the function: + local who=..., + local 'msg=hello, ...' (quoted because the value has a space), and + echo '...'. The trace line comes before the command runs, so a program that fails on a line is named on the previous trace line.

A -c one-liner traces a single expansion in a terminal, with no file to edit

The session’s last command ran the loop from the opening as bash -xc '…': the trace printed + echo '<New>' and then + echo '<Delhi>', one pass per word of the unquoted $name, with each line of output right under the command that wrote it. Written as for w in "$name", the loop makes one pass.

Checkpoint

Check: What set -x shows and when

  1. Question 1 of 2 What does set -x cause Bash to do that it otherwise does not?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Write every simple command to standard error, with its expansion already done, before Bash runs it

    Pair set -x with set +x to trace a block. For the whole run, start Bash with bash -x script or bash -xc 'one line'.

  2. Question 2 of 2 A script with set -e and set -x printed + cp report.csv /mnt/share/ and then an error message from cp, and nothing after that. Which command failed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The cp on that trace line: Bash prints a trace line just before it runs the command, so the error right under it is that cp's own

    Read a trace as what Bash is about to run: the lines under a trace line belong to that command, and under set -e a failure there ends the script before the next trace line.

PS4 names the file, line and function at trace time

The default PS4='+ ' is enough for a short block. For a trace that runs over many lines or spans several functions, a richer PS4 names where each line is. The habit is the one buggy_ps4.sh used:

A PS4 that names file, line and function at trace time Shell · ps4_navigable.sh
#!/bin/bash
# A navigable PS4 that names the file, the line and the function. Single-quote so the three expansions
# fire at trace time, not at PS4-set time.
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '

The three expansions do three jobs. ${BASH_SOURCE##*/} is the current file’s basename: ## is the expansion POSIX calls Remove Largest Prefix Pattern, and here it deletes the longest match of */ from the front. ${LINENO} is the current line number in that file. ${FUNCNAME[0]} is the name of the function currently running, or empty at the top level; the ${…:-main} default replaces the empty value with main. Single-quote the whole PS4 so Bash reads these expansions at trace time, not at PS4-set time; otherwise they freeze to the values they had on the line that set PS4.

The lesson’s second run printed + buggy_ps4.sh:12:main: x=2, + buggy_ps4.sh:13:main: greet errors-and-debugging, then + buggy_ps4.sh:6:greet: local who=..., which is enough to open the file at line 6 and look at the function. For a script with sourced files, the basename makes the file obvious too.

Key idea

Single-quote PS4 so file, line and function expand at trace time; a PS4 in double quotes keeps the values from the line that set it, which stops the trace being navigable.

Send the trace to its own file with a dedicated descriptor

set -x writes to standard error by default. For a script that reports its own errors to stderr, the trace and the error lines mingle, and a user who runs the script sees both. BASH_XTRACEFD, new in Bash 4.1, names a file descriptor, and Bash then writes the trace there instead of to standard error. Open a file as that descriptor, set the variable, and the trace lands in the file:

to_file.sh writes the trace to trace.log through fd 3; stdout and stderr stay clean Python · xtracefd_session.py
from session import run, write

# BASH_XTRACEFD = N sends the trace output to file descriptor N instead of stderr (fd 2). This keeps the script's
# own stderr for errors and the trace in a separate file, so a tee or grep over stderr is not swamped by trace lines.
write("to_file.sh", """#!/bin/bash
# Open file descriptor 3 for writing to trace.log, then send xtrace output there.
exec 3> trace.log
BASH_XTRACEFD=3
PS4='+ ${LINENO}: '

set -x
greet() {
    local who=${1-world}
    echo "hello, $who"
}
greet "errors-and-debugging"
echo "data on stdout"
set +x

exec 3>&-
""")

run("""
chmod +x to_file.sh
bash to_file.sh
echo "---trace.log content---"
cat trace.log
echo "---script's own stderr was clean, trace went to the file---"
""")

Output

$ chmod +x to_file.sh
$ bash to_file.sh
hello, errors-and-debugging
data on stdout
$ echo "---trace.log content---"
---trace.log content---
$ cat trace.log
+ 12: greet errors-and-debugging
+ 9: local who=errors-and-debugging
+ 10: echo 'hello, errors-and-debugging'
+ 13: echo 'data on stdout'
+ 14: set +x
$ echo "---script's own stderr was clean, trace went to the file---"
---script's own stderr was clean, trace went to the file---

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 xtracefd_session.py

The three key lines of to_file.sh are exec 3> trace.log (open fd 3 for writing to trace.log), BASH_XTRACEFD=3 (point xtrace at fd 3), and set -x (switch on tracing). The function then ran with no visible trace; the terminal only showed hello, errors-and-debugging and data on stdout, and trace.log held every traced line. set +x then exec 3>&- at the end stop the trace and close the descriptor; a script that leaves fd 3 open is fine too, since every descriptor closes when the shell exits.

Three common uses: a long run under CI (the trace is an artefact, not a flood on stderr); a script a user runs by hand (the terminal stays clean); and a debug mode a flag enables ([ -n "${TRACE-}" ] && { exec 3>trace.log; BASH_XTRACEFD=3; set -x; }), which the script turns on with TRACE=1 ./script.sh.

A trace writes secrets into the log too

set -x prints every word after expansion, so a line that reads a password or passes a token prints that value in the trace, and anyone who can read the log can read that value. Keep such lines outside the traced block, and keep trace files readable only by the account that runs the script.

Checkpoint

Check: Sending the trace to its own file · PS4 names file, line and function

  1. Question 1 of 2 Why set BASH_XTRACEFD to a file descriptor other than 2 for a long run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The script's own stderr stays clean, so a user who runs the script sees only its real errors while the trace lands in a log the on-call engineer can read

    Pair BASH_XTRACEFD with a condition such as [ -n "${TRACE-}" ] && { exec 3>trace.log; BASH_XTRACEFD=3; set -x; }, so a user can turn on tracing with TRACE=1.

  2. Question 2 of 2 A learner writes PS4="+ ${BASH_SOURCE##*/}:${LINENO}: " with double quotes on line 2 of a script. Why does every trace line then show line 2?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Double quotes expand ${BASH_SOURCE##*/} and ${LINENO} once, when PS4 is set; after that the stored string is the file and line from that one moment

    Single-quote PS4 so file, line and function expand at trace time; the result is the "+ buggy_ps4.sh:12:main: x=2" shape the second session showed.

Catch syntax errors first, then lint with ShellCheck

bash -n is the parse-only mode: the shell reads the script, checks the grammar and reports syntax errors, but runs no command. It is the first thing to try on a script that fails before the first line of output. In Bash 5.3 the message names the line where an unterminated construct began, which is often more useful than the line it ends on:

bash -n on unterminated.sh, and shellcheck on buggy_logic.sh Python · bash_n_session.py
from session import run, write

# bash -n parses the script and reports syntax errors without running any command. Bash 5.3 names the line where
# an unterminated construct began, which turns a cryptic "unexpected end of file" into a navigable hint.
# shellcheck is the lint for the rest: a successful bash -n only says "the parser is happy", not "the script is right".
write("unterminated.sh", """#!/bin/bash
if [ -z "$1" ]; then
    echo "needs an argument"
# missing fi
""")

write("buggy_logic.sh", """#!/bin/bash
# Two slips ShellCheck's rules name on sight: an unquoted expansion that splits on spaces (SC2086), and
# -a used as "and" inside [ ] (SC2166), which is not well defined when an operand looks like an operator.
file=$1
if [ -r $file -a -s $file ]; then
    echo "ok: $file is readable and non-empty"
fi
""")

run("""
chmod +x unterminated.sh buggy_logic.sh
# bash -n parses only; a syntax error names the line of the opening construct.
!bash -n unterminated.sh
# Fixed syntax: shellcheck catches the two slips bash -n does not.
echo "---bash -n is quiet on buggy_logic.sh:---"
bash -n buggy_logic.sh
echo "---shellcheck is not:---"
!shellcheck buggy_logic.sh
""")

Output

$ chmod +x unterminated.sh buggy_logic.sh
# bash -n parses only; a syntax error names the line of the opening construct.
$ bash -n unterminated.sh
unterminated.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
[exit status 2]
# Fixed syntax: shellcheck catches the two slips bash -n does not.
$ echo "---bash -n is quiet on buggy_logic.sh:---"
---bash -n is quiet on buggy_logic.sh:---
$ bash -n buggy_logic.sh
$ echo "---shellcheck is not:---"
---shellcheck is not:---
$ shellcheck buggy_logic.sh

In buggy_logic.sh line 5:
if [ -r $file -a -s $file ]; then
        ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.
              ^-- SC2166 (warning): Prefer [ p ] && [ q ] as [ p -a q ] is not well defined.
                    ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
if [ -r "$file" -a -s "$file" ]; then

For more information:
  https://www.shellcheck.net/wiki/SC2166 -- Prefer [ p ] && [ q ] as [ p -a q...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bash_n_session.py

bash -n unterminated.sh ended with syntax error: unexpected end of file from `if' command on line 2 and status 2. The if on line 2 was missing its fi, and the message named the line where that if began as well as line 5, where the file ended. Bash 5.3 added that starting line to the message; older versions name only the end of the file, which in a long script can be hundreds of lines below the slip.

A clean bash -n only says “the parser is happy”. The next tool is ShellCheck 0.11.0 (shellcheck script.sh), which checks for many slips that a parser cannot see. buggy_logic.sh passed bash -n and failed shellcheck: SC2086 (quote the expansion to prevent word splitting), SC2166 ([ p -a q ] is not well defined; use [ p ] && [ q ]). ShellCheck prints the file, the line, the carets under the span, and a link to the wiki page for every code; most codes name the fix in one line.

The ShellCheck codes that the earlier lessons of this module named are worth keeping on a crib sheet: SC2155 (declare and assign separately, from strict mode), SC2064 (single-quote the trap action, from traps and cleanup), SC2164 and SC2181 (cd … || exit and “check the status directly”, from error-handling patterns), and SC2086 (quote expansions, from this lesson’s example).

A pre-commit hook that runs bash -n and shellcheck stops many slips before they land

Put the pair in .git/hooks/pre-commit and exit non-zero if either fails, and the slip never reaches the branch. shellcheck -o all also turns on the optional checks, such as SC2248 (quote even a variable that holds no special character) and SC2312 (a command substitution whose exit status is lost). The quality-and-tooling module of this track covers the hook in full.

Checkpoint

Check: bash -n for syntax, ShellCheck for the rest

  1. Question 1 of 2 What does bash -n script do, and what does it NOT catch?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It parses the script and reports syntax errors; it does not run any command, so it does not catch quoting slips, dead branches or wrong exit codes

    bash -n is the first thing to try when the script fails before the first output; shellcheck is the second pass, and often the one that names the slip.

  2. Question 2 of 2 ShellCheck reports SC2086 on grep $pattern file. What is the slip and what is the fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Unquoted $pattern is split on whitespace and globbed; quote it: grep "$pattern" file

    Quote every expansion unless you want the split or the glob on purpose; ShellCheck stays quiet where no split can happen, such as inside [[ ]] or on the right of a plain assignment.

Shrink the bug and let a DEBUG trap narrate

A trace of 400 lines is harder to read than a trace of six, and the bug is almost never spread across the whole script. The practice: copy the failing block of the script into a scratch file (bug.sh), the failing input into a scratch file (bug.txt), and run the pair in isolation. The smaller the reproduction, the smaller the trace.

For the few bugs where every line of the smallest trace reads fine, trap … DEBUG is the next tool. A DEBUG trap runs before every simple command (Traps and cleanup describes it, and the next example sets one up); write the current BASH_COMMAND and one named variable to stderr, and the trace becomes a running commentary:

A DEBUG trap that narrates every simple command and shows one named variable Shell · debug_narrator.sh
#!/bin/bash
# A DEBUG trap that narrates every simple command and the current count. set -T (functrace) is required
# for the trap to be inherited into functions.
set -T
trap 'printf '\''%s: %s (count=%s)\n'\'' "$LINENO" "$BASH_COMMAND" "${count-unset}" >&2' DEBUG

Three defences worth building into every long-running script:

  • A --debug flag or a DEBUG environment variable that turns on set -x, so an on-call engineer can turn tracing on without editing the file.
  • A log directory with one file per run (logs/$(date +%Y%m%d-%H%M%S).log), so two runs do not fight for one file.
  • A one-line summary at the end of the trace: how many records, how long, which exit code.

The The list of findings a linter such as ShellCheck prints for a script: each names the file, the line and a code (SC2086, SC2155 …) with the slip it saw, and points to the code's wiki page. is the fastest first look; the trace is the slowest, so leave it for the bugs lint cannot name. The scene below reads as a workflow, from “I know something is wrong” to “I know which line and why”:

Interactive · stage by stage Narrow a Bash bug from "broken" to "that word" Step through the moves that take a bug from a symptom to one line. Each stage names one tool.
Narrow a Bash bug from "broken" to "that word", step 7 of 7. Showing A symptom, bash -n, shellcheck, Shrink the case, set -x with PS4, A DEBUG trap, One line, one word. In focus: One line, one word.a syntax errora code names itA symptomwrong output or exitbash -nsyntax only, runs nothingshellcheckquoting, tests, trapsShrink the casescratch script and inputset -x with PS4file, line, functionA DEBUG trapnarrates each commandOne line, one wordthe fix is clear

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. A symptom first. Something is wrong: a wrong output, a non-zero exit, a hang. Write the symptom down as one sentence, such as "one city prints as two lines"; it is the check the fix has to pass.
  2. Parse with bash -n. bash -n reads the whole script and runs nothing. In the session it stopped unterminated.sh with "unexpected end of file from `if' command on line 2" and status 2. A syntax error ends the search at once.
  3. Lint with shellcheck. A clean parse says nothing about meaning. buggy_logic.sh passed bash -n, and shellcheck 0.11.0 named SC2086 twice (unquoted $file) and SC2166 (-a inside [ ]). A code that explains the symptom also ends the search.
  4. Shrink the case. Still unexplained? Copy the failing block into bug.sh and the failing input into bug.txt, then delete from both while the symptom stays. The smaller the pair, the shorter the trace.
  5. Trace with set -x. Run the small pair under bash -x with a PS4 that names the file, the line and the function, as buggy_ps4.sh did. For a long run, BASH_XTRACEFD sends the trace to a log of its own, as to_file.sh wrote five lines to trace.log.
  6. Let a DEBUG trap narrate. When every traced line looks right, a DEBUG trap with set -T prints $LINENO, $BASH_COMMAND and one variable before each command, so you watch the value change step by step.
  7. One line, one word. The slip is now one line and usually one word: a missing quote, a misspelt name, a test on an empty value. Fix it in bug.sh, run bash -n and shellcheck on the full script again, and check the symptom sentence once more. The two side arrows are the early exits: a syntax error, or a code that explains the symptom.

Checkpoint

Check: Shrink the bug, trap DEBUG, and the workflow

  1. One question A 300-line batch script fails only on one record. What is the fastest first step to a smaller trace?

    Choose one answer.

    Show the answer: One question

    Answer: Copy the failing record into bug.txt and the processing block of the script into bug.sh; run the pair with bash -x

    Pair the shrink with bash -n and shellcheck on the small file; often the fix is clear before the trace runs at all.

Practise: pick the right tool, read the trace

The questions read trace lines, ask what bash -n and ShellCheck report, and pick a tool for a case: a descriptor left open, a DEBUG trap that has to reach into functions, a run that must not change anything. Decide before you check:

Practice that adapts to you

Practise this lesson

8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 Which of these is the trace prefix Bash uses by default?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: '+ ' (a plus sign and a space)

    Change PS4 for a richer prefix with file, line and function. Keep it single-quoted so the expansions fire at trace time.

  2. Question 2 of 8 Which of these are useful expansions inside PS4?

    Choose every answer that is right.

    Show the answer: Question 2 of 8

    Answer:

    • ${LINENO}
    • ${BASH_SOURCE##*/}
    • ${FUNCNAME[0]:-main}

    Combine them for the standard navigable PS4: '+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '.

  3. Question 3 of 8 A script runs exec 3>trace.log; BASH_XTRACEFD=3; set -x. Does the script need exec 3>&- at the end?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: No: Bash closes every file descriptor when the shell exits. The explicit close is tidy but not required

    Close the descriptor when a block of the script has stopped needing the trace, which also tells a reader of the script "the tracing section ended here".

  4. Question 4 of 8 Bash 5.3 names the opening line of an unterminated construct. Which message would it print for a script whose if on line 2 has no closing fi?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: syntax error: unexpected end of file from `if' command on line 2

    The named line is the one to open in the editor; everything after it is suspect until a matching fi is in place.

  5. Question 5 of 8 Which of these ShellCheck codes name a slip that an earlier lesson of this module taught?

    Choose every answer that is right.

    Show the answer: Question 5 of 8

    Answer:

    • SC2155 (declare and assign separately)
    • SC2064 (single-quote a trap action so it expands when the trap fires)
    • SC2164 (cd … || exit)

    The ShellCheck codes the earlier lessons named are worth keeping in a small crib: SC2155 (declare separately), SC2064 (single-quote trap), SC2164 (cd || exit), SC2181 (check status directly), SC2086 (quote expansions).

  6. Question 6 of 8 A DEBUG trap prints LINENO and BASH_COMMAND before every simple command. Which option is needed for the DEBUG trap to fire inside functions?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: set -T (functrace)

    A tracing preamble can switch them all on at once: set -eETuo pipefail is errexit, errtrace, functrace, nounset and pipefail.

  7. Question 7 of 8 A learner wants to see what a script would do without really doing it. Which of these does NOT help?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: set -x alone

    "Dry run" is a script-level feature, not a Bash option; design it into the script so a user can pass --dry-run and see the plan.

  8. Question 8 of 8 A trace line reads: + runner.sh:46:process_batch: curl --fail -sS https://api.example.com/import -d @records.json. What does the trace tell you about where the script is?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: The command about to run is a curl call, from line 46 of runner.sh, inside the function process_batch

    Make the three fields visible on every trace line, and a 500-line log reads line by line.

Interview questions

Warm-up (fresher to 2–5 years): what does bash -x script show that a plain bash script does not?

bash -x prints each command to standard error just before Bash runs it, after every expansion is done, with the prefix in PS4 (by default “+ ”). So you see the values a command really received: + x=2 for an assignment, + echo 'hello, world' for an echo whose argument holds a space, and one line per pass for the body of a loop. A plain bash script prints only what the commands themselves write. A richer PS4 adds the file, the line and the function, and BASH_XTRACEFD sends the trace to a file of its own instead of standard error.

Two questions follow, one about a failure that appears only on the production host and one about what a trace can expose in a shared CI log. Work out what you would check first in each, then open its page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A monitoring script reads a queue count and pages the on-call engineer when it is over 100. It works in testing. In production, about one run in three logs "[: : integer expected" and sends no page, even when the queue is over 100; a rerun by hand passes. Walk through narrowing the bug on that host, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A CI job runs its deploy script under set -x, and the job log, which a dozen engineers can read, holds a line "+ TOKEN=tok_9f3c…" and the same token again inside a curl -H argument. Walk through what leaked, how to stop it without giving up the trace, and the check that catches the next slip.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does set -x print, and how do you trace only part of a script?

    Every simple command, after expansion, on standard error just before it runs, behind the PS4 prefix. set -x and set +x bracket a block; bash -x script traces the whole run.

  2. How do you make each trace line name its file, line and function?

    Set PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' in single quotes, so the expansions fire at trace time rather than when PS4 is set.

  3. How do you send the trace to its own file instead of the screen?

    Open a descriptor for the file with exec 3> trace.log and point the trace at it with BASH_XTRACEFD=3 before set -x; the user sees the normal output while the trace lands in trace.log.

  4. What does bash -n find, and what is new about it in Bash 5.3?

    Syntax errors, without running a single command. Bash 5.3 names the line of the opening construct, so an unexpected end of file points at the block that was never closed.

  5. Which slips does ShellCheck catch that the parser misses?

    Among others SC2086 for unquoted expansions, SC2155 for local with a substitution, SC2064 for a double-quoted trap, SC2164 for cd without || exit, and SC2181 for testing $? instead of the command.

  6. What do you do when a long script fails and the trace is too long to read?

    Shrink the bug: copy the failing block and the input that breaks it into scratch files, and trace that pair; a DEBUG trap can narrate each step when lint and trace are not enough.

Key takeaways

  • set -x prints every simple command before Bash runs it, prefixed with PS4 (default + ). set -x and set +x bracket a block; bash -x script traces the whole run.
  • A navigable PS4 names the file, the line and the function: PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '. Single-quote it so the expansions fire at trace time, not at PS4-set time.
  • BASH_XTRACEFD=N sends the trace to the file descriptor the script opens as N; a user running the script sees the normal output while the trace lands in trace.log.
  • bash -n parses the script and reports syntax errors without running a command; Bash 5.3 names the line of the opening construct, which turns “unexpected end of file” into a navigable hint. ShellCheck catches the slips the parser misses: SC2086 quoting, SC2155 declarations, SC2064 trap quoting, SC2164 cd … || exit, SC2181 “check status directly”.
  • Shrink the bug: copy the failing block and the failing input into scratch files, trace that pair, and let a DEBUG trap narrate every step when the lint and the trace are not enough; a run that still fails on four lines of input tells you where to look much faster than a 400-line log.

Terms in this lesson

xtrace
Bash's set -x option (also set -o xtrace): the shell writes each simple command to stderr before running it, with its expansion done, prefixed by PS4 (default '+ '). set +x stops it; BASH_XTRACEFD=N sends the trace to file descriptor N instead.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.