Lesson 7 • Intermediate
Shell Scripting Basics
By the end of this lesson you'll be able to turn a list of terminal commands into a reusable bash script — with variables, input, decisions, loops, and functions — so the computer does the repetitive work for you.
What You'll Learn
- Write the shebang #!/bin/bash and make a script executable with chmod +x
- Store and reuse values with variables and $VAR / ${VAR}
- Capture command output with command substitution $(...)
- Read user input and accept positional arguments $1, $@, $#
- Make decisions with if / elif / else and test / [ ] / [[ ]]
- Repeat work with for and while loops, package it in functions, and check exit codes with $?
.sh file and run bash script.sh. The Output panel shows exactly what you should see.if statements are the "if the sauce is too thin, add flour" notes, and loops are "repeat for each plate."1. The Shebang & Variables
A script is just a text file full of commands. The first line is the shebang — #!/bin/bash — which tells the system to run the file with the bash interpreter. After that, you store values in variables: write name=value with no spaces around the =, and read it back with $name. Use ${name} braces when the variable name sits right next to other text.
#!/bin/bash
# The shebang (line 1) tells the system: run this file with /bin/bash.
# It MUST be the very first line, starting with #! (no space before #).
# A variable = a named label for a value. Assign with name=value.
# CRITICAL: no spaces around the = sign.
name="Ada"
language="bash"
year=2026
# Read a variable back by putting $ in front of its name.
echo "Hello, $name!"
# Use ${VAR} braces when the name touches other text, so bash knows
# where the variable name ends.
echo "You are learning ${language} scripting."
echo "It is the year ${year}."Hello, Ada!
You are learning bash scripting.
It is the year 2026.Running a script
There are two ways to run a saved script. Either make it executable once with chmod +x and run it directly, or hand the file to bash each time:
$ chmod +x script.sh # make it executable (only needed once) $ ./script.sh # run it directly # OR, with no chmod needed: $ bash script.sh # run it by handing the file to bash
Forgetting chmod +x is the classic "Permission denied" trip-up — see Common Errors below.
2. Command Substitution & Input
Command substitution — $(command) — runs a command and drops its output straight into your line. So today=$(date +%A) stores today's weekday in a variable. To get input from the person running the script, use read: it pauses, waits for them to type, and stores the result in a variable you name.
#!/bin/bash
# Command substitution: $(command) runs the command and drops its
# OUTPUT right into your line. The script below uses fixed values so
# the output is predictable, but the comments show the real commands.
today="Monday" # real version: today=$(date +%A)
file_count=3 # real version: file_count=$(ls | wc -l)
echo "Today is ${today}."
echo "There are ${file_count} files here."
# 'read' pauses and stores what the user types into a variable.
# In an interactive shell this would prompt you; here we show the idea:
# read -p "What is your name? " user_name
# echo "Welcome, ${user_name}!"
user_name="Grace" # pretend the user typed this
echo "Welcome, ${user_name}!"Today is Monday.
There are 3 files here.
Welcome, Grace!3. Positional Arguments
Scripts become powerful when you pass them arguments — the extra words you type after the script name. Bash hands them to you as $1 (first), $2 (second), and so on. $# is the count of arguments, $@ is all of them as a list, and $0 is the script's own name. This is how one script can act on whatever you give it.
#!/bin/bash
# Positional arguments are the words you type AFTER the script name.
# Save this as greet.sh and run: bash greet.sh Alice Bob Carol
#
# $0 -> the script's own name (greet.sh)
# $1 -> the first argument (Alice)
# $2 -> the second argument (Bob)
# $# -> how many arguments there are (3)
# $@ -> ALL the arguments, as a list
echo "Script name: $0"
echo "First arg: $1"
echo "Arg count: $#"
# Loop over every argument with $@:
for person in "$@"; do
echo "Hello, ${person}!"
doneScript name: greet.sh
First arg: Alice
Arg count: 3
Hello, Alice!
Hello, Bob!
Hello, Carol!greet.sh and run bash greet.sh Alice Bob Carol in your own terminal — the arguments fill $1, $@ and $#.4. Conditionals: if, test, [ ] and [[ ]]
Conditionals run code only when a test passes. The shape is if [ test ]; then ... fi — and you must close it with fi. Numbers use -gt -ge -lt -le -eq -ne; strings use = and !=; files use checks like -f (exists). [ ] is the classic test command; [[ ]] is the modern bash version that's safer with strings. Watch the spaces — you need one inside the brackets.
#!/bin/bash
# Conditionals run code only when a test is true.
# [ ] is the classic test command; [[ ]] is the modern bash version
# (safer with strings and supports && and ||). Mind the spaces:
# you need a space INSIDE the brackets: [ "$x" -gt 0 ]
score=85
# Numeric comparisons: -gt -ge -lt -le -eq -ne
if [ "$score" -ge 90 ]; then
echo "Grade: A"
elif [ "$score" -ge 80 ]; then
echo "Grade: B" # 85 is >= 80, so this branch runs
else
echo "Grade: C"
fi
# String comparison uses = (equal) and != (not equal).
name="admin"
if [[ "$name" == "admin" ]]; then
echo "Access granted"
fi
# File test: -f is true if the file exists and is a regular file.
if [ -f "/etc/hostname" ]; then
echo "Host file found"
fiGrade: B
Access granted
Host file found5. Loops, Functions & Exit Codes
A for loop walks through a list; a while loop repeats as long as its test stays true. A function wraps commands under a name so you can reuse them — inside it, $1 is the first argument and local keeps a variable private. Finally, every command sets $?, its exit code: 0 means success and anything else means a failure.
#!/bin/bash
# Loops repeat work; functions package work so you can reuse it.
# for over an explicit list
echo "--- for loop ---"
for i in 1 2 3; do
echo "Step $i"
done
# while repeats WHILE its test stays true
echo "--- while loop ---"
count=3
while [ "$count" -gt 0 ]; do
echo "${count}..."
count=$((count - 1)) # $(( )) does the arithmetic
done
echo "Liftoff!"
# A function. $1 is its first argument; 'local' keeps the var private.
greet() {
local who=$1
echo "Hi, ${who}!"
}
echo "--- function ---"
greet "Linus"
# Every command sets $? — its exit code. 0 means success, non-zero a failure.
ls /etc/hostname > /dev/null # this succeeds
echo "Exit code: $?" # 0--- for loop ---
Step 1
Step 2
Step 3
--- while loop ---
3...
2...
1...
Liftoff!
--- function ---
Hi, Linus!
Exit code: 0🎯 Your Turn: complete the for-loop
The script is almost done — fill in the two blanks marked ___ using the # 👉 hints, then run it and check the Output panel matches.
#!/bin/bash
# 🎯 YOUR TURN — finish the for-loop, then run it.
fruits="apple banana cherry"
# 1) Loop over each fruit in the list.
for fruit in ___; do # 👉 replace ___ with $fruits
# 2) Print one line per fruit using the loop variable.
echo "I like ___" # 👉 replace ___ with ${fruit}
done
# ✅ Expected output:
# I like apple
# I like banana
# I like cherryI like apple
I like banana
I like cherry🎯 Your Turn: complete the if-test
Two blanks again: pick the right comparison operator, and the keyword that closes an if block. Then run it.
#!/bin/bash
# 🎯 YOUR TURN — fill in the if-test, then run it.
age=20
# 1) Test whether age is 18 OR MORE. (hint: -ge means ">=")
if [ "$age" ___ 18 ]; then # 👉 replace ___ with -ge
echo "You can vote"
# 2) Close the if block with the right keyword.
___ # 👉 replace ___ with fi
# ✅ Expected output:
# You can voteYou can votePro Tips
- 💡 Quote your variables: write
"$file", not$file. If the value has spaces, quoting keeps it as one piece instead of splitting it. - 💡 Use
localinside functions so a function's variables don't leak into the rest of the script — the same idea aslet/constscope in JavaScript. - 💡
$((...))does maths:count=$((count + 1)). Plain$( )runs a command; the double parentheses do arithmetic. - 💡 Fail loudly: start scripts with
set -euo pipefailonce you're comfortable — it stops the script on the first error instead of charging on.
Common Errors (and the fix)
- "command not found" after an assignment — you put spaces around
=.name = "Alice"is wrong; writename="Alice"with no spaces. - "syntax error: unexpected end of file" — you forgot to close a block. Every
ifneedsfi, everyfor/whileneedsdone, and everycasebranch needs;;. - "Permission denied" when running
./script.sh— the file isn't executable. Runchmod +x script.shfirst, or just run it withbash script.sh. - "too many arguments" / "unary operator expected" in a test — an unquoted variable that's empty or has spaces broke the
[ ]test. Always quote it:[ "$x" -gt 0 ]. - "[: missing `]'" — you left out the space inside the brackets. It must be
[ "$x" = "y" ], with spaces just inside both brackets.
📋 Quick Reference
| Concept | Syntax | Notes |
|---|---|---|
| Shebang | #!/bin/bash | Must be line 1 |
| Variable | name="value" | No spaces around = |
| Read it | $name / ${name} | Braces near other text |
| Substitution | x=$(date) | Captures command output |
| Input | read -p "Q? " v | Stores typed text in v |
| Arguments | $1 $@ $# | First, all, count |
| If | if [ cond ]; then … fi | Close with fi |
| For loop | for x in list; do … done | Close with done |
| While loop | while [ cond ]; do … done | Repeats while true |
| Function | f() { … } | $1 = first arg |
| Arithmetic | n=$((n + 1)) | Double parentheses |
| Exit code | $? | 0 = success |
Frequently Asked Questions
Q: Why does name = "Alice" fail but name="Alice" work?
Bash treats a space as the separator between a command and its arguments. With spaces, bash thinks 'name' is a command and tries to run it with the arguments '=' and 'Alice'. Variable assignment must have NO spaces around the equals sign: name="Alice".
Q: What is the difference between $VAR and ${VAR}?
They do the same thing — read the variable's value. You need the braces ${VAR} only when the name touches other characters, so bash knows where the name ends. For example ${file}_backup works, but $file_backup looks for a variable called 'file_backup'.
Q: When should I use [ ] versus [[ ]]?
[ ] (the test command) is POSIX and works in any shell. [[ ]] is a bash/zsh feature that is safer with strings, lets you use && and || inside, and supports pattern matching. In bash scripts, prefer [[ ]] for string and file tests; use [ ] when you need maximum portability.
Q: What does $? mean, and what is an exit code?
$? holds the exit code of the command that just ran. By convention 0 means success and any non-zero value means a failure (1 is a generic error). Scripts use exit codes so other programs — and the shell itself — can tell whether a command worked.
Q: Why do people say to always quote variables, like "$file"?
If a variable's value contains spaces (a filename like 'my notes.txt'), an unquoted $file splits into separate words and breaks your command. Wrapping it in double quotes — "$file" — keeps the whole value as one piece. Quoting variables prevents a whole class of subtle bugs.
Mini-Challenge: file-counter report
No blanks this time — just a brief and an outline. Write a small, genuinely useful script that takes a folder name as an argument, guards against being called with none, and reports how many items it holds. Check your output against the example in the comments.
#!/bin/bash
# 🎯 MINI-CHALLENGE: a tiny file-counter report
# Save as report.sh and run: bash report.sh Documents
#
# 1. Read the folder name from the first argument ($1) into a variable.
# 2. If no argument was given ($# is 0), print "Usage: report.sh <folder>"
# and stop the script with exit 1 (a non-zero exit code = error).
# 3. Otherwise, use a for-loop over the items and a counter variable to
# count them, then print: "<folder> contains N item(s)".
#
# Hints: count=$((count + 1)) | for item in "$folder"/*; do ... done
#
# ✅ Example (folder has 4 items): Documents contains 4 item(s)
# your code here🎉 Lesson Complete!
- ✅ A script starts with the shebang
#!/bin/bash; make it runnable withchmod +x - ✅ Variables use
name=value(no spaces) and read back as$name/${name} - ✅
$(...)captures command output;readgets input;$1/$@/$#are arguments - ✅ Decide with
if … fiand[ ]/[[ ]]; repeat withforandwhile … done - ✅ Package work in functions; check success with the exit code
$? - ✅ Next lesson: Process Management — list, background, and stop running programs with
ps,kill, and jobs
Sign up for free to track which lessons you've completed and get learning reminders.