What Happens When You Leave Out the Quotes
In a nutshell
The shell expands variables and then splits them into words. If you do not add quotes, a single value that contains a space turns into several arguments, and at that moment the script does something entirely different.
Why this was needed
Here is a cleanup script.
OLD=$(find /data -name '*.tmp' -mtime +7)
rm $OLD
The moment a file called /data/my report.tmp appears, rm tries to delete the two things /data/my and report.tmp. With bad luck, a directory called /data/my really exists, and it disappears.
Two problems are stacked here.
- No quotes - word splitting happens.
- Line-by-line parsing - a file name can contain a newline (it is possible).
How it works
Rule 1 - always quote variables
f="my report.txt"
rm $f # rm my report.txt → 인자 2개
rm "$f" # rm "my report.txt" → 인자 1개 ✅
There are almost no exceptions. The day the judgment "I'm sure this won't have a space" is wrong is the day the script breaks.
The same goes for arrays.
args=(-l -a "my dir")
ls "${args[@]}" # 각 원소가 하나의 인자로 ✅
ls ${args[@]} # 다시 쪼개진다 ✗
The difference between "$@" and "$*" is the same principle. "$@" preserves each argument separately, while "$*" joins them into one string. When you pass on the arguments the script received as they are, it is always "$@".
Rule 2 - pass file lists with NUL
The only character that cannot appear in a file name is NUL. Newlines and spaces can both appear. So a safe pipe is NUL-delimited.
# 안전
find /data -name '*.tmp' -mtime +7 -print0 | xargs -0 rm --
# 더 안전 (xargs 도 필요 없다)
find /data -name '*.tmp' -mtime +7 -delete
# 루프가 필요하면
while IFS= read -r -d '' f; do
process "$f"
done < <(find /data -name '*.tmp' -print0)
The IFS= read -r combination is also an idiom. IFS= prevents leading and trailing whitespace from being trimmed, and -r prevents backslashes from being interpreted as escapes.
Rule 3 - separate options from files with --
If a file named -rf exists (you can create one), rm $file interprets it as an option. -- is a marker meaning "everything from here on is an argument".
rm -- "$f"
grep -- "$pattern" "$file"
Rule 4 - parse options with getopts
Code that loops over $1 by hand with a case soon falls apart. For short options, getopts is the standard.
verbose=0; out=""
while getopts ":vo:" opt; do
case $opt in
v) verbose=1 ;;
o) out=$OPTARG ;;
:) echo "-$OPTARG 에 값이 필요합니다" >&2; exit 2 ;;
\?) echo "알 수 없는 옵션: -$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
# 남은 것이 위치 인자
The leading colon (":vo:") turns on quiet error mode. You need it so that the : and \? branches work and we can build the messages ourselves.
Rule 5 - do not trust input
Never turn user input into a command.
# 절대 금지
eval "grep $user_input file.txt"
# 값으로만 쓴다
grep -- "$user_input" file.txt
eval executes a string as shell code. If ; rm -rf / comes in as input, it is executed as is. Likewise, validate file paths too - if a value like ../../etc/passwd arrives, it escapes the intended directory.
Checking tools
shellcheck catches most of the mistakes above. It points out missing quotes (SC2086), read calls without -r (SC2162), and even a useless cat (SC2002). If you put it in CI, you do not have to repeat these remarks in reviews.
What it looks like in the field
- A batch job runs fine for years, then one day deletes a file → the day a file with a space first appeared.
- It fails once Korean characters appear in a path → usually a quoting problem, not an encoding problem.
- The behavior changes when you swap the order of options → the typical symptom of a hand-made parser.
What you will do in the lab that follows
You will create files with hostile names yourself and test the rules one by one. The grader runs the script you wrote directly. If "$@" is replaced by $*, 3 arguments arrive as 5; a hand-rolled parser instead of getopts falls apart on -vo; and if you use eval, the injection string the grader feeds in is actually executed. An answer that is merely written down cannot pass.