set Command: Tutorial & Examples
The shell's own settings dial — three letters that turn bash from forgiving into safe.
What It Is
set is the command that changes how the shell itself behaves. Not a program it runs — the shell itself. Most commands you type are files on disk that bash finds and launches; set is a builtin baked into bash, and its whole job is to flip the switches that decide how your shell treats the next command, the next typo, the next thing that goes wrong. Think of it as the settings panel for the most-used tool on any Linux server, and like most settings panels, almost nobody opens it until something breaks.
Here's why this unassuming little word matters more than its three letters suggest. By default, bash is relentlessly forgiving — and that forgiveness is exactly what turns a small script into a disaster. A command fails halfway through, bash shrugs and runs the next line anyway. You reference a variable you forgot to set, bash quietly treats it as an empty string and carries on. A command in the middle of a pipe dies, bash reports success because the last command was fine. None of that is a bug — it's bash being relaxed about a world where errors are common and the show must go on. But "the show must go on" is a terrible philosophy when the show is rm -rf "$DIR/" and $DIR was never set. set is how you tell bash to stop being so casual. We'll explain every flag it has — and then teach you the one line every serious script starts with, and exactly why each character of it earns its place.
Your First Look
The pure-magic version is the one you'll actually paste at the top of scripts. Just three flags and a fourth option:
set -euo pipefail
That single line is the difference between a script that fails loudly the instant something goes wrong and one that ploughs cheerfully through the wreckage. We'll dissect it character by character below — it's the whole reason this page exists.
But set has a second, completely different face, and it surprises everyone the first time. Run it bare, with no arguments at all:
set
BASH_VERSION='5.2.37(1)-release'
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
myvar=42
myfunc ()
{
echo hi
}
With no arguments, set dumps every variable and every function the shell currently knows about — your environment, your locals, even the full source of every function you've defined. It's the shell turning its pockets inside out. (That's the accident of history part: the same word both flips options and, given nothing to do, prints the entire state of your shell. One command, two unrelated jobs, because in 1979 nobody was precious about it.)
So set wears two hats: set -<flags> changes behavior, and bare set introspects. The rest of this page is mostly about the first hat, because that's the one that saves your weekend.
How I Use It
I almost never type set interactively. Its home is the top of a script, line one after the shebang, and the move is muscle memory: shebang, then set -euo pipefail, then start writing logic I can now trust to stop when it should.
The mental shift took me embarrassingly long to internalize, so let me hand it over directly. By default I had to remember to check $? after every single command — did that cd actually work before I rm'd? did that download actually succeed before I unpacked it? — and I'd forget, and a script would sail past a failed cd and delete files in entirely the wrong directory. set -e deletes that whole class of bug: I stop checking exit codes by hand, because bash now checks every one for me and bails the instant any of them is non-zero. set -u does the same for typos — fat-finger a variable name and the script halts at the typo instead of silently expanding it to nothing. pipefail plugs the last leak: a failure anywhere in a pipe now actually counts as a failure.
Together they convert bash from "best-effort, keep going no matter what" into "fail fast, fail loud, fail at the exact line that broke." That's the posture you want for anything that runs unattended — a cron job, a deploy script, a backup. The other half of my usage is debugging: when a script misbehaves I drop in set -x to watch every line execute with its variables expanded, find the bad line, and pull it back out. Those two — set -euo pipefail at the top, set -x when stumped — are 95% of how anyone actually uses this command.
The Famous Line, Explained
set -euo pipefail is so common it's practically a shell ritual, and it deserves to be understood rather than copied on faith. Four independent safety switches, each fixing one way bash will otherwise let a script lie to you. Let's earn each one with real output.
-e (errexit): stop at the first failure
-e means exit immediately if any command fails (returns a non-zero exit status). Without it, bash runs the next line regardless:
set -e
echo "before"
false # this command "fails" — exit status 1
echo "after" # with -e, this NEVER runs
before
The script stopped dead at false, never printed after, and exited with status 1. Without -e it would have printed both lines and exited 0, blissfully unaware anything went wrong. This is the single most valuable flag in scripting: it means you no longer have to write command || exit 1 after every risky line — the shell does it for all of them at once.
-u (nounset): unset variables are errors, not blanks
-u (also nounset) makes referencing an undefined variable a hard error instead of a silent empty string:
set -u
echo "user is: $USERNAME_TYPO"
bash: USERNAME_TYPO: unbound variable
Without -u, $USERNAME_TYPO would expand to nothing and you'd print user is: — and somewhere downstream rm -rf "$BASEDIR/cache" would quietly become rm -rf "/cache". The horror-story version of this bug has wiped real machines. -u catches the typo at the source.
Pro Tip
When a variable is allowed to be unset, tell bash so explicitly with the default-value expansion
${VAR:-fallback}— it's immune to-uand reads as documentation. And to demand a variable with a custom message, use${VAR:?must be set}, which aborts with exactly the text you wrote. Both work hand-in-glove withset -u.
-o pipefail: a pipe is only as healthy as its sickest command
This is the subtle one, and the one that catches even experienced people. A pipeline like a | b | c normally reports the exit status of the last command only. So if a dies but c is fine, bash calls the whole pipe a success:
false | true
echo "without pipefail: $?" # prints 0 — the failure vanished!
set -o pipefail
false | true
echo "with pipefail: $?" # prints 1 — caught
without pipefail: 0
with pipefail: 1
Read that twice: a command failed, and by default the pipe shrugged it off because the last stage succeeded. curl … | tar x will happily "succeed" even when the download bombed and tar just unpacked nothing. pipefail makes the pipe inherit the failure of any stage, so set -e can actually catch it. The three (-e, -u, pipefail) only reach their full power together — which is precisely why they travel as a set.
And why is pipefail spelled out as -o pipefail while the others are single letters? Because -e, -u, -x and friends predate it by decades — they're the original short flags. pipefail arrived later (it's a bash extension, not in the POSIX standard, and notably absent from dash, the /bin/sh on Debian and Ubuntu), and by then the single-letter alphabet was getting crowded, so it got a long name via -o. That's also the trap that bites people who write #!/bin/sh — pipefail simply isn't there.
The Flags, Explained
The complete menu, so you never have to wonder what a stray letter in someone's script does. Bash sums these up in one cryptic line you'll see in the set synopsis — set [-abefhkmnptuvxBCEHPT] — and here's what each actually does. Every short flag also has a readable long name you can set with -o, shown in parentheses.
The everyday ones:
-e(errexit) — exit on the first command that fails. The workhorse.-u(nounset) — referencing an unset variable is an error.-x(xtrace) — print each command, with variables expanded, just before running it. The debugging flag.-o pipefail— a pipeline fails if any stage fails (no short form).-v(verbose) — print each input line as read, before expansion.-x's less-useful cousin (it shows the line you wrote;-xshows the line as actually run — usually you want-x).
The export / assignment ones:
-a(allexport) — every variable you create or modify is automatically exported to child processes. Handy for sourcing a file ofKEY=valuelines straight into the environment.-k(keyword) — allowsVAR=valassignments to appear after the command name and still be placed in its environment. Obscure; you'll rarely touch it.
The interactive / job-control ones (mostly on by default in your terminal, off in scripts):
-m(monitor) — job control: background jobs run in their own process group and you get those[1] Donenotifications.-b(notify) — report a background job's completion immediately, not just before the next prompt.-H(histexpand) — enable!-style history expansion (!!,!$). On in interactive shells.-h(hashall) — remember (hash) the full path of commands once looked up, so bash doesn't re-search$PATHevery time. On by default; you'd only turn it off while developing a script and swapping binaries underfoot.
The "I know what I'm doing" ones:
-f(noglob) — disable filename globbing, so*and?stay literal. Useful when passing patterns to a command that wants to expand them itself.-C(noclobber) — refuse to overwrite an existing file with>redirection (use>|to force it). A gentle guard against clobbering a file you meant to append to.-n(noexec) — read and parse the script but don't execute it. A free syntax check:bash -n script.shtells you if the file parses without running a single line. Lovely for catching an unclosedifbefore it does damage.-t(onecmd) — exit after reading and running one command. Rare.-p(privileged) — turns on automatically when the real and effective user IDs differ (a setuid context); it stops bash from importing functions and the$ENVfile. You don't set this; you notice it.-P(physical) — makecdand friends resolve symlinks to their real physical path instead of the symlinked one.-B(braceexpand) — brace expansion ({a,b,c}→a b c). On by default; listed for completeness.-E(errtrace) and-T(functrace) — makeERRandDEBUG/RETURNtraps get inherited by shell functions. The companions you reach for when you want a singletrap … ERRhandler to fire no matter how deep the failure happened.
And the structural ones that aren't really "options" at all:
--— end of options. Everything after becomes a positional parameter.set -- "$@"is the idiom for safely resetting the argument list.+instead of-— turns a flag off.set -xon,set +xoff. This is the trick for tracing just one suspicious section of a script (see Advanced Usage).
Note
Every flag here has a matching long name you switch with
-o name/+o name—set -o errexitis identical toset -e, just self-documenting. In a shared script,set -o errexit -o nounset -o pipefailis arguably kinder to the next reader than the terse-euo pipefail. Same machinery; pick the spelling your future self will thank you for.
Reading set -x Output
set -x is the closest thing bash has to a debugger, and once you can read its output you'll never echo "got here" your way through a script again. It prints every command after expansion — so you see the real values, not the variables. Watch:
set -x
x=hello
echo "$x" | tr a-z A-Z
+ x=hello
+ echo hello
+ tr a-z A-Z
HELLO
Each traced line is prefixed with + and shows the command as bash actually ran it — note echo hello, not echo "$x". That expansion is the whole point: the bug is almost always a variable that held something other than what you assumed, and -x shows you the truth. The lines without + (here, HELLO) are the program's real output, so you can see cause and effect interleaved.
That leading + isn't fixed — it's the first character of the PS4 variable (the "prompt" for trace output, sibling to the PS1 prompt you see every day). Redefine PS4 and you can make traces tell you where in the file each line lives:
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
x=1
y=2
+ :1: x=1
+ :1: y=2
Now every traced line carries its source file and line number — turning a wall of trace into something you can actually navigate. A common production trick is to also stamp the time, so a slow script reveals which line is dragging. That's the kind of thing that feels like a secret the first time someone shows you.
Pro Tip
To trace a script without editing it, run
bash -x script.sh— same effect as puttingset -xat the top, but temporary and non-invasive. Andbash -n script.shparses it without running anything (that'sset -n), a zero-risk syntax check before you let a dangerous script loose.
Gotchas
set -e is the most useful flag and also the most surprising — it has corners that have launched a thousand mailing-list arguments. The rule is simpler than the folklore: errexit is suppressed wherever bash is already inspecting the exit status itself. Concretely:
- A failing command in an
if,while,&&,||, or!does NOT trigger exit. That's by design — you're explicitly testing the result, so a "failure" is just afalseanswer. This bites hard with functions:
set -e
foo() { false; echo "still here"; }
if foo; then echo "ok"; else echo "handled"; fi
still here
ok
The false inside foo didn't stop the function, because foo was called as an if condition — so -e is disabled for its entire body. People expect the false to abort and are baffled when echo "still here" runs anyway. Knowing this one rule explains nearly every "but I set -e!" confusion you'll ever hit.
-
set -eis the king of leaky abstractions. The exact rules differ between bash versions and between bash and POSIXsh. Greg's Wiki (the famous "BashFAQ 105") argues you should check exit codes explicitly rather than lean on-efor anything subtle. Most of us use-eanyway — it catches the common careless failures beautifully — while knowing its blind spots. -
a=$(somecmd)masks the failure ofsomecmdunder-ein older bashes, because the assignment "succeeds" even if the command substitution failed. Modern bash improved this, but the safe habit is to split risky command substitutions onto their own line. -
-utrips on$@/$*when there are no arguments in some shells, and on associative-array lookups of missing keys. Use${1:-}and friends to be explicit. -
pipefailreports the last failing stage, not the first. Usually what you want — but if bothaandcina|b|cfail, the status you get isc's. Rarely matters; worth knowing when it does.
Advanced Usage
A few moves that separate someone who pastes set -euo pipefail from someone who wields it.
Trace only the suspect section. set -x on at the start of the dodgy block, set +x off at the end. You get a focused trace of exactly the part you care about, not three hundred lines of noise:
set -x # tracing ON
build_the_thing
deploy_it
set +x # tracing OFF — back to quiet
Source a config file straight into the environment with allexport:
set -a # everything I define from here is exported
source ./app.env # a file of KEY=value lines
set +a # stop auto-exporting
Every KEY=value in app.env is now a real environment variable for child processes — no manual export per line.
Reset the positional parameters with set --. This is how you safely build or replace $1, $2, …:
set -- alice bob carol
echo "$# args, first is $1" # 3 args, first is alice
Find your shell's current flags in the special variable $-. It's a terse string of the active single-letter options:
echo "$-"
hBc
That h, B, c means hashall, braceexpand, and "command from -c" are on. A script can test [[ $- == *e* ]] to ask "am I running under errexit?" — handy for libraries that must behave correctly whether or not the caller turned -e on. It's the shell quietly telling you about itself, the same introspective spirit as bare set.
History & Philosophy
set is old — it predates Linux by a decade, going back to the Bourne shell that Stephen Bourne wrote at Bell Labs in the late 1970s. Its dual personality (flip options or, given nothing, print everything) is a fossil from an era when shell builtins were added pragmatically, one need at a time, with little worry about conceptual tidiness. It is, like much of the early Unix toolset, less designed than grown — and it works, so it stayed.
The deeper lesson hiding in set -euo pipefail is a quiet philosophical statement about failure. Bash's default — keep going, treat the missing as empty, trust the last command in the pipe — comes from the interactive world, where a human is watching, a mistake just means retyping, and stopping a whole session over one fumbled command would be infuriating. But a script has no human watching. It runs at 3am from cron, and "keep going no matter what" stops being convenience and becomes a way to compound one small error into a wrecked system. set is the seam between those two worlds: it lets the same shell be forgiving when you're typing and ruthless when you're automating. Learning to flip that switch — to make your scripts fail fast and fail loud — is one of the genuine rites of passage from "writes shell that mostly works" to "writes shell you'd trust to run unwatched." Pull the thread and you find the whole discipline of defensive programming on the other end.
Cheat Sheet
The line that matters:
set -euo pipefail— the safety quartet at the top of every serious script. Exit on error, error on unset variable, catch failures mid-pipe.
The everyday flags (use + to turn any off):
-eexit on error ·-uunset vars are errors ·-xtrace every command ·-vprint lines as read ·-o pipefailpipe fails if any stage fails
Debugging without editing the file:
bash -x script.shtrace a run ·bash -n script.shsyntax-check without running · customize the trace prefix withPS4
The structural moves:
set(bare) dump all variables + functions ·set --reset positional parameters ·$-the currently-active flags ·set -olist every option and its on/off state
Flag Reference
The common safety preamble is set -euo pipefail.
| Option | Meaning |
|---|---|
-e / -o errexit |
Exit on any command failure |
-u / -o nounset |
Error on referencing an unset variable |
-o pipefail |
A pipeline fails if any stage fails |
-x / -o xtrace |
Print each command before running it (debugging) |
-C / -o noclobber |
Don't overwrite existing files with > |
-f / -o noglob |
Disable filename globbing |
-- |
End of options |
-o |
Show all options and their on/off state |
See Also
bash— the shell this all configurestrap— run cleanup code on errors and signals, the natural partner toset -eexport— whatset -aautomates- shell — the program these options tune
- exit status — the non-zero return codes
set -ereacts to - pipe — what
pipefailguards - cron — where unattended scripts run, and where
set -euo pipefailearns its keep
Wrote a careful script that still wrecked something at 3am because one line failed and bash kept going?
CleverUptime watches the aftermath — the disk that filled, the service that died, the backup that silently stopped running — and tells you in plain language what broke and why, so a script that failed quietly doesn't stay quiet until it's a crisis.
Want to see your own server's health right now? One command, no signup, no install.