Lesson 2: Variables & Data Types

Goal: Master variables, quoting, substitution, and special variables in bash.


Table of Contents

  1. Defining Variables
  2. Quoting — The Most Important Rule
  3. Environment Variables
  4. Special Variables
  5. Command Substitution
  6. Parameter Expansion & Defaults
  7. Arithmetic
  8. Constants with readonly
  9. Exercises

Defining Variables

Basic assignment

# Correct — no spaces around =
NAME="Benjamin"
AGE=30
FILE_PATH="/var/log/syslog"

# WRONG — spaces cause errors
NAME = "Benjamin"    # Error: NAME: command not found

Using variables

echo "Hello, $NAME"
echo "You are $AGE years old"
echo "Log file: $FILE_PATH"

Braces for clarity

FRUIT="apple"
echo "I like ${FRUIT}s"     # I like apples
echo "I like $FRUITs"       # Error: $FRUITs is undefined!

Rule: Use ${var} when the variable is followed by characters that could be part of the name.


Quoting — The Most Important Rule

Quoting is the #1 source of bugs in shell scripts. Understand the three types:

Double quotes — Variables expanded

NAME="World"
echo "Hello, $NAME"       # Hello, World
echo "Path: $(pwd)"       # Path: /home/benjamin
echo "Files: $(ls | wc -l)"  # Files: 15

Single quotes — Everything literal

NAME="World"
echo 'Hello, $NAME'       # Hello, $NAME (literally)
echo 'No $(expansion)'    # No $(expansion) (literally)

No quotes — Word splitting & globbing

FILES="file1.txt file2.txt"

# Without quotes: word splitting happens
for f in $FILES; do echo "$f"; done
# file1.txt
# file2.txt

# With quotes: treated as single string
for f in "$FILES"; do echo "$f"; done
# file1.txt file2.txt

The Golden Rule

Always double-quote your variables: "$var"

Unless you explicitly want word splitting or globbing.

# WRONG — breaks on filenames with spaces
for f in $(ls); do echo $f; done

# CORRECT
for f in *; do echo "$f"; done

# WRONG — breaks if DIR has spaces
cd $DIR

# CORRECT
cd "$DIR"

# WRONG — expands globs, splits words
echo $USER_INPUT

# CORRECT
echo "$USER_INPUT"

Environment Variables

View all environment variables

env
printenv

Common environment variables

echo "$HOME"       # /home/benjamin
echo "$USER"       # benjamin
echo "$SHELL"      # /bin/bash
echo "$PATH"       # /usr/local/bin:/usr/bin:...
echo "$PWD"        # /current/directory
echo "$HOSTNAME"   # myserver
echo "$LANG"       # en_US.UTF-8
echo "$EDITOR"     # nano or vim
echo "$TERM"       # xterm-256color

Export — make available to child processes

# Only available in current shell
MY_VAR="hello"
bash -c 'echo $MY_VAR'   # (empty — child can't see it)

# Available to child processes
export MY_VAR="hello"
bash -c 'echo $MY_VAR'   # hello

Practical: .env files

# Create .env file
cat > .env << 'EOF'
DB_HOST=localhost
DB_PORT=5432
DB_USER=admin
DB_PASS=secret123
EOF

# Load .env into current shell
set -a              # auto-export all variables
source .env
set +a

echo "$DB_HOST"     # localhost
echo "$DB_PORT"     # 5432

Special Variables

#!/bin/bash

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Last exit code: $?"
echo "Current PID: $$"

Run it:

chmod +x special.sh
./special.sh hello world 42

Output:

Script name: ./special.sh
First argument: hello
Second argument: world
All arguments: hello world 42
Number of arguments: 3
Last exit code: 0
Current PID: 12345

$@ vs $*

#!/bin/bash
# Save as args-demo.sh

echo "--- Using \$@ (preserves quoting) ---"
for arg in "$@"; do
    echo "  Arg: '$arg'"
done

echo "--- Using \$* (merges into one string) ---"
for arg in "$*"; do
    echo "  Arg: '$arg'"
done
./args-demo.sh "hello world" foo bar

Output:

--- Using $@ (preserves quoting) ---
  Arg: 'hello world'
  Arg: 'foo'
  Arg: 'bar'
--- Using $* (merges into one string) ---
  Arg: 'hello world foo bar'

Rule: Always use "$@" to forward arguments.


Command Substitution

Execute a command and capture its output.

$(command) — modern syntax

TODAY=$(date +%Y-%m-%d)
echo "Today is $TODAY"

FILES=$(ls -1 | wc -l)
echo "Files in directory: $FILES"

KERNEL=$(uname -r)
echo "Kernel: $KERNEL"

IP=$(hostname -I | awk '{print $1}')
echo "IP Address: $IP"

Nested substitution

echo "Script is at: $(dirname $(readlink -f $0))"

Backticks (legacy — avoid)

# Old style — harder to read, can't nest
TODAY=`date +%Y-%m-%d`

# Modern — use this instead
TODAY=$(date +%Y-%m-%d)

Parameter Expansion & Defaults

Default values

# Use default if variable is unset or empty
NAME="${1:-Guest}"
echo "Hello, $NAME"

# ./script.sh          → Hello, Guest
# ./script.sh Benjamin → Hello, Benjamin

All default patterns

# Default value if unset/empty
echo "${VAR:-default}"

# Assign default if unset/empty
echo "${VAR:=default}"     # also sets VAR

# Error if unset/empty
echo "${VAR:?Variable VAR is required}"

# Use alternative if SET
echo "${VAR:+is set}"

Practical: Configuration with defaults

#!/bin/bash
set -euo pipefail

# Configuration with sensible defaults
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-myapp}"
APP_PORT="${APP_PORT:-3000}"
LOG_LEVEL="${LOG_LEVEL:-info}"

echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo "App listening on: $APP_PORT"
echo "Log level: $LOG_LEVEL"

This is the standard pattern used in Docker entrypoints.


Arithmetic

$(( )) — Arithmetic expansion

A=10
B=3

echo "$((A + B))"      # 13
echo "$((A - B))"      # 7
echo "$((A * B))"      # 30
echo "$((A / B))"      # 3 (integer division!)
echo "$((A % B))"      # 1 (modulo)
echo "$((A ** 2))"     # 100 (exponentiation)

Increment / decrement

COUNT=0
((COUNT++))
echo "$COUNT"    # 1

((COUNT += 5))
echo "$COUNT"    # 6

((COUNT--))
echo "$COUNT"    # 5

Floating point (use bc)

# Bash only does integers. For decimals, use bc:
echo "scale=2; 10 / 3" | bc     # 3.33
echo "scale=4; 22 / 7" | bc     # 3.1428

RESULT=$(echo "scale=2; 100 / 3" | bc)
echo "Result: $RESULT"           # Result: 33.33

Constants with readonly

#!/bin/bash
readonly APP_NAME="MyApp"
readonly VERSION="1.0.0"
readonly MAX_RETRIES=3

echo "$APP_NAME v$VERSION"

# This would cause an error:
# APP_NAME="Changed"   # Error: APP_NAME: readonly variable

Constants for script paths

#!/bin/bash
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="$SCRIPT_DIR/config.yaml"
readonly LOG_DIR="$SCRIPT_DIR/logs"

Exercises

Exercise 1: Variable basics

cat > vars.sh << 'EOF'
#!/bin/bash
set -euo pipefail

FIRST_NAME="Benjamin"
LAST_NAME="Tietz"
FULL_NAME="$FIRST_NAME $LAST_NAME"
GREETING="Hello, ${FULL_NAME}!"

echo "$GREETING"
echo "Name length: ${#FULL_NAME} characters"
echo "Uppercase: ${FULL_NAME^^}"
echo "Lowercase: ${FULL_NAME,,}"
EOF

chmod +x vars.sh && ./vars.sh
rm vars.sh

Exercise 2: Arguments and defaults

cat > greet.sh << 'EOF'
#!/bin/bash
set -euo pipefail

NAME="${1:-World}"
GREETING="${2:-Hello}"
TIMES="${3:-1}"

for ((i=1; i<=TIMES; i++)); do
    echo "$GREETING, $NAME! (${i}/${TIMES})"
done
EOF

chmod +x greet.sh
./greet.sh                        # Hello, World! (1/1)
./greet.sh "Benjamin" "Hi" 3     # Hi, Benjamin! (1/3) ...
rm greet.sh

Exercise 3: Environment and .env files

cat > app-config.sh << 'EOF'
#!/bin/bash
set -euo pipefail

# Load .env if it exists
if [[ -f .env ]]; then
    set -a
    source .env
    set +a
    echo "Loaded .env file"
fi

DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
APP_ENV="${APP_ENV:-development}"

echo "Environment: $APP_ENV"
echo "Database:    $DB_HOST:$DB_PORT"
EOF

# Create .env
echo "APP_ENV=production" > .env
echo "DB_HOST=db.example.com" >> .env

chmod +x app-config.sh && ./app-config.sh
rm app-config.sh .env

Key Takeaways

  • No spaces around = in assignments
  • Always double-quote variables: "$var"
  • "$@" to forward all arguments (not $*)
  • ${VAR:-default} for default values
  • $(command) for command substitution (not backticks)
  • $((...)) for arithmetic (integers only; use bc for decimals)
  • readonly for constants
  • export to pass variables to child processes

Next Lesson: Lesson 3: Conditionals & Logic →