Bash Variables and Quoting
Variables are how shell scripts hold and pass data. Bash’s variable system is simple in principle and full of gotchas in practice — most of them around quoting. Get the quoting right and your scripts work on any input. Get it wrong and they break the moment a filename has a space.
Set and read
# Set (NO spaces around =)
name="alice"
count=42
# Read (use $)
echo $name
echo "$name" # always prefer quoted
# WRONG (this looks like a command!)
name = "alice" # bash: name: command not found
Single vs double vs no quotes
| Style | Variable expansion? | Word splitting? |
|---|---|---|
"$var" |
Yes | No (treats as one word) |
'$var' |
No (literal $var) | No |
$var |
Yes | Yes (splits on spaces!) |
Why this matters
file="my report.pdf"
rm $file # WRONG: tries to remove "my" and "report.pdf"
rm "$file" # CORRECT: removes "my report.pdf"
Always double-quote variable expansions unless you have a specific reason not to.
Special variables
$0 # script name
$1 $2 # first, second positional argument
$@ # all arguments as separate words ("$@" preserves them)
$* # all arguments as one string
$# # number of arguments
$? # exit status of last command
$$ # PID of current shell
$! # PID of last background command
$_ # last argument of last command
Default values
# Use default if unset
name=${USER_NAME:-anonymous}
# Set if unset
: ${PORT:=8080}
# Error if unset
: ${API_KEY:?must set API_KEY}
# Use only if set
greeting=${USER_NAME:+"Hello, $USER_NAME!"}
Substring and length
str="hello world"
echo "${#str}" # length: 11
echo "${str:0:5}" # substring: hello
echo "${str:6}" # from position 6: world
# Replace
echo "${str/world/bash}" # hello bash
echo "${str//l/L}" # heLLo worLd (global)
# Trim
file="document.tar.gz"
echo "${file%.gz}" # document.tar (remove shortest match from end)
echo "${file%%.*}" # document (longest match from end)
echo "${file#*.}" # tar.gz (remove shortest from start)
echo "${file##*.}" # gz (longest from start)
Arrays
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # apple
echo "${fruits[@]}" # all elements
echo "${#fruits[@]}" # count: 3
# Append
fruits+=("date")
# Loop
for f in "${fruits[@]}"; do
echo "$f"
done
# Associative arrays (bash 4+)
declare -A scores
scores["alice"]=95
scores["bob"]=82
echo "${scores["alice"]}"
Command substitution
# Modern (preferred)
files=$(ls *.txt)
date=$(date +%F)
# Old backticks (still works)
files=`ls *.txt`
# Use the result
for f in $(ls *.txt); do # WRONG if filenames have spaces
echo "$f"
done
# Better: use a glob directly
for f in *.txt; do
echo "$f"
done
Arithmetic
count=10
total=$((count * 5)) # 50
((count++)) # increment
((count > 5)) && echo "big" # comparison
# Float math: use bc or awk
echo "scale=2; 10/3" | bc # 3.33
awk "BEGIN {print 10/3}" # 3.33333
Read from stdin or prompt
read -p "Your name: " name
echo "Hi, $name"
read -s -p "Password: " pw # silent
echo
# Read each line of a file
while IFS= read -r line; do
echo "got: $line"
done < input.txt
Common mistakes
- Spaces around =:
x=1works;x = 1doesn’t. - Forgetting quotes:
$varword-splits. Use"$var". - “$@” vs $@:
"$@"preserves each arg as one word;$@splits them again. - Using $? after a pipe: that’s the exit status of the LAST command in the pipe. Use
set -o pipefailif you care about earlier ones.
What to learn next
Variables alone don’t do much. Loops, conditionals, and functions turn them into actual programs. Up next.