Guides

Why your bash script breaks on spaces — and deletes the wrong file

Illustration: a long label is cut into two halves, and each half gets hooked onto the wrong box

A folder holds three files: my, file.txt, and my file.txt. You delete the third one:

$ F="my file.txt"
$ rm $F
$ ls
my file.txt

The file you wanted is still alive. my and file.txt are gone. The command deleted two innocent files and didn't touch the one you asked about.

The error shows up quietly or loudly. Sometimes like this: cat: my: No such file or directory. Sometimes like this: [: my: binary operator expected. Sometimes too many arguments. The cause is almost always one of these four.

Cause 1: no quotes around the variable

The most common one. Bash first substitutes the value and then splits the result on spaces. So rm $F becomes rm my file.txt, which is two arguments.

How to check. Show what the command actually receives. printf prints each argument in its own brackets:

$ printf '[%s]\n' $F
[my]
[file.txt]
$ printf '[%s]\n' "$F"
[my file.txt]

How to fix. Put every variable in double quotes: rm "$F". For a beginner this rule has no exceptions. Substitution with $(...) needs quotes too: DIR="$(pwd)".

Here's a sneaky case. [ -f $F ] with an empty variable turns into [ -f ], which is the test "the string -f is not empty." The answer: true. No file exists and the condition passes. With quotes, [ -f "$F" ] is honest.

Cause 2: looping over ls output

The folder holds a b.txt and c.txt. The loop sees three files:

$ for f in $(ls *.txt); do echo "<$f>"; done
<a>
<b.txt>
<c.txt>

How to check. Run the loop with echo, as above, before you put rm or mv in it. That's a dry run, and it's free.

How to fix. Let bash enumerate the names itself. It doesn't split the result on spaces:

$ for f in *.txt; do echo "<$f>"; done
<a b.txt>
<c.txt>

Inside the loop still write "$f". There's a second subtlety: with no matches, f holds the literal text *.txt. A shopt -s nullglob line above the loop turns that off. Bulk renaming with this kind of protection is covered in how to rename many files at once.

For files in nested folders, separate names with a null byte: find . -name '*.log' -print0 | xargs -0 rm.

Cause 3: a wrapper script passes arguments through $*

You wrote a wrapper that forwards its arguments. Here's what happens to the arguments "one two" and three:

$ args() { printf '[%s]\n' "$@"; }
$ set -- "one two" three
$ args $*
[one]
[two]
[three]
$ args "$*"
[one two three]
$ args "$@"
[one two]
[three]

How to check. Swap the printf from the example in for the real command.

How to fix. Only "$@", and always with the quotes. It's the one form that keeps arguments the way they arrived.

Cause 4: the variable turned out empty

Here quotes no longer save you. In January 2015 Steam for Linux wiped users' home folders. The script had the line rm -rf "$STEAMROOT/"*, and it did have quotes. But the variable turned out empty, and the command became rm -rf /*.

How to check. Run the script with bash -u script.sh. Any use of an unset variable becomes an error.

How to fix. Make the script fail before rm can do damage:

$ bash -c 'echo "rm -rf ${NOPE:?not set}/"'
bash: line 1: NOPE: not set

The ${VAR:?} form stops the script if the variable is empty or unset. Put it in every dangerous command. And add set -u at the top of the script.

Note that the failed line returns a nonzero exit code. That's what to check in automation.

How to stop catching this by hand

There's a ready-made program for it. ShellCheck finds all these mistakes before you run anything:

$ shellcheck s.sh
In s.sh line 3:
rm $F
   ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

Install it (brew install shellcheck or apt install shellcheck), or paste your script at shellcheck.net. Run everything an AI agent wrote through it before executing, especially if the script has rm, mv, or sudo. And add it to your prompt from the start: "check the script with shellcheck and fix every warning." Agents can do this on their own.

Keep the short version: quotes around every variable, loops without ls, "$@" for arguments, ${VAR:?} before rm.

If the terminal still feels foreign, start with the basic commands for beginners.

Why did it work in my Mac terminal?

In zsh, the default on Macs, unquoted variables aren't split on spaces. But a script with #!/bin/bash as its first line runs in bash, and there it breaks.

What's the difference between single and double quotes?

Double quotes substitute variables, single quotes don't: '$F' stays literally $F. For variables you need double quotes.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →