Shell builtins · bash builtin
set
Turn shell options such as -e, -u, -x and pipefail on or off, and replace the positional parameters.
set [-abefhkmnptuvxBCEHPT] [-o OPTION] [--] [ARG...]
set does two unrelated jobs. With option letters it switches shell behavior on (-x) or
off (+x, a plus sign): exit on error, treat unset variables as errors, trace every command,
and so on. Long names go through -o, as in set -o pipefail. With other arguments it
replaces the positional parameters $1, $2, ... of the current shell or function, which is
handy for splitting a string or resetting $@.
The options you will meet in scripts are -e (errexit), -u (nounset), -o pipefail, -x
(xtrace), -f (no globbing) and -C (noclobber). set -euo pipefail at the top of a script
catches many mistakes early, but -e in particular has a long list of situations where it
does nothing; read Strict mode and its caveats before relying on it.
Run with no arguments, set prints every shell variable and function, which is long. set
-o lists the options with their state, and $- holds the active single-letter flags. bash
has a second, separate set of options, changed with shopt -s and shopt -u, for
things like nullglob and globstar; those are not changed with set.
Examples
Stop a script at the first failing command with -e
set -e
echo "start"
ls missing-file
echo "never printed"
start ls: cannot access 'missing-file': No such file or directory
ls fails with status 2, and with -e the shell exits right there with the same
status, so the last echo never runs. Without -e the script would carry on.
Where -e does not trigger
set -e
if grep -q CRITICAL logs/app.log; then echo found; fi
grep -q CRITICAL logs/app.log || echo "not found, still running"
false | true
echo "reached the end"
not found, still running reached the end
A failing command used as an if condition, on the left of || or &&, or anywhere
but last in a pipeline does not stop the shell. That is by design, and it is why -e is
a safety net, not error handling.
Catch typos in variable names with -u
set -u
backup_dir=/tmp/backups
echo "cleaning ${backup_dri}"
echo "not reached"
/bin/bash: line 3: backup_dri: unbound variable
With -u, expanding a variable that was never set is an error that aborts a
non-interactive shell; bash 5.2 exits with status 127 here.
Without it, the typo would quietly expand to an empty string, a
dangerous thing in front of rm -rf.
Allow an optional variable under -u
set -u
echo "level: ${LOG_LEVEL:-info}"
echo "args: $#"
level: info args: 0
${var:-default} is allowed under -u because it says what to do when the variable is
unset. $#, $@ and $* never trigger it either (since bash 4.4, an empty "$@" is
fine).
Make a pipeline fail when any part fails
grep ERROR missing.log | wc -l; echo "status without pipefail: $?"
set -o pipefail
grep ERROR missing.log | wc -l; echo "status with pipefail: $?"
grep: missing.log: No such file or directory 0 status without pipefail: 0 grep: missing.log: No such file or directory 0 status with pipefail: 2
A pipeline's status is normally that of its last command, and wc succeeded. With
pipefail it is the status of the rightmost command that failed, here grep's 2, so
set -e and if can see the error.
Trace each command before it runs with -x
set -x
name="file with spaces.txt"
wc -l "$name"
set +x
echo "tracing is off"
+ name='file with spaces.txt' + wc -l 'file with spaces.txt' 1 file with spaces.txt + set +x tracing is off
Each command is printed to stderr with a + prefix after expansion, with quotes added
where an argument contains spaces. set +x turns tracing off again (and is itself
traced). This is the first tool to reach for when a script misbehaves.
Show line numbers in the trace with PS4
PS4='+ line $LINENO: '
set -x
n=$(wc -l < todo.txt)
echo "$n tasks"
++ line 3: wc -l + line 3: n=5 + line 4: echo '5 tasks' 5 tasks
PS4 is the prefix of trace lines and is expanded each time, so $LINENO shows where
each command is. Nested command substitutions get an extra +.
Replace the positional parameters
set -- alpha "beta gamma" delta
echo "count: $#"
printf '[%s]\n' "$@"
count: 3 [alpha] [beta gamma] [delta]
set -- throws away the current $1, $2, ... and sets new ones. The quoted argument
stays one parameter. In a function this changes only the function's own arguments.
Split a string into fields with set
line="2026-03-14 08:00:01 ERROR db timeout"
set -- $line
echo "date=$1 level=$3 rest=${*:4}"
date=2026-03-14 level=ERROR rest=db timeout
The unquoted $line is word-split into five parameters. Unquoted expansion also globs,
so a * in the data would turn into file names; use set -f first or read -a when
the input is not trusted.
Why -- matters when the data starts with a dash
value="-x"
set $value
echo "\$1 is now: [${1-unset}]"
set +x
set -- $value
echo "\$1 is now: [$1]"
+ echo '$1 is now: [unset]' $1 is now: [unset] + set +x $1 is now: [-x]
Without --, set -x is taken as the option and tracing switches on (watch the +
lines) instead of setting $1. With -- everything after it is a positional parameter.
Disable filename expansion with -f
pattern='*.txt'
echo $pattern
set -f
echo $pattern
file with spaces.txt notes.txt todo.txt *.txt
Normally an unquoted *.txt expands to the matching files. After set -f (noglob) it is
left alone. Quoting the variable is still the better fix in most cases.
Refuse to overwrite files with -C
set -C
echo "new notes" > notes.txt
echo "status: $?"
echo "forced" >| notes.txt && head -n 1 notes.txt
/bin/bash: line 2: notes.txt: cannot overwrite existing file status: 1 forced
With noclobber, > will not truncate an existing file and the redirection fails. >|
overrides it for one command. >> (append) is still allowed.
See which options are on
set -euo pipefail
echo "flags: $-"
set -o | grep -E '^(errexit|nounset|pipefail|xtrace|noclobber) '
flags: ehuBc errexit on noclobber off nounset on pipefail on xtrace off
$- lists the active single-letter flags (h and B are on by default, c means the
shell was started with -c). set -o prints every long option with on or off.
Common pitfalls
set -eis ignored insideif/whileconditions,&&/||lists, and functions called from them, so a function that relies on it can keep running after an error. See Strict mode and its caveats.local x=$(cmd)andexport x=$(cmd)always succeed, hiding a failingcmdfromset -e. Declare first, then assign on its own line.With
set -u,"${arr[@]}"on an empty array was an error before bash 4.4, and$1in a script called without arguments always is. Use${1:-}for optional arguments.set -xoutput goes to stderr and includes secrets passed on command lines. Turn it off around such commands, or send it elsewhere withBASH_XTRACEFD.set $varwithout--misbehaves when the value begins with-or+: it changes shell options instead of$1.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
-e,-u,-x,-f,-C,-vandset --are POSIX and work in dash.-o pipefailis in POSIX.1-2024 and supported by bash, zsh, ksh and the dash 0.5.12 in Debian 13; older/bin/shimplementations may reject it, so test before relying on it in a#!/bin/shscript.
Options worth knowing
A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.
| Option | What it does |
|---|---|
-e | errexit: exit when a command fails, outside of conditions and lists. |
-u | nounset: treat expanding an unset variable as an error. |
-x | xtrace: print each command to stderr, after expansion, before running it. |
-o OPTION | Turn on the named option (pipefail, errexit, noclobber, vi, ...); +o turns it off. |
-f | noglob: disable filename expansion. |
-C | noclobber: make > fail on existing files; >| overrides. |
-v | verbose: print input lines as they are read, before expansion. |
-n | noexec: read commands without running them (syntax check in scripts). |
-a | allexport: export every variable that is assigned. |
-E | errtrace: let functions and subshells inherit the ERR trap. |
-T | functrace: let functions inherit DEBUG and RETURN traps. |
-b | notify: report finished background jobs immediately. |
-m | monitor: enable job control. |
-h | hashall: remember command locations (on by default). |
-B | braceexpand: perform brace expansion (on by default). |
-H | histexpand: enable ! history expansion (interactive default). |
-P | physical: do not follow symlinks when changing directory. |
-- | End of options; remaining arguments become the positional parameters, and with none, they are cleared. |
Related
trapRun a command when the shell receives a signal or exits, typically to clean up temporary files.unsetRemove shell variables, array elements or functions, so they no longer exist rather than being empty.shiftDrop the first N positional parameters ($1, $2, ...) and renumber the rest down.declareCreate variables with attributes (array, associative array, integer, read-only, exported), or show how variables are defined.exportMark shell variables (or bash functions) to be passed to every program the shell starts.- Strict mode and its caveatsWhat set -euo pipefail actually does, the cases where -e silently does nothing, the traps in -u and pipefail, and a balanced way to use it.
- Debugging bash scriptsCheck syntax with bash -n, trace with set -x, add file and line numbers through PS4, send the trace elsewhere with BASH_XTRACEFD, and report failures with trap ERR.
- Script argumentsPositional parameters $1 to ${10}, $# and $0, why "$@" is almost always right and "$*" rarely, shift, getopts, and ending options with --.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.