Never Build Shell Commands As Strings
You're one unquoted variable away from deleting the wrong directory. When your AI agent — or your own script — glues a command together as a string, the shell gets a second vote on what that string means. A stray space, an empty variable, or a filename with a glob character, and it runs something you never wrote.
Why strings split apart
The shell does not run the string you typed. It runs the string after word splitting and pathname expansion. Both happen on every unquoted expansion.
DIR="my project"
rm -rf $DIR # runs: rm -rf my project → two arguments
rm -rf "$DIR" # runs: rm -rf 'my project' → one argument
Worse, if $DIR is empty or unset, rm -rf $DIR/ becomes rm -rf /. The command looks correct in your editor and is catastrophic at runtime. This is Bash Pitfall #1 and #2: the results of an expansion are still subject to word splitting and pathname expansion, so you always double-quote parameter expansions.
The three habits
| Habit | Instead of | Do this |
|---|---|---|
| Argument lists | cmd="rm -rf $files"; $cmd | args=(-rf "$file"); rm "${args[@]}" |
| Every expansion | grep $pat $file | grep "$pat" "$file" |
| Fail fast | (no guard) | set -euo pipefail |
Use arrays for argument lists. A Bash array keeps each element a distinct word regardless of spaces or empty values. "${args[@]}" expands to exactly the elements you put in — no re-splitting, no globbing:
flags=(--exclude "*.log" --exclude "tmp dir")
rsync "${flags[@]}" "$src" "$dst"
Quote every expansion by default. "$var", "${arr[@]}", "$(cmd)". The rare case where you want splitting is the exception you comment; quoting is the default you never think about.
Fail fast, don't destroy state silently
Start scripts with the strict trio:
set -euo pipefail
-e(errexit) — abort on any command that exits non-zero.-u(nounset) — treat an unset variable as an error, so a typo'd$DIRRstops the script instead of expanding to nothing.-o pipefail— a pipeline's exit status is that of the last command to fail, not just the final stage, socurl … | tar …doesn't report success whencurldied.
Together they turn silent, half-finished damage into a loud, early exit. set -u alone would have caught the empty-$DIR disaster above.
Power user notes
set -ehas sharp edges. It is suppressed inside conditionals, in most pipeline stages, and in command substitutions (beforeinherit_errexit, Bash 4.4+). It is a safety net, not a substitute for checking exit codes on the commands that matter.pipefailinteracts with head/tee. It can surface aSIGPIPEwhen a reader likeheadcloses early. Know it before you debug a "random" failure.IFS=$'\n\t'is the fourth line of the common "strict mode" — it removes space from the field separator so accidental word splitting is far less destructive.- ShellCheck catches these statically. Run
shellcheck script.shin CI; SC2086 flags exactly the unquoted expansions this article is about, before they ever run.
Never construct commands in strings. Build argument lists as arrays, quote everything, and let the script fail loudly the instant something is off.
Resources
- Bash Pitfalls — Greg's Wiki (word splitting, quoting, arrays: pitfalls #1, #2, #14, #24)
- BashFAQ/105 — Why doesn't
set -edo what I expected? - The Set Builtin — GNU Bash manual (errexit, nounset, pipefail)
- Bash Arrays — GNU Bash manual
- ShellCheck SC2086 — quote to prevent word splitting
Shipping scripts that AI agents write and run? Yeda AI audits and hardens the automation in your stack.