Bash & the POSIX Shell Module 9
Robust scripts: errors, traps and debugging interview questions
Interview questions on “Robust scripts: errors, traps and debugging”: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
What does set -e not do?
Answered in the lesson set -e, set -u and pipefail: what they really do
-
Fresher to mid
What does trap "commands" EXIT guarantee, and what does it not?
It guarantees that Bash runs commands when the shell exits: at the end of the script, at an exit, when set -e stops it, and, in Bash 5.3, when an INT, TERM or HUP with no trap of its own ends it. It does not cover SIGKILL (kill -9, the Linux OOM killer), which signal(7) says cannot be caught, and it does not promise that commands finish: a power cut can stop them half-way. It is also not portable: dash runs no EXIT trap when a signal ends the script, so a script for sh traps INT and TERM as well. Write the cleanup so that it is safe to run twice and touches only what the script created.
From the lesson Traps and cleanup: EXIT, ERR and signals
-
Fresher to mid
Why do errors go to stderr instead of stdout, and how is >&2 read?
-
Fresher to mid
What does bash -x script show that a plain bash script does not?
Answered in the lesson Debugging: bash -x, PS4, trap DEBUG and logs
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A deploy script starts with set -e and runs rm -rf "$build_dir/old". A rename in the pipeline config left build_dir unset, and the script deleted /old and everything in it. Explain why set -e did not stop it, and write the preamble and the guard that would have.
- What it tests
- Knowing that set -e reads only exit statuses, that an unset variable expands to nothing unless something turns the expansion into an error, and how to guard a destructive command.
- First step
- Trace the expansion before blaming set -e. build_dir was unset, so "$build_dir/old" became "/old", and rm -rf /old succeeded: the shell saw a status of 0. Then ask which option or expansion form turns a missing value into an error before rm runs.
- Follow-ups
-
- Why does set -u catch build_dir but leave "$@" alone when the script gets no arguments?
- What does ${build_dir:?missing build dir} do, and when is it a better guard than set -u?
- How would ShellCheck have flagged this script before it ran?
From the lesson set -e, set -u and pipefail: what they really do
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A release script runs under set -euo pipefail and picks the newest build with ls -t builds/ | head -n 1. It passed for a year. Since builds/ grew past 3,000 entries, most runs stop with status 141 and a few still pass. Explain the 141 and why it flaps, then fix the line without turning pipefail off.
- What it tests
- Reading 141 as 128 + 13 (SIGPIPE), knowing which stage is killed when a reader exits early, seeing that output size and timing decide the race, and fixing it so that every other failure stays visible.
- First step
- Decode the number before touching the script: 141 is 128 + 13, and 13 is SIGPIPE, so a stage was killed while writing to a pipe whose reader had gone. head -n 1 leaves after one line, so the writer is ls. Then ask what changed: not the script but the size of the listing. Compare ls -t builds/ | wc -c with the size of a pipe.
- Follow-ups
-
- Why does a small listing usually end with 0, although head still reads only one line?
- Which fixes keep pipefail on, and what does each one cost?
- Why is parsing ls output a weak way to find the newest build in the first place?
From the lesson set -e, set -u and pipefail: what they really do
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A service writes /var/run/import.lock when it starts and removes it in an EXIT trap. After the OOM killer ended it, the lock stayed, and every later start refused with "lock held". No trap can run after SIGKILL, so design a lock that recovers without one, and say what each design can still get wrong.
- What it tests
- Designing a lock that does not depend on cleanup code: a PID with a liveness check, or a kernel lock that is released when the holder's descriptors close, and knowing where each one fails.
- First step
- Stop trying to make the cleanup run; it cannot after SIGKILL. Ask instead what the next start can check for itself to tell a live holder from a dead one, and whether the operating system can do the releasing.
- Follow-ups
-
- When does a PID in the lock file lie, and how do you make it lie less often?
- Why does a kernel lock survive SIGKILL, and what can still keep it held?
- Which design would you choose on Linux, and which on a system without flock(1)?
From the lesson Traps and cleanup: EXIT, ERR and signals
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A build script sets workdir=./out at the top. A helper function declares local workdir, fills it with mktemp -d and sets trap 'rm -rf -- "$workdir"' EXIT, single-quoted as ShellCheck asks. When the script ended, ./out and its results were gone and the temporary folder was still there. Explain what ran, and fix it.
- What it tests
- Knowing that a single-quoted trap expands when it runs and in the scope it runs in, that a local variable is gone by then, and how to fix the value at set time safely.
- First step
- Ask when and where the action is expanded. The single quotes delay the expansion until the trap runs, so work out which workdir exists at that moment: the function's local one, or the script's own.
- Follow-ups
-
- Why did a crash inside the helper delete ./out too, but an exit inside it did not?
- How can the trap capture the value safely when it is set?
- What goes wrong when two helpers each set their own EXIT trap?
From the lesson Traps and cleanup: EXIT, ERR and signals
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A nightly import reads records.csv with while IFS=, read -r id amount && [ -n "$id" ]; do import_one; done. It ran green for weeks. The night the export held a blank line after its third record, the job imported 3 records, skipped the 180 below it, and still exited 0. Explain why, and make such a shortfall impossible to miss.
- What it tests
- Knowing how a while loop ends and what status it returns, telling "skip this line" apart from "stop reading", and checking a batch's counts so that a silent shortfall fails the job.
- First step
- Separate "the loop ended" from "the records were imported". Ask what ends this loop, what status a while loop returns when its condition fails, and how many lines the file really holds compared with how many the job counted.
- Follow-ups
-
- Why does the loop end with status 0, although it stopped 180 records early?
- Which count would have caught the shortfall even with the original loop?
- What else can make a read loop lose the last record, and how do you guard against it?
From the lesson Error-handling patterns for scripts
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A deploy job runs cd build && make install from its checkout. On one CI runner it installed another project's files: the job log showed an unexpected absolute path just before make ran, every status was 0, and ShellCheck reports nothing. cd did not fail. What happened, and how do you make the script immune?
- What it tests
- Knowing that cd searches CDPATH for a name without a leading slash and prints the folder it chose, that a successful cd can still land in the wrong place, and how a script guards its folder.
- First step
- Start from the clue in the log: cd prints a path only in a few cases, so work out which one applied. Compare the environment of the odd runner with the others, and look for a variable that changes where cd looks.
- Follow-ups
-
- Why would cd build || exit not have helped here?
- Which ways of writing the folder never consult CDPATH?
- How can the script check that it is in the right folder before it installs anything?
From the lesson Error-handling patterns for scripts
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
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.
- What it tests
- Turning on a trace on a production host without flooding its logs, reading the test builtin's message and the trace line above it, and finding why a value read from outside is sometimes empty.
- First step
- Make a failing run show its values. Add a switch near the top, [ -n "${TRACE-}" ] && { exec 3>>trace.log; BASH_XTRACEFD=3; set -x; }, run the job with TRACE=1 until it fails once, and read the trace lines just above the error: they show the command and the values it really received.
- Follow-ups
-
- Where does the bad value appear in the message, and why does this one look blank?
- Why would switching to [[ $pending -gt 100 ]] hide the bug instead of fixing it?
- How do you check that a value read from outside is a whole number before you test it?
From the lesson Debugging: bash -x, PS4, trap DEBUG and logs
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
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.
- What it tests
- Knowing that xtrace prints every expanded word, keeping secret-handling lines out of the trace, keeping secrets off command lines, and limiting who can read a trace.
- First step
- Count what leaked and where. Each trace line is a command as Bash was about to run it, so the token appears on the assignment and again in the curl argument. Then list everyone and everything that can read that log, including the copies the CI keeps, because the token has to be treated as known to all of them.
- Follow-ups
-
- Which shape of set +x block hides the token completely, and why does a plain set +x still leave a line behind?
- How can curl get the token without it appearing in a trace or a process list?
- Who should be able to read a trace file, and how does the script make sure of that?
From the lesson Debugging: bash -x, PS4, trap DEBUG and logs
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
All Bash & the POSIX Shell interview questions · The Bash & the POSIX Shell track