Why this lesson
A backup script is told to delete old report.txt. It deletes a file called old instead, and complains that report.txt does not exist. Nothing is wrong with the logic - one pair of quotes is missing. This is the most common bash bug there is.
What you need to know already: variables and $name (6.1); what a command's arguments are - the words after the command name, e.g. rm a b gives rm two arguments (1.5).
Unquoted expansion does two things you did not ask for
file="old report.txt"
rm $file # rm gets "old" and "report.txt" <- TWO arguments
rm "$file" # rm gets "old report.txt" <- one
When bash expands an unquoted $var, it then does two more steps to the value:
- Word splitting - it cuts the value into separate words wherever it finds a character from IFS (space, tab, newline - 6.1).
- Globbing - each word that contains
*,?or[...]is treated as a glob, a filename pattern (*.log= every name ending in .log), and replaced by the matching file names.
So a value containing a space becomes two arguments, and a value containing * becomes a list of filenames. Double quotes "..." switch both steps off: the value stays exactly one argument.
pattern="*.log"
echo $pattern # expands to every .log file in the directory
echo "$pattern" # prints *.log
The rule: quote every expansion. "$var", "$(cmd)", "${arr[@]}" (an array, 6.12). The exceptions are rare enough to think about one at a time when they come up.
A script's own arguments: $1, $@ and $*
When you run ./deploy.sh prod "my app", the script receives its arguments in positional parameters: $1 is prod, $2 is my app, $# is how many there are (2), and $0 is the script's own name. Two special forms mean "all of them":
"$@" each argument stays a separate word, spaces preserved. ALWAYS this.
"$*" all arguments glued into ONE string, joined by a space.
$@ unquoted: word-splits every argument again. Never useful.
A wrapper is a small script or function that adds something and then calls another command with the same arguments:
wrapper() {
echo "running with $# arguments"
real_command "$@" # passes the arguments through exactly as given
}
With $* or unquoted $@, wrapper deploy "my app" silently becomes three arguments: deploy, my, app.
Where it bites
for f in $(ls); do ... # breaks on ANY filename with a space
for f in *; do ... # correct: the glob gives one word per file
for VAR in WORDS; do ...; done runs the body once per word, with $VAR set to each in turn. $(ls) produces text that is then word-split - so old report.txt turns into two loop rounds. A glob like * hands bash each filename whole. Never loop over the output of ls.
Single vs double quotes
'single: nothing is expanded - $var and $(cmd) stay as typed'
"double: $var and $(cmd) are expanded, but no splitting or globbing"
Single quotes for anything you want kept exactly - a pattern, a password, a program passed to another tool. Double quotes when you need the value in.
What you can now do
- Explain what happens to an unquoted
$var(splitting, then globbing). - Pass arguments through with
"$@"and loop over files with a glob, notls.