Why Bash Scripts Break on Filenames with Spaces
Inspect argument boundaries to fix Bash filename bugs, then test spaces, wildcard characters, leading dashes, arrays, and empty matches.

A file named release notes.txt exists, but your script complains that release and notes.txt do not exist. Before changing the filename, inspect the arguments your command receives. An unquoted variable can turn one stored path into several arguments.
The repair is usually to preserve the path with "$file". But quoting does not solve every filename problem: a leading dash can still be interpreted as an option, and a loop over an unmatched pattern needs an explicit policy.
Show the arguments rather than the displayed command
Run the following in Bash, not an unspecified shell. The argument examples were checked in Bash 3.2.57 and are applicable to these basic expansion forms in current Bash.
practice_dir=$(mktemp -d)
cd "$practice_dir"
printf 'alpha\n' > 'release notes.txt'
printf 'beta\n' > 'report*.txt'
printf 'gamma\n' > '-draft.txt'
show_args() {
printf 'argc=%s\n' "$#"
printf '<%s>\n' "$@"
}
file='release notes.txt'
show_args $file
show_args "$file"
The first call produces:
argc=2
<release>
<notes.txt>
The second produces:
argc=1
<release notes.txt>
The variable assignment preserved the text in both cases. The difference happened when Bash expanded it in the command. With its default field separators, an unquoted expansion undergoes word splitting. Double quotes let the variable expand while preserving it as one argument.
A printed line such as echo "$file" cannot prove that an earlier command received one argument. The diagnostic function shows the count and the boundaries directly.
A wildcard stored in a filename is another expansion risk
Add a second report and inspect a variable containing a literal asterisk:
printf 'delta\n' > report-final.txt
file='report*.txt'
show_args $file
show_args "$file"
The unquoted expansion can match both report*.txt and report-final.txt. The quoted expansion passes exactly the literal name report*.txt. Bash performs filename expansion after word splitting; a variable that survived splitting can still turn into multiple paths.
Quoting a wildcard pattern has a different effect from quoting a path:
show_args ./*.txt
show_args './*.txt'
The first intentionally asks Bash to find matching paths. The second passes the literal pattern. Use expansion where you want a set of files; preserve each resulting path where you want one command argument.
Fix the loop at its consumer
This loop asks Bash to expand the pattern, then passes each matched filename intact to an input redirection:
for file in ./*.txt; do
[ -f "$file" ] || continue
printf 'Bytes in <%s>: ' "$file"
wc -c < "$file"
done
The glob creates separate path values even when a name contains spaces. Quoting at wc preserves each value. The ./ prefix also keeps a name such as -draft.txt from beginning with a dash. Input redirection means wc receives no filename option to interpret.
For commands that accept a -- option terminator, cp -- "$file" "$destination" separates options from operands. Check the command’s documentation; the shell does not make every tool recognize --.
By default, Bash leaves an unmatched glob literal. The file test above skips that literal and also skips directories. If your program must report “no inputs” as an error, count the files and check the count after the loop. Quietly doing nothing is a policy choice, not a quoting rule.
Keep multiple paths as multiple values
Do not store a list as one space-separated string and expect quotes to reconstruct it. Use a Bash array:
files=('release notes.txt' 'report*.txt' '-draft.txt')
show_args "${files[@]}"
The result is three arguments. "${files[*]}" joins the array into one argument instead. Similarly, "$@" preserves separate positional arguments when forwarding them to another command.
Avoid for file in $(ls): it turns formatted command output back into shell words, losing the boundaries you need. Line-by-line parsing is also insufficient for arbitrary filenames because a filename can contain a newline. A direct glob is enough for this single-directory exercise; recursive workflows need a delimiter-aware approach, such as find -print0 with a NUL-aware consumer.
LabEx’s Passing Arguments to the Script and Shell Arrays are relevant practice entries. Their directory names establish the topics; the argument probe above is an independent example.
Before accepting a repair, test an ordinary name, a space, a literal wildcard, a leading dash, and no matches. For a script that must handle arbitrary names, also test a newline. Record the argument count and the file actually read; a command that merely exits successfully can still have processed the wrong path.
References
- GNU Bash: word splitting — when expanded text becomes separate fields.
- GNU Bash: double quotes — which expansions remain active inside quotes.
- GNU Bash: filename expansion — wildcard matching and unmatched patterns.
- GNU Bash: arrays — preserving separate values with array expansion.