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