Eli's Shell Script Quest

A hands-on adventure through the terminal — from first commands to scripting mastery

$ echo "Let's roll initiative on some shell scripts"

Welcome, Eli

This is your personal roadmap for learning shell scripting. Everything here is designed so you can learn by doing — open your Terminal on your Mac, follow along, and try every command yourself.

You'll be using zsh (the default shell on macOS) and learning bash-compatible scripting — the skills transfer between both. We'll start with navigating the terminal and editing files with nano, then level up from there.

Each module is a new chapter. Work through them in order. Don't rush — the goal is understanding, not speed.

How to Use This Page

Click any module header to expand it. Each module has lessons, commands to try in your terminal, and a quest (a mini-project) at the end. Check off items as you go!

1

Opening Night: Meet the Terminal

Find your way around the command line — your new backstage pass
▼

▶ Opening Terminal

On your Mac, open Terminal (search for it in Spotlight with Cmd + Space, then type "Terminal"). This is where you'll do everything.

When it opens, you'll see something called a prompt. It might look like:

eli@macbook ~ %

That % at the end means you're in zsh. Bash uses $. Both work almost identically for what we'll be doing.

▶ Where Am I? (pwd, ls, cd)

The terminal always has a "current directory" — think of it as the room you're standing in.

# Print Working Directory — shows where you are
pwd
/Users/eli

# List what's in the current directory
ls
Desktop  Documents  Downloads  Music  ...

# List with more detail (long format, human-readable sizes)
ls -lh

# Change Directory — move into a folder
cd Documents

# Go back up one level
cd ..

# Go straight home from anywhere
cd ~

✍ Try It Yourself

  1. Open Terminal
  2. Run pwd — what directory are you in?
  3. Run ls to see what's there
  4. Run ls -lh — notice the extra info (permissions, sizes, dates)
  5. cd Desktop, then pwd again to confirm you moved
  6. cd ~ to go back home

▶ Making and Removing Things (mkdir, touch, rm)

# Create a new directory (folder)
mkdir shell-quest

# Move into it
cd shell-quest

# Create an empty file
touch hello.txt

# Verify it's there
ls
hello.txt

# Remove a file (careful — no trash can!)
rm hello.txt

# Remove a directory (must be empty, or use -r)
rmdir some-empty-folder
rm -r some-folder-with-stuff

⚠ Heads Up

rm is permanent. There's no undo, no Trash. Double-check before you delete. A good habit: run ls first to make sure you know what you're about to remove.

▶ Reading Files (cat, less, head, tail)

# Print the entire file to the screen
cat somefile.txt

# Scroll through a long file (press q to quit)
less somefile.txt

# Just see the first 10 lines
head somefile.txt

# Just see the last 10 lines
tail somefile.txt

# See a specific number of lines
head -n 5 somefile.txt

▶ Useful Shortcuts

  • Tab — autocompletes file/folder names (use this constantly!)
  • Up Arrow — scrolls through your previous commands
  • Ctrl + C — cancel/stop the current running command
  • Ctrl + L — clear the screen (or type clear)
  • Ctrl + A — jump to the beginning of the line
  • Ctrl + E — jump to the end of the line

🎲 Module 1 Quest: Set the Stage

Create your workspace for this entire curriculum:

  1. Navigate to your home directory (cd ~)
  2. Create a directory called shell-quest
  3. Inside it, create three sub-directories: scripts, notes, projects
  4. Run ls -R shell-quest to see the whole structure (-R means recursive)
  5. Create a file called quest-log.txt inside notes
  • Created ~/shell-quest
  • Created scripts, notes, projects inside it
  • Verified with ls -R
  • Created quest-log.txt
2

The Editor's Booth: Learning nano

Edit files from the terminal like a sound engineer tweaking levels
▼

▶ Why Edit in the Terminal?

When you're working on a remote server, SSHing into a machine, or just want to stay in the flow, opening a graphical text editor slows you down. Terminal editors let you create and modify files without ever leaving the command line.

nano is the friendliest terminal editor — it shows its controls right on screen. Think of it as the sound board's "easy mode" before you graduate to the full mixing console (vim, eventually).

▶ Opening and Creating Files

# Open (or create) a file
nano myfile.txt

# Open a file with line numbers shown
nano -l myfile.txt

When nano opens, you'll see a cursor and a bar at the bottom showing keyboard shortcuts. The ^ symbol means Ctrl.

▶ Essential nano Controls

Shortcut What It Does
Ctrl + OSave ("Write Out") — press Enter to confirm
Ctrl + XExit nano (will ask to save if modified)
Ctrl + KCut the current line
Ctrl + UPaste ("Uncut") the cut line
Ctrl + WSearch for text
Ctrl + GHelp screen
Ctrl + /Go to a specific line number

✍ Try It Yourself

  1. cd ~/shell-quest/notes
  2. nano quest-log.txt
  3. Type: "Module 1 complete. I can navigate directories and create files."
  4. Save with Ctrl + O, then Enter
  5. Exit with Ctrl + X
  6. Verify: cat quest-log.txt

▶ nano Tips & Tricks

  • You can cut multiple lines by pressing Ctrl + K repeatedly — they all get added to the paste buffer
  • Use Ctrl + W then Ctrl + R inside the search to do search-and-replace
  • If nano feels too plain, you can enable syntax highlighting by editing ~/.nanorc — but don't worry about that yet

🎲 Module 2 Quest: The Cue Sheet

Sound engineers keep cue sheets for shows. Create one!

  1. Use nano to create ~/shell-quest/notes/cue-sheet.txt
  2. Add at least 5 "cues" for an imaginary show (e.g., "CUE 1: House lights dim - Scene 1 begins")
  3. Save and exit
  4. Use head -n 3 cue-sheet.txt to see just the first 3 cues
  5. Open the file again in nano and use Ctrl + W to search for one of your cues
  • Created cue-sheet.txt with nano
  • Added 5+ cues
  • Used head to preview
  • Used Ctrl+W to search inside nano
3

Rolling Your First Script

Write and run your first shell script — roll for initiative!
▼

▶ What's a Shell Script?

A shell script is just a text file full of commands that the shell runs in order. Instead of typing 10 commands one by one, you put them in a file and run it once. It's like programming a macro on a sound board — set it up once, trigger it anytime.

▶ The Shebang Line

Every script starts with a special first line that tells the system which program should run it:

#!/bin/bash

This is called the shebang (hash + bang). Even though you're using zsh day-to-day, writing scripts with #!/bin/bash makes them portable — they'll work on almost any Unix/Linux system.

Bash vs. Zsh for Scripts

Your interactive shell is zsh, but your scripts will use bash. This is totally normal. The commands are 95% the same. We'll note the rare differences when they come up. You could also use #!/bin/zsh if you specifically want zsh features.

▶ Your First Script

# Create the file
nano ~/shell-quest/scripts/hello.sh

Type this into nano:

#!/bin/bash

# My first shell script!
echo "Hello, Eli!"
echo "Today is $(date +%A), $(date +%B) $(date +%d)."
echo "You are logged in as: $(whoami)"
echo "Your current directory is: $(pwd)"

Save and exit. Now make it executable and run it:

# Make the script executable
chmod +x ~/shell-quest/scripts/hello.sh

# Run it!
~/shell-quest/scripts/hello.sh
Hello, Eli!
Today is Saturday, February 22.
You are logged in as: eli
Your current directory is: /Users/eli

▶ Breaking It Down

  • echo — prints text to the screen
  • $(command) — runs a command and inserts its output into the string. This is called command substitution
  • chmod +x — gives the file "execute" permission so you can run it as a program
  • # — lines starting with # are comments (ignored by the shell). The shebang is a special exception

▶ Variables

Variables store values you can reuse. Think of them as labeled faders on a sound board.

#!/bin/bash

# Setting variables (NO spaces around the = sign!)
name="Eli"
character_class="Wizard"
level=17

# Using variables (prefix with $)
echo "$name the $character_class, Level $level"
Eli the Wizard, Level 17

# Curly braces help when text is right next to the variable
echo "${name}'s spell slots are full"

⚠ Common Gotcha

name = "Eli" (with spaces) does NOT work. Bash/zsh will think name is a command. It must be name="Eli" with no spaces around =.

🎲 Module 3 Quest: Character Sheet Generator

Create a script called character-sheet.sh in your scripts folder:

  1. Define variables for a D&D character: name, race, class, level, hit points
  2. Use echo to print a formatted character sheet, like:
    ============================
      CHARACTER SHEET
    ============================
    Name:   Thaldrin
    Race:   Half-Elf
    Class:  Ranger
    Level:  5
    HP:     42
    ============================
  3. Include the current date and time at the bottom using command substitution
  4. Make it executable and run it
  • Script created with shebang line
  • Uses at least 5 variables
  • Output is nicely formatted
  • Includes command substitution
  • Made executable and runs cleanly
4

Taking Input & Making Choices

Make scripts interactive with user input and if/else decisions
▼

▶ Reading User Input

A script that only prints static text is like a sound board you can't adjust. Let's make it interactive.

#!/bin/bash

echo "What is your character's name?"
read char_name

echo "Welcome, $char_name! Your quest begins."

read pauses the script and waits for the user to type something, then stores it in the variable.

# read with a prompt on the same line
read -p "Enter your character's name: " char_name

# read a secret (password) without showing it
read -sp "Enter the secret passphrase: " passphrase
echo  # add a newline since -s hides the Enter

▶ If / Else: Making Decisions

Scripts can make decisions just like choosing which sound cue to fire based on what's happening on stage.

#!/bin/bash

read -p "Roll for initiative (enter a number 1-20): " roll

if [ $roll -ge 15 ]; then
    echo "Excellent roll! You act first."
elif [ $roll -ge 8 ]; then
    echo "Decent. You're somewhere in the middle."
else
    echo "Oof. The goblins go before you."
fi

▶ Comparison Operators

Inside [ ] (called "test brackets"), you use special operators for numbers:

Operator Meaning Example
-eqequal to[ $a -eq 5 ]
-nenot equal to[ $a -ne 5 ]
-gtgreater than[ $a -gt 5 ]
-ltless than[ $a -lt 5 ]
-gegreater/equal[ $a -ge 5 ]
-leless/equal[ $a -le 5 ]

For strings, use = and !=:

if [ "$answer" = "yes" ]; then
    echo "Onward!"
fi

Quoting Variables

Always put variables in double quotes inside test brackets: [ "$var" -gt 5 ]. Without quotes, if the variable is empty, the shell sees [ -gt 5 ] which is a syntax error.

▶ Special Variable: $RANDOM

Bash gives you a built-in random number generator — perfect for dice rolls!

# $RANDOM gives a number from 0-32767
# Use modulo (%) to get a range, then add 1

# Roll a d20
d20=$(( (RANDOM % 20) + 1 ))
echo "You rolled a $d20!"

# Roll a d6
d6=$(( (RANDOM % 6) + 1 ))
echo "Damage: $d6"

$(( )) is arithmetic expansion — it does math inside a script.

🎲 Module 4 Quest: The Dungeon Door

Create dungeon-door.sh — a mini text adventure:

  1. Ask the player for their character name
  2. Present them with a locked door and 3 choices: pick the lock, force it open, or cast a spell
  3. Use $RANDOM to generate a d20 roll for the attempt
  4. Use if/elif/else to determine success:
    • Lock picking needs a 12+ to succeed
    • Forcing needs a 16+ (it's a heavy door!)
    • Spell needs a 10+ (magic is reliable)
  5. Print different outcomes for success and failure
  • Uses read for player input
  • Has at least 3 choices
  • Uses $RANDOM for the dice roll
  • Uses if/elif/else for outcomes
  • Prints the roll result so the player can see it
5

Loops & Repetition

Automate repetitive tasks with for loops, while loops, and more
▼

▶ For Loops

A for loop repeats a block of commands for each item in a list. Like running through a cue list on the sound board — one cue at a time.

#!/bin/bash

# Loop through a list of words
for creature in goblin orc dragon beholder; do
    echo "A wild $creature appears!"
done

# Loop through a range of numbers
for i in {1..5}; do
    echo "Round $i of combat!"
done

# Loop through files in a directory
for file in ~/shell-quest/notes/*; do
    echo "Found file: $file"
done

▶ While Loops

A while loop keeps going as long as a condition is true.

#!/bin/bash

hp=20

while [ $hp -gt 0 ]; do
    damage=$(( (RANDOM % 6) + 1 ))
    hp=$(( hp - damage ))
    echo "Hit for $damage damage! HP remaining: $hp"
done

echo "The monster is defeated!"

▶ Loop Control: break and continue

# break - exit the loop entirely
while true; do
    read -p "Enter command (quit to exit): " cmd
    if [ "$cmd" = "quit" ]; then
        break
    fi
    echo "You entered: $cmd"
done

# continue - skip to the next loop iteration
for i in {1..10}; do
    if [ $(( i % 2 )) -eq 0 ]; then
        continue  # skip even numbers
    fi
    echo "Odd number: $i"
done

✍ Try It Yourself

  1. Write a for loop that counts down from 10 to 1 and then prints "Liftoff!" (Hint: {10..1})
  2. Write a while loop that asks the user for a password and keeps asking until they type "swordfish"

🎲 Module 5 Quest: Tennis Score Tracker

Create tennis-scorer.sh — a script that tracks points in a tennis game:

  1. Ask for two player names
  2. Use a while loop that runs until someone wins the game
  3. Each iteration, randomly award a point to player 1 or player 2
  4. Track the score using tennis scoring: 0, 15, 30, 40, Game
  5. Print the score after each point
  6. Announce the winner when someone reaches "Game"

Bonus challenge: Handle deuce (40-40) and advantage!

  • Uses a while loop
  • Tracks two players' scores
  • Uses correct tennis scoring terms
  • Announces the winner
  • Bonus: handles deuce/advantage
6

Functions & Arguments

Organize your code into reusable pieces and handle script arguments
▼

▶ Functions: Your Reusable Cues

On a sound board, you set up cue groups you can trigger whenever you need them. Functions work the same way in scripts — write a block of code once, call it by name.

#!/bin/bash

# Define a function
roll_d20() {
    local result=$(( (RANDOM % 20) + 1 ))
    echo $result
}

# Call it
my_roll=$(roll_d20)
echo "You rolled: $my_roll"

# Function with parameters
roll_dice() {
    local sides=$1   # first argument
    local count=$2   # second argument
    local total=0

    for i in $(seq 1 "$count"); do
        total=$(( total + (RANDOM % sides) + 1 ))
    done

    echo $total
}

# Roll 3d6 (three six-sided dice)
damage=$(roll_dice 6 3)
echo "Fireball damage: $damage"

▶ Local vs Global Variables

The local keyword keeps a variable inside the function so it doesn't leak out and accidentally overwrite things in the rest of your script. Always use local for variables that only matter inside a function.

▶ Script Arguments ($1, $2, $@, $#)

You can pass arguments to a script when you run it from the command line:

./greet.sh Eli Wizard

Inside the script:

#!/bin/bash

echo "Script name: $0"       # ./greet.sh
echo "First argument: $1"    # Eli
echo "Second argument: $2"   # Wizard
echo "All arguments: $@"     # Eli Wizard
echo "Number of args: $#"    # 2

▶ Exit Codes

Every command returns an exit code: 0 means success, anything else means failure. You can check the last exit code with $?:

ls /nonexistent-folder
echo "Exit code: $?"  # probably 1 (error)

ls ~
echo "Exit code: $?"  # 0 (success)

You can set your own exit code with exit:

if [ $# -eq 0 ]; then
    echo "Usage: $0 <name>"
    exit 1
fi

🎲 Module 6 Quest: The Dice Roller Toolkit

Create dice-roller.sh — a flexible dice rolling script:

  1. Accept a dice notation as a command-line argument (e.g., ./dice-roller.sh 2d8)
  2. Parse the notation to extract the number of dice and number of sides (hint: look up IFS or cut)
  3. Write a roll_dice function that takes sides and count
  4. Print each individual roll and the total
  5. If no argument is given, print usage instructions and exit with code 1

Example run:

./dice-roller.sh 3d6
Rolling 3d6...
  Roll 1: 4
  Roll 2: 6
  Roll 3: 2
  Total: 12
  • Accepts command-line arguments
  • Parses dice notation (NdS)
  • Uses a function for rolling
  • Shows individual rolls and total
  • Handles missing arguments gracefully
7

Pipes, Redirection & Text Processing

Chain commands together and control where output goes
▼

▶ Output Redirection

You can send command output to a file instead of the screen:

# Write output to a file (creates or overwrites)
echo "Session notes" > notes.txt

# Append to a file (doesn't overwrite)
echo "More notes" >> notes.txt

# Redirect errors to a file
ls /fake/path 2> errors.txt

# Redirect both output and errors
ls /fake/path > output.txt 2>&1

▶ Pipes: Connecting Commands

The pipe (|) sends the output of one command as input to another. It's like routing audio from one channel to another on the sound board.

# Count how many files are in a directory
ls | wc -l

# Search through output
cat cue-sheet.txt | grep "Scene 2"

# Sort lines alphabetically
cat names.txt | sort

# Remove duplicate lines
cat names.txt | sort | uniq

# Chain multiple pipes
cat server.log | grep "ERROR" | wc -l
# ^ reads the log, filters for ERROR lines, counts them

▶ Handy Text Commands

Command What It Does
grep "pattern" fileSearch for lines matching a pattern
wc -lCount lines
sortSort lines alphabetically
uniqRemove adjacent duplicates (sort first!)
cut -d',' -f1Extract a field from delimited text
tr 'a-z' 'A-Z'Translate/replace characters
sed 's/old/new/g'Find and replace in text

🎲 Module 7 Quest: Show Report Generator

Create a script called show-report.sh that processes a show log:

  1. First, create a sample log file show-log.txt with entries like:
    CUE,1,lights,SUCCESS
    CUE,2,sound,SUCCESS
    CUE,3,sound,MISSED
    CUE,4,lights,SUCCESS
    CUE,5,sound,SUCCESS
    CUE,6,projection,MISSED
  2. Write a script that:
    • Counts total cues
    • Counts successful vs missed cues
    • Lists all the missed cues
    • Shows a "success rate" percentage
  3. Use pipes, grep, wc, and cut to do it!
  • Created sample show-log.txt
  • Script counts total cues
  • Script reports missed cues
  • Uses pipes effectively
  • Calculates success rate
8

Arrays & Case Statements

Work with lists of data and build clean menus
▼

▶ Arrays

An array holds a list of values. Think of it as a playlist on the sound board.

#!/bin/bash

# Create an array
party=("Thaldrin" "Mira" "Bork" "Zephyr")

# Access elements (0-indexed)
echo "Leader: ${party[0]}"     # Thaldrin
echo "Healer: ${party[2]}"     # Bork

# All elements
echo "Full party: ${party[@]}"

# Number of elements
echo "Party size: ${#party[@]}"

# Add to array
party+=("NPC Guide")

# Loop through array
for member in "${party[@]}"; do
    echo "- $member"
done

▶ Case Statements

Case statements are a clean way to handle multiple choices — much nicer than a long chain of if/elif:

#!/bin/bash

read -p "Choose your class (warrior/mage/rogue): " class

case $class in
    warrior|fighter)
        echo "Strength is your ally. +5 HP"
        ;;
    mage|wizard)
        echo "Knowledge is power. +3 spell slots"
        ;;
    rogue|thief)
        echo "Stealth is key. +2 sneak attack"
        ;;
    *)
        echo "Unknown class: $class"
        ;;
esac

The | lets you match multiple patterns. The *) is the default/catch-all. Each option ends with ;;.

🎲 Module 8 Quest: Sound Board Simulator

This one's right up your alley! Create sound-board.sh:

  1. Define an array of sound cue names (e.g., "Thunder", "Applause", "Doorbell", "Crickets", "Dramatic Sting")
  2. Use a while loop to show a menu: list all sounds with numbers
  3. Use a case statement to handle the user's selection
  4. When a sound is "played", print something fun like:
    ♫ Playing: Thunder ♫
    [======    ] 60%
    [==========] 100% - Done!
  5. Add options to add new sounds to the array and to quit
  • Uses an array for the sound list
  • Shows a numbered menu
  • Uses case for selection handling
  • Can add new sounds dynamically
  • Loops until user quits
9

Real-World Scripting

File operations, process management, and practical automation
▼

▶ File Tests

Check if files and directories exist before working with them:

#!/bin/bash

if [ -f "config.txt" ]; then
    echo "Config file found!"
else
    echo "Config missing, creating default..."
    echo "volume=80" > config.txt
fi

if [ -d "backups" ]; then
    echo "Backup directory exists"
fi

if [ -x "myscript.sh" ]; then
    echo "Script is executable"
fi
Test Checks If...
-f fileFile exists (and is a regular file)
-d dirDirectory exists
-e pathAnything exists at that path
-r fileFile is readable
-w fileFile is writable
-x fileFile is executable
-s fileFile exists and is not empty

▶ Working with Dates & Timestamps

# Current date and time
date

# Formatted date
date +"%Y-%m-%d"          # 2026-02-22
date +"%H:%M:%S"          # 14:30:05

# Great for backup file names
backup_name="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
echo "$backup_name"
backup_20260222_143005.tar.gz

▶ Practical: A Backup Script

#!/bin/bash

source_dir="$HOME/shell-quest"
backup_dir="$HOME/backups"
timestamp=$(date +%Y%m%d_%H%M%S)
backup_file="shell-quest-backup_${timestamp}.tar.gz"

# Create backup directory if it doesn't exist
if [ ! -d "$backup_dir" ]; then
    mkdir -p "$backup_dir"
    echo "Created backup directory"
fi

# Create the backup
tar -czf "$backup_dir/$backup_file" -C "$HOME" "shell-quest"

if [ $? -eq 0 ]; then
    echo "Backup created: $backup_file"
    echo "Size: $(du -h "$backup_dir/$backup_file" | cut -f1)"
else
    echo "Backup failed!"
    exit 1
fi

🎲 Module 9 Quest: Project Organizer

Create organize.sh — a script that organizes files in a directory by type:

  1. Accept a directory path as an argument (default to current directory)
  2. Create subdirectories: images, documents, scripts, other
  3. Loop through files and move them into the right folder based on extension:
    • .jpg .png .gif → images
    • .txt .pdf .doc → documents
    • .sh .py .js → scripts
    • Everything else → other
  4. Print a summary of how many files were moved to each folder
  5. Use file tests to skip directories (only move regular files)

Test it on a folder you fill with dummy files first!

  • Accepts directory argument with default
  • Creates category directories
  • Sorts files by extension
  • Uses case statement for extensions
  • Prints a summary
  • Tested safely on dummy files
10

Boss Battle: The Grand Project

Combine everything you've learned into one epic script
▼

▶ Choose Your Final Quest

You've learned the fundamentals. Now it's time to build something real. Pick one of these projects (or pitch your own):

Option A: D&D Combat Simulator

Build a turn-based combat system in bash:

  • Player creates a character with stats (HP, attack, defense, name)
  • Randomly generated monsters with scaling difficulty
  • Turn-based combat with attack, defend, and use-item options
  • Dice rolls determine hit/miss and damage
  • Track and save high scores to a file
  • Multiple encounters that get harder

Option B: Show Cue Manager

Build a full show management tool:

  • Create, edit, and delete cue lists stored in files
  • Add cues with type (sound, lights, projection), timing, and notes
  • Search cues by type or keyword
  • "Run" a show that steps through cues with timing
  • Generate a printable show report
  • Back up cue lists with timestamps

Option C: Tennis Tournament Tracker

Build a tournament bracket system:

  • Input player names for the tournament
  • Automatically generate a bracket
  • Simulate or manually enter match results
  • Track win/loss records, sets, and games
  • Display bracket progress as ASCII art
  • Save tournament results to a log file

▶ Tips for the Grand Project

  • Plan before you code. Sketch out what your script needs to do on paper or in a notes file
  • Build incrementally. Get the simplest version working first, then add features
  • Test often. Run your script after every few lines of change
  • Use functions to keep your code organized — one function per feature
  • Comment your code so you remember what each section does when you come back to it
  • Chose a project
  • Wrote a plan / outline
  • Built a minimum viable version
  • Added at least 3 features beyond the basics
  • Script runs without errors
  • Code is commented and organized with functions