Lesson 2: Variables & Data Types
Goal: Master variables, quoting, substitution, and special variables in bash.
Table of Contents
- Defining Variables
- Quoting — The Most Important Rule
- Environment Variables
- Special Variables
- Command Substitution
- Parameter Expansion & Defaults
- Arithmetic
- Constants with readonly
- 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; usebcfor decimals)readonlyfor constantsexportto pass variables to child processes
Next Lesson: Lesson 3: Conditionals & Logic →