set, shopt — Shell Option Modifiers
Bash is permissive by default, and that can hide problems: a failed command may not stop your script, a pipeline may report success even when something inside it failed, and an unset variable may quietly become an empty string. set -e, set -u, set -o pipefail, and shopt exist to change those defaults and make Bash behave more predictably and safely.
set -e — Exit On Error
By default, a script keeps running even after a command fails — you’ve relied on this constantly throughout this course, checking $? or using if to react to failure deliberately. set -e (also called errexit) changes that default: the script exits immediately the moment any command fails, without you having to check for it explicitly.
#!/usr/bin/env bash
set -e
echo "About to fail"
false
echo "This line never runs"About to failWithout set -e, that last echo would have run anyway, printing right after the silent failure of false — exactly the behavior every script in this course has had by default up to this point.
The Surprising Cases Where set -e Doesn’t Trigger
set -e has several well-known exceptions, and not knowing them is a common source of scripts that seem to have set -e enabled but don’t actually stop when you’d expect:
A command tested by if, while, or until doesn’t trigger set -e, because its failure is the whole point of the test:
set -e
if false; then
echo "won't print"
fi
echo "Script keeps going — this is expected"Script keeps going — this is expectedThe left-hand side of && or || doesn’t trigger set -e either, for the same reason — its exit status is already being deliberately handled:
set -e
false || echo "handled — script continues"
echo "Still running"handled — script continues
Still runningOnly the last command in a pipeline determines whether set -e fires, by default — an earlier stage’s failure is invisible to set -e unless pipefail is also enabled, covered next.
And the single most surprising case, worth its own full example: the exit status of a command substitution used to initialize a local variable is masked, because the exit status Bash actually sees is local’s own (which almost always succeeds), not the command substitution’s:
set -e
get_value() {
local result=$(false)
echo "This still runs, even though false clearly failed"
}
get_value
echo "Script continued past a real failure"This still runs, even though false clearly failed
Script continued past a real failureThis exact pattern is worked through in full, along with its fix, in this chapter’s Shell-Safety Considerations section.
set -u — Error On Unset Variables
set -u (also nounset) turns referencing an unset variable into an immediate error, instead of silently treating it as an empty string:
set -u
echo "$undefined_variable"bash: undefined_variable: unbound variableThis is genuinely useful for catching typos — $flie instead of $file silently expands to nothing without set -u, potentially producing a confusing downstream failure far from the actual mistake, but errors immediately and clearly with it enabled.
Positional parameters are covered by this too: a script that expects $1 but was called with no arguments will error immediately under set -u, rather than quietly treating $1 as empty — which is exactly the kind of missing-argument bug you use to check with $# for explicitly.
See the failure on script:
cat > fail.sh << 'EOF'
set -u
echo "$1"
EOF
chmod +x fail.sh
./fail.sh./fail.sh: line 2: $1: unbound variableIf you genuinely want to allow a variable to be unset and default to empty, use ${var:-} to opt into that deliberately.
set -o pipefail — Making Pipeline Failures Visible
As covered previously, a pipeline’s exit status is normally just its last command’s — an earlier stage failing is invisible unless you manually check PIPESTATUS. pipefail changes the pipeline’s overall exit status to reflect the rightmost failing stage, not just the last one:
false | true
echo "Without pipefail: $?"
set -o pipefail
false | true
echo "With pipefail: $?"Without pipefail: 0
With pipefail: 1With pipefail enabled, set -e can now correctly detect a failure anywhere in a pipeline automatically — without it, set -e would see only true’s successful exit status and have no idea false failed at all.
Putting It Together: set -euo pipefail
This combined form shows up constantly at the top of production Bash scripts. Here’s exactly how it parses: -e and -u are short flags, -o takes a following word as its argument, and pipefail is that argument — so set -euo pipefail is really set -e -u -o pipefail, just written more compactly. pipefail has no short-flag form of its own, which is why it always has to be spelled out after -o.
#!/usr/bin/env bash
set -euo pipefail
echo "Strict mode is active"With all three enabled: any command failure stops the script (except in the documented exception cases above), any unset variable reference is an error, and pipeline failures are no longer hidden behind a successful last stage. This is a genuinely strong default for most scripts — the exceptions above are worth knowing precisely because this combination is so commonly used.
shopt — Additional Shell Behavior Options
shopt is a separate mechanism from set, controlling a different set of Bash-specific behavioral toggles. Enable an option with -s (set), disable one with -u (unset):
shopt -s nullglobNote
shopt -u and set -u are unrelated despite sharing a letter — shopt -u optionname disables a specific shopt option, while set -u (covered above) is the entirely separate “error on unset variables” behavior. This naming overlap trips people up occasionally; keep the two mechanisms mentally separate.
nullglob
An unmatched glob pattern, by default, runs a for loop once with the literal pattern text rather than zero times. nullglob fixes exactly that:
shopt -s nullglob
for f in /tmp/shell-course-demo/*.nonexistent; do
echo "Found: $f"
done
echo "Loop completed with zero iterations, as expected"Loop completed with zero iterations, as expectedfailglob
Stricter still: with failglob enabled, an unmatched glob pattern makes the command itself fail with an error, rather than silently expanding to nothing or to the literal pattern text — useful when a script should treat “no matches” as a real problem worth stopping for, not a case to quietly continue past.
globstar
Enables ** for recursive directory matching:
shopt -s globstar
echo **/*.mdWith globstar enabled, ** matches through any depth of subdirectories, not just the current one — **/*.md finds .md files anywhere underneath the current directory, recursively, without needing to shell out to find for a task this simple. You can combine with tr if readability matters:
echo **/*.md | tr ' ' '\n'Best Practices
- Start most scripts with
set -euo pipefail, and know the exceptions above well enough to recognize when a command’s failure genuinely won’t be caught. - Always split
local varandvar=$(command)onto two separate lines, in every script, regardless of whetherset -eis active — this fully sidesteps the local/command-substitution masking problem covered in this chapter’s Shell-Safety Considerations section. - Use
${var:-}deliberately when a variable is genuinely allowed to be unset underset -u, rather than disablingset -uentirely just to accommodate one variable. - Enable
nullglobfor any script that loops over a glob pattern that might not match anything — it’s a one-line fix for a genuinely surprising default behavior.
Shell-Safety Considerations
The local/command-substitution masking bug from earlier in this chapter is worth seeing fixed directly — it’s the single most cited set -e surprise for exactly this reason: it looks like set -e isn’t working at all, when really it’s working exactly as documented, just not in the place you’d expect.
Naive (broken) version:
set -e
get_value() {
local result=$(false)
echo "Reached this line despite a real failure above"
}
get_value
echo "Script kept running"Reached this line despite a real failure above
Script kept runningfalse genuinely failed inside the command substitution — but the exit status Bash actually evaluates for set -e’s purposes is the exit status of the whole local result=$(false) statement, and that status belongs to local itself, which succeeded at declaring the variable regardless of what its assigned value’s command produced.
Corrected version — declare and assign on separate lines:
set -e
get_value() {
local result
result=$(false)
echo "This line is never reached now"
}
get_value
echo "This never prints either"Nothing prints.f
No output at all — result=$(false) on its own line has its own real exit status, exactly matching false’s, and set -e correctly stops the script right there. This two-line pattern — local var followed by a separate assignment — is worth using as a fixed habit anywhere local meets a command substitution, whether or not set -e happens to be active in that particular script.
Hands-On: Strict Mode, Piece By Piece
1. Set up a working directory.
mkdir -p ~/shell-course/ch18
cd ~/shell-course/ch182. Confirm set -e’s basic behavior and one of its exceptions.
cat > basic-e.sh << 'EOF'
#!/usr/bin/env bash
set -e
echo "Before"
false
echo "After — should NOT print"
EOF
chmod +x basic-e.sh
./basic-e.sh
cat > exception-if.sh << 'EOF'
#!/usr/bin/env bash
set -e
if false; then
echo "won't print"
fi
echo "Script continues — this SHOULD print"
EOF
chmod +x exception-if.sh
./exception-if.sh3. Trigger set -u on a typo’d variable name.
cat > strict-u.sh << 'EOF'
#!/usr/bin/env bash
set -u
file_name="report.txt"
echo "$flie_name"
EOF
chmod +x strict-u.sh
./strict-u.sh4. Compare pipeline exit status with and without pipefail.
false | true
echo "Without pipefail: $?"
set -o pipefail
false | true
echo "With pipefail: $?"
set +o pipefail5. Reproduce the local masking bug, then fix it.
cat > masking-broken.sh << 'EOF'
#!/usr/bin/env bash
set -e
get_value() {
local result=$(false)
echo "Reached despite failure"
}
get_value
echo "Script kept running"
EOF
chmod +x masking-broken.sh
./masking-broken.sh
cat > masking-fixed.sh << 'EOF'
#!/usr/bin/env bash
set -e
get_value() {
local result
result=$(false)
echo "Never reached"
}
get_value
echo "Never reached either"
EOF
chmod +x masking-fixed.sh
./masking-fixed.sh; echo "Exit status: $?"6. Try nullglob against a pattern with no matches.
shopt -s nullglob
for f in ./*.nonexistent; do
echo "Found: $f"
done
echo "Zero-iteration loop completed cleanly"
shopt -u nullglob7. Clean up.
cd ~
rm -rf ~/shell-course/ch18You’ve now enabled set -e, set -u, and pipefail individually, seen exactly where set -e doesn’t trigger — including the local/command-substitution case in full, both broken and fixed — and used nullglob to fix the empty-glob surprise from earlier chapters. The next chapter covers traps and cleanup: running code automatically on exit, error, or signal, and the patterns for safely cleaning up temporary resources no matter how a script ends.