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=1 works; x = 1 doesn’t.
  • Forgetting quotes: $var word-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 pipefail if 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *