Variables, quoting and exit codes
Assign and expand values safely, understand word splitting, and read the status a command returns.
Variables
name=ada # no spaces around =
count=3
files=$(ls -1 | wc -l) # command substitution
today=$(date +%F)
echo "$name has $count items" # double quotes expand
echo '$name is literal' # single quotes do not
readonly MAX_RETRIES=5
export SERVICE_URL="https://api.example.com" # visible to child processes- Assignment takes no spaces.
name = adamakes the shell look for a command calledname. - Use braces when the name runs into other characters:
${name}_backuprather than$name_backup. - A variable set in a script disappears with the script unless it is exported or the file is sourced.
readonlyanddeclare -rstop accidental reassignment further down a long script.
Quoting and word splitting
An unquoted expansion is split on whitespace and then treated as a glob pattern. That is the source of most problems that appear only when a filename contains a space.
file="my report.txt"
cat $file # two arguments: "my" and "report.txt"
cat "$file" # one argument: the whole name
rm *.txt # the glob expands in the current directory
rm "*.txt" # tries to delete a file literally named *.txt
set -- one "two three"
for arg in "$@"; do echo "[$arg]"; done # keeps each argument intact| Form | Behaviour |
|---|---|
$var | Split on whitespace and glob-expanded |
"$var" | One word, exactly the value |
'text' | No expansion at all; variable names stay as written |
"$@" | Each positional parameter as its own argument |
$((1 + 2)) | Arithmetic expansion, not command substitution |
Quote every expansion unless you have a specific reason not to. "$var" and "$@" are correct far more often than their unquoted forms.
Exit codes
grep -q ERROR app.log
echo $? # 0 = success, anything else = failure
if command_that_might_fail; then
echo "ok"
else
echo "failed"
fi
grep ERROR app.log && echo "found" || echo "not found"
exit 2 # a script otherwise returns the last command's status
# a pipeline reports only the last command unless pipefail is set
false | true; echo $?
set -o pipefail
false | true; echo $? # now 1: the failure propagates- 0 is success and everything else is failure. 1 is generic, 2 is often a usage error, 126 means not executable and 127 means not found.
&&runs the next command only on success;||only on failure.- In a pipeline the status is that of the last command by default, so a failing producer can be completely invisible without
pipefail. - Return a meaningful status from your own scripts so the automation that calls them can react.
⚠️
Ignore an exit code and the failure resurfaces much later as corrupted output. Check
$? after any command whose failure would surprise you, and read ${PIPESTATUS[@]} when you need the status of every stage in a pipeline.FAQ
Why did my script keep going after an error?
Bash does not stop on failure by default. Either check each command explicitly or start the script with
set -euo pipefail and handle the exceptions deliberately.Single quotes or double quotes?
Single quotes for literal text, as in
grep '$5' file; double quotes when you need expansion, as in echo "$name". Never leave an expansion unquoted just because it looks simple.Related
Conditionals and loops Writing a robust script
Last refreshed 2026-09-18.