Looping over files is a find problem
The text-processing guide covers the case this snippet warns about: when a glob is not enough, find -print0 | while read -d "" and find -exec are the two loops that survive spaces, newlines and 40,000 files. Full guide: Bash Text Processing: find, grep, sed, and awk.
The loop that loses files with spaces
for f in $(ls /data/exports) has a built-in data-loss mode. Bash word-splits the output of $(ls ...), so a filename like Q3 final.xlsx arrives in the loop as two items, Q3 and final.xlsx. A backup job built on that loop tries to copy two paths that do not exist, logs two harmless "no such file" lines, and moves on — every night, run after run, while every file without a space in its name backs up fine. That is exactly why nobody catches it: the job runs green until the day someone needs the one file that never made it into any backup.
The bug is not cp. It is looping over parsed command output instead of a glob.
Loop a glob — the form for files
for f in "$SRC_DIR"/*.xlsx lets bash expand the glob itself, which produces a correctly-separated list no matter what is in the names. Quoting "$f" on use is the other half — the split that bit us happens when the variable is used, not only in the loop header. The -- tells cp to stop reading options, so a file named -rf cannot turn into a flag.
Loop a range or a counter — the form for numbers
{1..5} is fixed before variables expand, so {1..$attempts} produces the literal string {1..5}-style garbage, not a range. The moment your bound is a variable, the C-style for (( ... )) is the correct tool.
Loop an array — quote the expansion
"${servers[@]}" with quotes and [@] keeps db primary as one element. Drop the quotes and you are back to word-splitting — the same bug, new disguise.
The rule across all of these: glob over parse, quote on use. For reading a file's lines one at a time, a for loop is the wrong tool — see read a file line by line. And wrap any loop that touches real files in set -euo pipefail so a failure stops the script instead of looping past it.
Quoting correctly stops the loop doing the wrong thing quietly. Making the whole script survive unattended is a separate layer — strict mode, an ERR trap that names the failing line, cleanup on every exit, and a log worth reading afterwards. That layer is collected as bashlib.sh and a ready-made template.sh in The Production Bash Toolkit.