Linux / shell
Unquoted variables — when a script silently changes the arguments
Written and reviewed by Sahil Srivastav
argc=2
<quarterly>
<report.csv>What this error actually means
The header is the output of the small argument-inspection example below. A value containing quarterly report.csv is intended to be one filename. When expanded unquoted as $file, Bash can split it at IFS separators into two words. Those words can then undergo pathname expansion, so wildcard characters stored in data may match files in the current directory.
Double quotes preserve the result of a parameter expansion as one argument, including spaces and wildcard characters. They also preserve an empty string as an empty argument. An unquoted empty expansion can disappear completely, changing the number and positions of arguments received by the program.
Shell quoting is syntax applied while parsing the command. Quote characters stored inside a variable do not become shell syntax during ordinary expansion. A string such as options='--label "nightly run"' is therefore not a reliable argument list. Using eval to reinterpret it introduces a second parse and can turn data into executable shell code.
Causes, most common first
- 1A path or user-provided value is expanded without quotes. Commands such as cp $source $destination expose data to splitting and globbing before cp starts. The receiving program cannot reconstruct the original boundaries because the shell has already changed them.
- 2An argument list is stored as one string. Several options plus their values are not a single scalar. Expanding the string unquoted breaks values containing spaces; expanding it quoted passes the entire list as one argument. A Bash array preserves the intended list structure.
- 3A loop parses line-oriented command output as words. for file in $(find ...) applies splitting and expansion to a whole output string. Filenames can contain spaces and newlines, so textual reconstruction loses information. Use find -exec or a NUL-delimited interface for filesystem names.
- 4Quoting is mistaken for option validation. Even a correctly quoted value beginning with a dash can be interpreted as an option by the receiving utility. Where supported, use -- to end option parsing. Quoting preserves boundaries; it does not define the utility’s argument semantics.
When you see it
- A job works with simple names but fails for a customer file containing spaces
- An asterisk in input selects unrelated files from the current directory
- Empty optional input shifts positional arguments or changes command behaviour
- Adding quote characters inside a variable does not preserve the intended boundaries
How to diagnose it
Step 1
Inspect the argument vector directly
Run this harmless example in Bash. It prints the count and each argument separately, making the missing boundary visible. Repeat with "$file" to see one argument containing the original space.
show_args() { printf 'argc=%s\n' "$#"; printf '<%s>\n' "$@"; }
file='quarterly report.csv'
show_args $fileStep 2
Use a shell-aware static check
If ShellCheck is installed, it flags many unquoted expansions and array mistakes. Review the warning in context rather than suppressing it because current fixtures contain only simple names. The tool does not know your business contract for empty or missing inputs.
shellcheck ./process-files.shStep 3
Test wildcard and empty values in isolation
Use the same show_args helper and a disposable directory. Observe how the current directory affects an unquoted wildcard and how an empty value changes the argument count. No files need to be modified to reproduce the parsing problem.
file='*.csv'
show_args "$file"
file=''
show_args "$file"Step 4
Inspect how the downstream tool parses arguments
After boundaries are correct, read the utility’s option rules and confirm whether it supports --. A leading-dash filename and an absent required operand are semantic cases the script must handle separately from whitespace.
The fix
Quote scalar expansions used as arguments: "$source", "$destination" and "$value". Forward a caller’s argument list using "$@", which preserves each argument separately. Do not replace it with "$*" when the receiving command needs the original boundaries.
Use a Bash array for a command assembled from multiple arguments. Append each option and value as separate array elements, then expand with "${args[@]}". If the script must be portable POSIX sh, use positional parameters with set -- rather than assuming Bash arrays are available.
Validate required values before invoking the utility. The example requires two arguments, validates the source as a regular file, and uses -- for GNU cp. It still leaves normal overwrite policy to the caller; quoting is not a complete file-transfer workflow. Choose destination and overwrite rules according to the application.
For filesystem iteration, avoid command substitution over names. find -exec passes paths directly, and NUL-delimited streams preserve names that include newlines. Do not attempt to escape every unusual character manually or use eval to resurrect quotes embedded in data.
#!/bin/bash
set -euo pipefail
source_file=${1:?usage: copy-report SOURCE DESTINATION}
destination_file=${2:?usage: copy-report SOURCE DESTINATION}
[[ -f "$source_file" ]] || {
printf 'Source is not a regular file: %s\n' "$source_file" >&2
exit 1
}
copy_args=(-- "$source_file" "$destination_file")
cp "${copy_args[@]}"How to stop it coming back
- Include spaces, empty strings, literal wildcard characters, leading dashes and newlines in argument-handling fixtures.
- Keep command names and argument lists separate from data; avoid building executable shell text from user input.
- Run ShellCheck in script review and explain the few intentional unquoted expansions locally.
FAQ
Does set -f solve this?
It disables pathname expansion but does not prevent word splitting or preserve an unquoted empty value. Quoting each scalar argument handles the intended boundary directly without changing glob behaviour for the entire script.
Are variables always split unless quoted?
No. Bash has contexts with different expansion rules, including assignment words and [[ ... ]]. The dangerous case discussed here is ordinary unquoted expansion into command arguments. Follow the rules of the actual context rather than mechanically adding escapes.
Why does a quoted wildcard stop matching files?
Because the wildcard is now literal data. If the script intentionally selects files by a pattern, write that selection explicitly using a glob or find predicate. A value representing one pathname should not accidentally become a pattern.
Related
Other errors engineers hit next to this one
- The same message processed twice (at-least-once delivery)
- Messages processed out of order across partitions
- Webhook delivered twice — customer charged twice
- Database and broker diverge after a dual write
- Retry storm: thundering herd after a dependency failure
- Outbound call has no timeout and exhausts workers
- Distributed lock lease expired while the holder was still working
- Clock skew: timestamp ordering or token expiry is inconsistent