Skip to main content

📜 Lesson 8.1: Your First Bash Script

Stop typing the same commands over and over — put them in a script and let the computer do the work.

🎯 Learning Objectives

  • Understand what a shell script is and when to use one
  • Write a script with the shebang line, comments, and echo
  • Make scripts executable and run them
  • Understand exit codes and use $?
  • Know the difference between ./script.sh, bash script.sh, and source script.sh

Estimated Time: 40 minutes

📑 In This Lesson

What Is a Shell Script?

A shell script is simply a text file containing a sequence of commands. Instead of typing each command one at a time in the terminal, you write them all in a file and run them together.

Shell scripts are perfect for:

  • Automation — backups, log rotation, deployments
  • Repetitive tasks — setting up new machines, batch file renaming
  • Complex pipelines — chaining commands with logic and error handling
  • Quick tools — system health checks, report generators
graph LR A["You write
commands in
a .sh file"] --> B["Make it
executable
(chmod +x)"] B --> C["Run it
(./script.sh)"] C --> D["Bash executes
each line
in order"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#6366f1,stroke:#4338ca,color:#fff style C fill:#f59e0b,stroke:#d97706,color:#fff style D fill:#22c55e,stroke:#166534,color:#fff

💡 When NOT to Use a Shell Script

Shell scripts are great for gluing commands together, but for complex logic, data structures, or web applications, a proper programming language (Python, JavaScript, etc.) is usually a better choice. A good rule of thumb: if your script exceeds 100 lines, consider whether a different language would be clearer.

Writing Your First Script

Let's create the classic "Hello, World!" script.

Step 1: Create the File

nano hello.sh

Step 2: Write the Script

#!/bin/bash
# My first Bash script!
# This prints a friendly greeting.

echo "Hello, World!"
echo "I am a Bash script running on $(hostname)"
echo "Today is $(date)"

Step 3: Make It Executable

chmod +x hello.sh

Step 4: Run It

./hello.sh

Expected Output

Hello, World!
I am a Bash script running on ray-desktop
Today is Mon Apr 14 10:30:00 PDT 2026

That's it! You just wrote and ran a shell script. Let's break down what each piece does.

The Shebang Line

The very first line of a script should be the shebang (also called hashbang):

#!/bin/bash

This tells the operating system which interpreter to use when executing the script. Without it, the system might try to use a different shell (like sh or dash), which could behave differently.

ShebangInterpreterWhen to Use
#!/bin/bashBashMost common — use this for Bash-specific features
#!/bin/shPOSIX shellMaximum portability across Unix systems
#!/usr/bin/env bashBash (found via PATH)Most portable Bash shebang — works even if Bash isn't at /bin/bash
#!/usr/bin/env python3Python 3For Python scripts

🐧 Why "Shebang"?

The name comes from the two characters: # (sharp/hash) + ! (bang). Hash-bang → shebang. Some people also call it "hashbang." The #! is read by the kernel, not the shell — it's how Linux knows what program should interpret the file.

Running Scripts

There are three ways to run a script, and they behave differently:

Method 1: ./script.sh (Execute)

# Requires the file to be executable (chmod +x)
# Uses the shebang line to pick the interpreter
# Runs in a NEW subprocess
./hello.sh

Method 2: bash script.sh (Explicit Interpreter)

# Does NOT require the file to be executable
# Ignores the shebang — uses whichever shell you specify
# Runs in a NEW subprocess
bash hello.sh

Method 3: source script.sh (Source in Current Shell)

# Runs in the CURRENT shell (not a subprocess)
# Variables and changes persist after the script ends
# Useful for loading config or environment variables
source hello.sh
# Shorthand:
. hello.sh

⚠️ Why ./?

You need ./ because the current directory isn't in your $PATH (for security reasons). ./hello.sh means "run hello.sh from the current directory." Without it, Bash looks for hello.sh in /usr/bin, /usr/local/bin, etc. and won't find it.

graph TD A["./script.sh or
bash script.sh"] --> B["New subprocess
(child shell)"] B --> C["Script runs
Variables stay in child"] B --> D["Child exits
Parent unchanged"] E["source script.sh"] --> F["Current shell
(no subprocess)"] F --> G["Script runs
Variables stay in current shell"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style E fill:#6366f1,stroke:#4338ca,color:#fff style B fill:#f59e0b,stroke:#d97706,color:#fff style F fill:#22c55e,stroke:#166534,color:#fff

Output with echo and printf

#!/bin/bash

# Basic output
echo "Hello, World!"

# Variables are expanded in double quotes
NAME="Ray"
echo "Hello, $NAME!"

# Single quotes are LITERAL — no expansion
echo 'Hello, $NAME!'   # Prints: Hello, $NAME!

# Escape characters with -e
echo -e "Line one\nLine two\tTabbed"

# Print without a trailing newline
echo -n "Loading..."

# printf for formatted output (more control)
printf "%-15s %5d\n" "Apples" 42
printf "%-15s %5d\n" "Bananas" 7
printf "%-15s %5d\n" "Cherries" 156

printf Output

Apples               42
Bananas                7
Cherries             156

💡 echo vs printf

echo is simpler and fine for most output. printf gives you C-style format strings with alignment, padding, and precision — useful when building formatted tables or reports in scripts.

Comments

Everything after # on a line is a comment — ignored by Bash. Use them generously.

#!/bin/bash

# ============================================
# backup.sh — Back up the projects directory
# Author: Ray De La Paz
# Date: 2026-04-14
# ============================================

# Define the source and destination
SOURCE="/home/ray/projects"
DEST="/mnt/backup"

# Create a timestamped archive
tar -czf "$DEST/projects-$(date +%Y%m%d).tar.gz" "$SOURCE"  # Uses gz compression

echo "Backup complete!"  # Let the user know

Good commenting practices:

  • Add a header block at the top with the script's purpose, author, and date
  • Comment the why, not the what# Compress using gzip isn't helpful when the command clearly uses -z, but # gzip chosen for speed over bzip2 is
  • Use blank lines and comment dividers to organize sections

💡 No Block Comments

Bash doesn't have multi-line block comments like /* ... */. Each comment line needs its own #. You'll sometimes see a workaround using heredocs (: << 'COMMENT'), but plain # on each line is standard practice.

Exit Codes

Every command (and script) returns an exit code — a number between 0 and 255 that indicates success or failure.

Exit CodeMeaning
0Success ✅
1General error
2Misuse of command / wrong arguments
126Command found but not executable
127Command not found
130Script killed by Ctrl+C
# Check the exit code of the last command
echo "Hello"
echo $?     # Prints: 0 (success)

ls /nonexistent
echo $?     # Prints: 2 (error)

# Use exit codes in your own scripts
#!/bin/bash
if [ -d "/home/ray/projects" ]; then
    echo "Projects directory exists"
    exit 0
else
    echo "ERROR: Projects directory not found!" >&2
    exit 1
fi

💡 >&2 — Writing to Standard Error

Error messages should go to stderr (standard error), not stdout. >&2 redirects output to stderr. This lets users separate normal output from errors: ./script.sh > output.log 2> errors.log

🐧 Quick Exit Code Check: && and ||

# && runs the next command ONLY if the first succeeds (exit 0)
mkdir /tmp/test && echo "Created!"

# || runs the next command ONLY if the first fails (exit non-zero)
mkdir /tmp/test || echo "Failed to create!"

# Common pattern: try something, handle failure
cd /some/dir || { echo "Can't cd!"; exit 1; }

A Useful Example

Here's a practical script that checks basic system health:

#!/bin/bash
# ============================================
# syscheck.sh — Quick system health report
# ============================================

echo "=============================="
echo "  System Health Report"
echo "  $(date)"
echo "=============================="
echo ""

echo "🖥️  Hostname: $(hostname)"
echo "🐧 OS: $(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2)"
echo "⏱️  Uptime:$(uptime -p)"
echo ""

echo "💾 Disk Usage (/):"
df -h / | tail -1 | awk '{printf "   Used: %s of %s (%s)\n", $3, $2, $5}'
echo ""

echo "🧠 Memory:"
free -h | awk '/Mem:/ {printf "   Used: %s of %s\n", $3, $2}'
echo ""

echo "⚙️  CPU Load (1m / 5m / 15m):"
echo "   $(cat /proc/loadavg | awk '{print $1, $2, $3}')"
echo ""

echo "🔥 Top 5 Processes by Memory:"
ps aux --sort=-%mem | head -6 | tail -5 | awk '{printf "   %-8s %5s%%  %s\n", $1, $4, $11}'
echo ""

echo "=============================="
echo "  Report complete!"
echo "=============================="
# Save it, make it executable, run it
chmod +x syscheck.sh
./syscheck.sh

💡 Take It Further

You could schedule this with cron to email yourself a daily health report, or save the output to a log file: ./syscheck.sh >> /var/log/syscheck.log

Exercises

🏋️ Exercise 1: Hello Script

  1. Create hello.sh with a proper shebang line
  2. Have it print your name, today's date, and your current directory
  3. Make it executable and run it
  4. Check the exit code with echo $?

🏋️ Exercise 2: Explore Exit Codes

  1. Run ls / and check $? (should be 0)
  2. Run ls /does_not_exist and check $? (should be 2)
  3. Run asdfghjkl (nonsense command) and check $? (should be 127)
  4. Write a script that exits with code 42, then verify: ./script.sh; echo $?

🏋️ Exercise 3: Source vs Execute

  1. Create a script called setvar.sh:
#!/bin/bash
MY_VAR="Hello from the script!"
echo "Variable set: $MY_VAR"
  1. Run it with bash setvar.sh, then echo $MY_VAR — the variable is gone
  2. Run it with source setvar.sh, then echo $MY_VAR — the variable persists!

🏋️ Exercise 4: Your Own syscheck

Copy the system health script from the example above. Customize it:

  1. Add your own ASCII art header
  2. Add a section that counts running services: systemctl list-units --type=service --state=running --no-pager | grep -c ".service"
  3. Add a section showing logged-in users: who
  4. Save the output to a file: ./syscheck.sh > ~/syscheck-report.txt

Knowledge Check

❓ Question 1

What does the shebang line (#!/bin/bash) do?

❓ Question 2

What exit code means "success"?

❓ Question 3

Why do you need ./ before a script name to run it?

❓ Question 4

What's the key difference when using source script.sh?

Summary

🎉 Key Takeaways

  • A shell script is a text file of commands — saved, reusable, and shareable
  • Start every script with #!/bin/bash (the shebang line)
  • Make scripts executable with chmod +x script.sh
  • ./script.sh runs in a subprocess; source script.sh runs in the current shell
  • echo for simple output; printf for formatted output
  • Comment your code with # — explain the why, not the what
  • Exit code 0 = success; anything else = failure. Check with $?
  • Use && (run if success) and || (run if failure) for quick error handling

🍎 On macOS

Shell scripting works on macOS, with one important note: macOS's default shell is Zsh, not Bash. However, Bash is still installed at /bin/bash. As long as your scripts start with #!/bin/bash, they will run with Bash regardless of your default shell. All the scripting content in Modules 8 works on macOS. If you want to write Zsh-specific scripts, use #!/bin/zsh instead.

🚀 What's Next?

Your scripts can do more than just run commands in order. The next lesson covers Variables, Input, and Conditionals — making your scripts smart and interactive.