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 = ada makes the shell look for a command called name.
  • Use braces when the name runs into other characters: ${name}_backup rather than $name_backup.
  • A variable set in a script disappears with the script unless it is exported or the file is sourced.
  • readonly and declare -r stop 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
FormBehaviour
$varSplit 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.

Conditionals and loops Writing a robust script

Last refreshed 2026-09-18.