Skip to main content

🧮 Lesson 8.2: Variables, Input, and Conditionals

Make your scripts dynamic — store data, ask questions, and make decisions.

🎯 Learning Objectives

  • Create and use variables (with proper quoting)
  • Work with environment variables and export
  • Capture command output with $()
  • Read user input with read
  • Write if/elif/else conditionals
  • Use test expressions for strings, numbers, and files

Estimated Time: 45 minutes

📑 In This Lesson

Variables

Variables store data that your script can use and reuse. In Bash, variable assignment has one critical rule: no spaces around the =.

#!/bin/bash

# ✅ Correct — no spaces
NAME="Ray"
AGE=30
GREETING="Hello, $NAME"

# ❌ WRONG — spaces cause errors
# NAME = "Ray"    → Bash thinks NAME is a command
# NAME= "Ray"     → Also broken

# Use variables with $
echo "Name: $NAME"
echo "Age: $AGE"
echo "$GREETING"

# Curly braces for clarity (required when variable is next to other text)
FILE="report"
echo "${FILE}_final.txt"    # report_final.txt
echo "$FILE_final.txt"      # WRONG — Bash looks for variable $FILE_final

⚠️ The #1 Bash Beginner Mistake

Spaces around = in variable assignment will break your script. NAME="Ray" is assignment. NAME = "Ray" tries to run a command called NAME with arguments = and "Ray". This catches everyone at least once.

Quoting Rules

Quoting is one of the most important (and confusing) parts of Bash. Here's the cheat sheet:

Quote TypeExpands Variables?Use When
"double quotes"✅ YesMost of the time — protects spaces while expanding variables
'single quotes'❌ NoYou want the literal text, no expansion at all
No quotes✅ YesSimple single-word values only (avoid for anything with spaces)
NAME="Ray De La Paz"

echo "Hello, $NAME"     # Hello, Ray De La Paz
echo 'Hello, $NAME'     # Hello, $NAME (literal)
echo Hello, $NAME       # Hello, Ray De La Paz (works, but fragile)

# Why double-quoting matters:
FILE="my document.txt"
cat $FILE       # ❌ Bash sees: cat my document.txt (two arguments!)
cat "$FILE"     # ✅ Bash sees: cat "my document.txt" (one argument)

🐧 Golden Rule of Bash

Always double-quote your variables unless you have a specific reason not to. "$VAR" prevents word splitting and glob expansion — two of the most common sources of bugs in shell scripts.

Environment Variables

Environment variables are inherited by child processes. Regular variables are not.

# Regular variable (only visible in this script/shell)
MY_VAR="local only"

# Environment variable (visible to child processes)
export MY_ENV_VAR="shared with children"

# One-liner: export at assignment
export API_KEY="abc123"

# View all environment variables
env
# or
printenv

# Some important built-in environment variables:
echo "$HOME"        # Your home directory (/home/ray)
echo "$USER"        # Current username (ray)
echo "$PATH"        # Where Bash looks for commands
echo "$PWD"         # Current working directory
echo "$SHELL"       # Your default shell (/bin/bash)
echo "$HOSTNAME"    # Machine name
echo "$RANDOM"      # A random number (0-32767)
echo "$$"           # Current process ID (PID)
graph TD A["Parent Shell
MY_VAR=hello
export ENV_VAR=world"] --> B["Child Script"] B --> C["Can see ENV_VAR ✅
Cannot see MY_VAR ❌"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#6366f1,stroke:#4338ca,color:#fff style C fill:#f59e0b,stroke:#d97706,color:#fff

💡 Persistent Environment Variables

Variables set in a terminal session disappear when you close it. To make them permanent, add export lines to ~/.bashrc (loaded for every new interactive shell) or ~/.profile (loaded at login).

Command Substitution

Capture the output of a command and store it in a variable:

# Modern syntax (preferred): $(command)
TODAY=$(date +%Y-%m-%d)
echo "Today is $TODAY"

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

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

# Old syntax (backticks) — still works but harder to nest
TODAY=`date +%Y-%m-%d`

# Nesting is clean with $():
echo "Files modified today: $(find . -newer $(date +%Y-%m-%d) -type f | wc -l)"
# vs. backticks (messy with escaping):
# echo "Files: `find . -newer \`date +%Y-%m-%d\` -type f | wc -l`"

💡 Arithmetic

Bash can do integer math with $(( )):

A=10
B=3
echo "Sum: $((A + B))"        # 13
echo "Product: $((A * B))"    # 30
echo "Division: $((A / B))"   # 3 (integer only!)
echo "Remainder: $((A % B))"  # 1

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

User Input with read

#!/bin/bash

# Basic input
echo -n "Enter your name: "
read NAME
echo "Hello, $NAME!"

# Prompt built into read (-p flag)
read -p "Enter your age: " AGE
echo "You are $AGE years old."

# Silent input for passwords (-s flag)
read -sp "Enter password: " PASSWORD
echo ""   # Newline after silent input
echo "Password is ${#PASSWORD} characters long."

# Read with a timeout (-t seconds)
read -t 5 -p "Quick! Favorite color? " COLOR || echo "(Too slow!)"

# Read with a default value
read -p "Directory [/tmp]: " DIR
DIR="${DIR:-/tmp}"    # Use /tmp if empty
echo "Using directory: $DIR"

# Read multiple values
read -p "Enter first and last name: " FIRST LAST
echo "First: $FIRST, Last: $LAST"

💡 Default Values with ${VAR:-default}

The ${VAR:-default} syntax returns default if $VAR is empty or unset. It's the standard way to provide fallback values:

NAME="${1:-World}"           # First argument, or "World"
LOG_DIR="${LOG_DIR:-/var/log}" # Env var, or /var/log
echo "Hello, $NAME!"

Script Arguments

Scripts can accept arguments from the command line — no need for interactive prompts.

#!/bin/bash
# greet.sh — Greets the user by name

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
$ ./greet.sh Ray Developer
Script name: ./greet.sh
First argument: Ray
Second argument: Developer
All arguments: Ray Developer
Number of arguments: 2
VariableMeaning
$0The script name itself
$1, $2, …First, second, … argument
$@All arguments (each as a separate word)
$*All arguments (as a single string)
$#Number of arguments
#!/bin/bash
# A script that requires exactly one argument

if [ $# -ne 1 ]; then
    echo "Usage: $0 <filename>" >&2
    exit 1
fi

echo "Processing file: $1"

if / elif / else

Conditionals let your script make decisions. The basic structure:

if [ condition ]; then
    # commands if true
elif [ another_condition ]; then
    # commands if this is true
else
    # commands if nothing above was true
fi

⚠️ Spaces Matter Inside [ ]

The brackets [ ] are actually a command (shorthand for test). Spaces after [ and before ] are required:

# ✅ Correct
if [ "$NAME" = "Ray" ]; then

# ❌ WRONG — will error
if ["$NAME" = "Ray"]; then
#!/bin/bash

read -p "Enter a number: " NUM

if [ "$NUM" -gt 100 ]; then
    echo "That's a big number!"
elif [ "$NUM" -gt 0 ]; then
    echo "That's a positive number."
elif [ "$NUM" -eq 0 ]; then
    echo "That's zero."
else
    echo "That's a negative number."
fi
graph TD A["Input number"] --> B{"Greater than 100?"} B -->|Yes| C["Big number!"] B -->|No| D{"Greater than 0?"} D -->|Yes| E["Positive number"] D -->|No| F{"Equal to 0?"} F -->|Yes| G["Zero"] F -->|No| H["Negative number"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#6366f1,stroke:#4338ca,color:#fff style D fill:#6366f1,stroke:#4338ca,color:#fff style F fill:#6366f1,stroke:#4338ca,color:#fff style C fill:#22c55e,stroke:#166534,color:#fff style E fill:#22c55e,stroke:#166534,color:#fff style G fill:#f59e0b,stroke:#d97706,color:#fff style H fill:#ef4444,stroke:#b91c1c,color:#fff

Test Expressions

String Comparisons

TestMeaning
[ "$a" = "$b" ]Strings are equal
[ "$a" != "$b" ]Strings are not equal
[ -z "$a" ]String is empty (zero length)
[ -n "$a" ]String is not empty

Numeric Comparisons

TestMeaning
[ "$a" -eq "$b" ]Equal
[ "$a" -ne "$b" ]Not equal
[ "$a" -gt "$b" ]Greater than
[ "$a" -ge "$b" ]Greater than or equal
[ "$a" -lt "$b" ]Less than
[ "$a" -le "$b" ]Less than or equal

File Tests

TestMeaning
[ -e "$f" ]File exists (any type)
[ -f "$f" ]Is a regular file
[ -d "$f" ]Is a directory
[ -r "$f" ]Is readable
[ -w "$f" ]Is writable
[ -x "$f" ]Is executable
[ -s "$f" ]File exists and is not empty

Combining Tests

# AND (both must be true)
if [ "$AGE" -ge 18 ] && [ "$AGE" -le 65 ]; then
    echo "Working age"
fi

# OR (either can be true)
if [ "$COLOR" = "red" ] || [ "$COLOR" = "blue" ]; then
    echo "Primary color!"
fi

# NOT (invert)
if [ ! -f "$FILE" ]; then
    echo "File does not exist"
fi

🐧 [[ ]] — The Bash Upgrade

Bash offers [[ ]] (double brackets) with extra features: pattern matching, regex, and safer quoting. If you're writing Bash-specific scripts (not POSIX sh), prefer [[ ]]:

# Pattern matching (globbing)
if [[ "$FILE" == *.txt ]]; then
    echo "It's a text file"
fi

# Regex matching
if [[ "$EMAIL" =~ ^[a-zA-Z]+@[a-zA-Z]+\.[a-zA-Z]+$ ]]; then
    echo "Looks like an email"
fi

# No word splitting — no need to quote variables (but still good practice)
if [[ $NAME = "Ray" ]]; then echo "Hi Ray"; fi

Practical Example

Here's a script that combines everything — a file backup utility with input validation:

#!/bin/bash
# ============================================
# backup.sh — Back up a directory with a timestamp
# Usage: ./backup.sh [source_directory]
# ============================================

# Get source from argument or ask
SOURCE="${1:-}"
if [ -z "$SOURCE" ]; then
    read -p "Directory to back up: " SOURCE
fi

# Validate the source
if [ ! -d "$SOURCE" ]; then
    echo "ERROR: '$SOURCE' is not a directory." >&2
    exit 1
fi

# Set up the backup
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$(basename "$SOURCE")_${TIMESTAMP}.tar.gz"
DEST_DIR="${HOME}/backups"

# Create destination if needed
if [ ! -d "$DEST_DIR" ]; then
    mkdir -p "$DEST_DIR"
    echo "Created backup directory: $DEST_DIR"
fi

# Perform the backup
echo "Backing up '$SOURCE' → '$DEST_DIR/$BACKUP_NAME'"
tar -czf "$DEST_DIR/$BACKUP_NAME" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"

# Check if it worked
if [ $? -eq 0 ]; then
    SIZE=$(du -h "$DEST_DIR/$BACKUP_NAME" | awk '{print $1}')
    echo "✅ Backup complete! Size: $SIZE"
    exit 0
else
    echo "❌ Backup failed!" >&2
    exit 1
fi

Exercises

🏋️ Exercise 1: Variable Practice

Create a script called myinfo.sh that:

  1. Stores your name, favorite color, and OS in variables
  2. Captures the current date with command substitution
  3. Prints all four values in a formatted message
  4. Uses ${VAR} syntax at least once

🏋️ Exercise 2: Interactive Greeter

Create greeter.sh that:

  1. Asks for the user's name (with a default of "friend")
  2. Asks for the current hour (0-23)
  3. Prints "Good morning", "Good afternoon", or "Good evening" based on the hour
  4. Handles invalid input (non-numeric, out of range)

🏋️ Exercise 3: File Checker

Create filecheck.sh that takes a filename as an argument and reports:

  1. Whether it exists
  2. Whether it's a file or directory
  3. Whether it's readable, writable, and executable
  4. If no argument is given, print a usage message and exit with code 1

🏋️ Exercise 4: Number Guessing Game

Create guess.sh — a number guessing game:

  1. Generate a random number 1-100: TARGET=$((RANDOM % 100 + 1))
  2. Ask the user to guess
  3. Tell them if the guess is too high, too low, or correct
  4. Count the number of guesses
  5. Hint: You'll need a loop — preview of the next lesson! Use while true; do ... done

Knowledge Check

❓ Question 1

What happens if you write NAME = "Ray" (with spaces)?

❓ Question 2

What does ${DIR:-/tmp} do?

❓ Question 3

Which test checks if a file exists and is a regular file?

❓ Question 4

What does $# contain inside a script?

Summary

🎉 Key Takeaways

  • Variables: NAME="value" (no spaces!). Access with $NAME or ${NAME}
  • Always double-quote variables: "$VAR" prevents word splitting bugs
  • export VAR makes variables visible to child processes
  • $(command) captures command output in a variable
  • read -p "Prompt: " VAR gets user input; ${VAR:-default} provides fallbacks
  • $1, $2, $@, $# handle script arguments
  • if [ condition ]; then ... fi for decisions (mind the spaces!)
  • Use -eq/-gt/-lt for numbers, =/!= for strings, -f/-d/-e for files

🍎 On macOS

All the variable and conditional syntax in this lesson works with #!/bin/bash scripts on macOS. One caveat: macOS ships with Bash 3.2 (from 2007) due to licensing. This means some newer Bash 4+ features (associative arrays, readarray) won't work unless you install a newer Bash via brew install bash. Everything in this lesson uses Bash 3-compatible syntax, so you're fine.

🚀 What's Next?

The final lesson covers Loops and Functions — repeating actions and organizing your code into reusable pieces.