ThisCalculator

Everyday

Bash Parameter Expansion: Two Subtle Script Traps

Updated · Standard formula, shown below

Bash Parameter Expansion: Two Subtle Script Traps

Two subtle Bash parameter-expansion traps cause everyday scripts to behave differently than their authors expect: unquoted expansions can undergo word splitting and pathname expansion, and a familiar-looking default-value operator can treat an empty string differently from an unset variable. Understanding both helps prevent broken arguments, surprising filenames, and incorrect defaults.

Trap 1: An expansion is not automatically one argument

When Bash expands a parameter outside quotes, it does more than substitute its value. For most ordinary parameter expansions, Bash then performs word splitting and pathname expansion. That means a variable intended to represent one argument can become several arguments, or a value containing wildcard characters can match files in the current directory.

For example:

file="quarterly report.txt"
cat $file

The unquoted $file can split at the space. Bash then tries to run cat with two arguments, quarterly and report.txt, rather than the one filename intended. The usual fix is:

cat "$file"

Quotes preserve the expanded value as a single argument. They do not become part of the value or filename; they tell the shell how to treat the expansion while parsing the command.

Wildcard characters make the same trap less obvious:

pattern="*.log"
printf '%s\n' $pattern

If matching log files exist, the unquoted expansion can turn into their filenames. If none match, the pattern is generally left as the literal text *.log unless options such as nullglob or failglob change that behavior. Quoting it prints the literal pattern instead:

printf '%s\n' "$pattern"

Quoting rules for values and lists

A reliable everyday rule is to quote parameter expansions unless you deliberately need splitting or glob matching. This applies to command arguments, tests, and assignments, though the exact context matters. For example, assignments do not split or perform pathname expansion on the right-hand side:

copy=$source

Even so, copy="$source" is clearer and remains safe if the code is moved into a different context. Quoting also helps prevent unexpected behavior when values contain spaces, tabs, newlines, or wildcard characters.

Arrays are a better fit than a single string when you need to store multiple arguments. Store each item as an array element, then expand the array with quotes:

args=(--label "quarterly report" "*.log")
command "${args[@]}"

"${args[@]}" expands each element as a separate argument, retaining spaces and literal wildcard characters inside each element. By contrast, "${args[*]}" expands the array as one joined word. An unquoted ${args[@]} can again expose elements to splitting and pathname expansion.

Trap 2: Unset and empty are different states

Bash provides several parameter-expansion operators that look similar but distinguish between a parameter that is unset and one that is set to an empty string. This difference often surfaces when scripts choose fallback values.

name=""
printf '%s\n' "${name-default}"
printf '%s\n' "${name:-default}"

The first expansion uses default only if name is unset. Since name is set, even though it is empty, the result is empty. The second uses default if name is either unset or null (empty). The colon is the crucial distinction.

The same distinction applies to assignment and error forms:

  • ${name=default} assigns the default only when name is unset.
  • ${name:=default} assigns it when name is unset or empty.
  • ${name?message} reports an error when name is unset.
  • ${name:?message} reports an error when it is unset or empty.

These assignment forms change the parameter as part of expansion. The error forms are useful for required settings, such as a file path a script cannot proceed without. In a non-interactive Bash script, an expansion using ? can cause the shell to exit; it is not merely a message-producing substitute for an ordinary conditional.

Choosing the right default operator

Choose based on what “missing” means for the variable. If an empty string is a meaningful, intentional value, use the no-colon form when you want to fall back only for an unset variable. If empty means “not provided,” use the colon form.

output=${OUTPUT:-result.txt}

This common pattern selects result.txt when OUTPUT is unset or empty. If the script should accept an explicitly empty value as meaningful, use:

output=${OUTPUT-result.txt}

Be precise about the intended policy. For example, an empty setting might be valid for an optional prefix, but not for a required directory. The operator cannot decide that policy for you; it only implements the unset-versus-empty test.

Related expansions and common misconceptions

Not every parameter expansion behaves exactly like a simple $variable. Bash’s length operator, substring operations, and pattern-removal forms have their own syntax and rules. Still, quoting the complete expansion is usually the right starting point when its result is meant to be one argument:

printf '%s\n' "${#name}"
printf '%s\n' "${path##*/}"

A common misconception is that double quotes disable all expansion. They do not: parameter expansion, command substitution, and arithmetic expansion still occur inside double quotes. The quotes prevent the resulting text from being split into multiple words or interpreted as filename patterns. Single quotes, in contrast, prevent those expansions and preserve their contents literally.

Another misconception is that Bash always splits on spaces in a variable. Word splitting uses the characters in IFS, which is normally space, tab, and newline, and it has detailed rules for leading, trailing, and repeated delimiters. Relying on those rules to parse structured data is fragile. Use arrays for argument lists and a purpose-built reader for structured input instead.

How to find and prevent these bugs

When a command receives the wrong number of arguments, inspect the expansion that supplies them. A quick diagnostic can show argument boundaries:

printf 'argument: <%s>\n' "$value"

For a command assembled from several variables, print or inspect each argument separately, or use Bash’s execution tracing with set -x while debugging. Tracing is useful, but it can expose values in logs, so avoid enabling it where secrets may be printed.

  • Quote scalar expansions used as command arguments: "$value".
  • Use "${array[@]}" to pass an array as separate arguments.
  • Choose :- or - according to whether an empty value counts as missing.
  • Test variables in both states: unset and set to an empty string.
  • Use shellcheck as a static checker for common quoting mistakes, while still reviewing the intended behavior.

Frequently Asked Questions

Why should I quote variables in Bash?
Quoting prevents most parameter expansions from being split into multiple words or treated as filename patterns. It preserves a scalar value as one argument.

What is the difference between ${x-default} and ${x:-default}?
The first uses the default only when x is unset. The second uses it when x is unset or empty.

Does double-quoting a variable stop its value from expanding?
No. Double quotes still allow parameter expansion. They protect the expansion result from word splitting and pathname expansion.

How do I pass a Bash array safely to a command?
Use command "${array[@]}". Each array element becomes one argument, even if it contains spaces.

Can an empty environment variable trigger a default?
Yes, if the script uses the colon form, such as ${SETTING:-default}. With ${SETTING-default}, a set-but-empty variable does not trigger the fallback.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.