📜 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, andsource 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
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.
| Shebang | Interpreter | When to Use |
|---|---|---|
#!/bin/bash | Bash | Most common — use this for Bash-specific features |
#!/bin/sh | POSIX shell | Maximum portability across Unix systems |
#!/usr/bin/env bash | Bash (found via PATH) | Most portable Bash shebang — works even if Bash isn't at /bin/bash |
#!/usr/bin/env python3 | Python 3 | For 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.
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.
Exit Codes
Every command (and script) returns an exit code — a number between 0 and 255 that indicates success or failure.
| Exit Code | Meaning |
|---|---|
0 | Success ✅ |
1 | General error |
2 | Misuse of command / wrong arguments |
126 | Command found but not executable |
127 | Command not found |
130 | Script 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
- Create
hello.shwith a proper shebang line - Have it print your name, today's date, and your current directory
- Make it executable and run it
- Check the exit code with
echo $?
🏋️ Exercise 2: Explore Exit Codes
- Run
ls /and check$?(should be 0) - Run
ls /does_not_existand check$?(should be 2) - Run
asdfghjkl(nonsense command) and check$?(should be 127) - Write a script that exits with code 42, then verify:
./script.sh; echo $?
🏋️ Exercise 3: Source vs Execute
- Create a script called
setvar.sh:
#!/bin/bash
MY_VAR="Hello from the script!"
echo "Variable set: $MY_VAR"
- Run it with
bash setvar.sh, thenecho $MY_VAR— the variable is gone - Run it with
source setvar.sh, thenecho $MY_VAR— the variable persists!
🏋️ Exercise 4: Your Own syscheck
Copy the system health script from the example above. Customize it:
- Add your own ASCII art header
- Add a section that counts running services:
systemctl list-units --type=service --state=running --no-pager | grep -c ".service" - Add a section showing logged-in users:
who - 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.shruns in a subprocess;source script.shruns in the current shellechofor simple output;printffor 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.
Comments
Everything after
#on a line is a comment — ignored by Bash. Use them generously.Good commenting practices:
# Compress using gzipisn't helpful when the command clearly uses-z, but# gzip chosen for speed over bzip2is💡 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.