Skip to content

Variables & Quoting

Quoting is the most common source of subtle bugs in bash — a single missing quote can turn a command that handles one file into one that silently handles dozens, or breaks on filenames with spaces.

Learning Objectives

  • Declare, assign, and read shell variables
  • Understand the difference between local, environment, and special variables
  • Apply the three quoting styles: single quotes, double quotes, no quotes
  • Use special variables: $0, $#, $@, $?, $$
  • Export variables to child processes

Variables

Declaring and Reading Variables

name="Nikhil"
echo $name
Nikhil

# No spaces around = in assignments
count=42
greeting="Hello, World"
pi=3.14159

# Read a variable
echo "$count"       # always quote variable references
echo "${count}"     # braces make the boundary explicit

No spaces around =

count = 42 is a syntax error in bash — bash sees count as a command with arguments = and 42. Variable assignment requires name=value with no spaces.

Environment Variables

Environment variables are inherited by child processes. View them with env or printenv:

echo "$HOME"        # your home directory
echo "$PATH"        # directories searched for commands
echo "$USER"        # your username
echo "$PWD"         # current directory (same as pwd)
echo "$SHELL"       # your default shell

Export a variable to make it available to child processes:

MY_VAR="hello"
export MY_VAR
bash -c 'echo "$MY_VAR"'    # child bash sees it
hello

# Or in one line
export MY_VAR="hello"

Without export, the variable exists only in the current shell:

MY_VAR="hello"               # not exported
bash -c 'echo "$MY_VAR"'     # empty — child does not see it


Special Variables

Variable Meaning
$0 Name of the script
$1$9 Positional arguments
$# Number of arguments
$@ All arguments as separate words
$* All arguments as a single word
$? Exit status of the last command
$$ PID of the current shell
$! PID of the last background process
#!/usr/bin/env bash
echo "Script name: $0"
echo "First arg:   $1"
echo "All args:    $@"
echo "Arg count:   $#"

Running bash myscript.sh foo bar baz:

Script name: myscript.sh
First arg:   foo
All args:    foo bar baz
Arg count:   3

# Check if a command succeeded
ls /nonexistent 2>/dev/null
echo "Exit status: $?"
Exit status: 2


Quoting

No Quotes — Word Splitting and Glob Expansion Happen

file="my report.txt"
cat $file             # WRONG: cat sees "my" and "report.txt" as separate args
cat "$file"           # CORRECT: cat sees one argument with a space in it

Unquoted variables also undergo glob expansion:

pattern="*.txt"
ls $pattern           # expands the glob — lists all .txt files
ls "$pattern"         # treats *.txt as a literal filename

Double Quotes — Variables and Command Substitution Expand, Globs Don't

name="Nikhil"
echo "Hello, $name"         # Hello, Nikhil
echo "Files: $(ls | wc -l)" # Files: 23
echo "Literal: \$name"      # Literal: $name

Double quotes protect against word splitting and glob expansion while still allowing variable and command substitution.

Single Quotes — Everything Is Literal

echo 'Hello, $name'         # Hello, $name  (no expansion)
echo 'Cost: $5.00'          # Cost: $5.00
echo 'No $(expansion) here' # No $(expansion) here

Single quotes prevent all expansion and substitution.

The rule to follow

Quote every variable reference with double quotes unless you have a specific reason not to. "$var", not $var. "$@", not $@. This prevents word-splitting bugs on filenames with spaces.


Command Substitution

Run a command and capture its output into a variable:

today=$(date +%Y-%m-%d)
echo "Today is $today"
Today is 2024-01-15

file_count=$(ls | wc -l)
echo "There are $file_count files here"

$() vs backticks

The modern syntax is $(command). Backtick syntax `command` is older and harder to nest. Use $() in all new scripts.


Variable Defaults and Errors

# Use a default if the variable is unset or empty
echo "${name:-"stranger"}"

# Assign a default if the variable is unset
: "${name:="stranger"}"

# Error out if the variable is unset
: "${REQUIRED_VAR:?Variable REQUIRED_VAR must be set}"

Common Mistakes

Quoting $@ vs $*

Use "$@" (double-quoted) to pass arguments to another command. "$@" expands to a list of separately quoted words. "$*" joins all arguments with a space into one string — which breaks if your arguments contain spaces.

Variable names are case-sensitive

$PATH and $path are different variables. Convention: use uppercase for environment variables and exported variables; use lowercase for local script variables.


Practice Exercises

Warm-Up (run and observe)

  1. Create a variable city="New York". Try echo $city and echo "$city". What is the difference when the value has a space?
  2. Run echo $? immediately after a successful ls. Run it after ls /nonexistent. What values do you see?
  3. Run echo $$. Then open another terminal and run echo $$. What do the numbers represent?

Main (write a short script)

Create ~/scripts/greet.sh that accepts a name as an argument and greets the user:

#!/usr/bin/env bash
set -euo pipefail

NAME="${1:?Usage: $0 <name>}"
TIMESTAMP=$(date +"%Y-%m-%d %H:%M")

echo "Hello, $NAME!"
echo "The time is: $TIMESTAMP"
echo "You are running: $0"
echo "Shell: $SHELL"

Test it: bash ~/scripts/greet.sh "Ada Lovelace"

Stretch

  1. What is the difference between $@ and $* when they are unquoted? When they are double-quoted? Write a script that demonstrates the difference with a filename containing a space.
  2. How do you append to a variable without overwriting it? (e.g., add a directory to PATH)
  3. Research declare -r VAR=value. What does -r do? What happens if you try to reassign a readonly variable?

Interview Questions

  1. What happens if you reference a variable that was never assigned?
Show answer

By default, bash treats unset variables as empty strings — no error. With set -u (or set -euo pipefail), referencing an unset variable causes the script to exit with an error. This is why set -u is recommended in production scripts.

  1. What is the difference between $@ and $*?
Show answer

When double-quoted: "$@" expands to a list of individually quoted words ("$1" "$2" "$3"). "$*" expands to a single string with all arguments joined by the first character of IFS (usually a space: "$1 $2 $3"). Use "$@" when passing arguments to other commands to preserve arguments that contain spaces.

  1. Why is count = 42 a syntax error in bash?
Show answer

Bash sees count as a command name and = and 42 as its arguments. Variable assignment requires name=value with no spaces. The parser treats anything before the first = (with no spaces) as the variable name.


day02-part2-stream-editing | day03-part2-user-input-expansion