Shell Scripting Basics
Reviewed & published by Brayan K
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.
Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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 $?
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}."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 bashForgetting 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}!"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}!"
done4. 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"
fi5. 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🎯 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 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 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 local inside functions so a function's variables don't leak into the rest of the script — the same idea as let/const scope in JavaScript.
- 💡 $((...)) does maths: count=$((count + 1)). Plain $( ) runs a command; the double parentheses do arithmetic.
- 💡 Fail loudly: start scripts with set -euo pipefail once 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; write name="Alice" with no spaces.
- "syntax error: unexpected end of file" — you forgot to close a block. Every if needs fi, every for/while needs done, and every case branch needs ;;.
- "Permission denied" when running ./script.sh — the file isn't executable. Run chmod +x script.sh first, or just run it with bash 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 |
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 with chmod +x
- ✅ Variables use name=value (no spaces) and read back as $name / ${name}
- ✅ $(...) captures command output; read gets input; $1/$@/$# are arguments
- ✅ Decide with if … fi and [ ]/[[ ]]; repeat with for and while … done
- ✅ Package work in functions; check success with the exit code $?
Practice quiz
What is the shebang line #!/bin/bash for?
- It tells the system to run the file with bash
- It is a comment
- It exits the script
- It prints bash
Answer: It tells the system to run the file with bash. The shebang on line 1 tells the system which interpreter to use.
How do you correctly assign a variable in bash?
- name = value
- name=value
- set name value
- let name = value
Answer: name=value. Assignment must have no spaces around the = sign: name=value.
How do you read back the value of a variable named user?
- @user
- &user
- $user
- %user
Answer: $user. Put a $ in front of the name: $user or ${user}.
What does $(command) do?
- Comments out the command
- Skips the command
- Defines a function
- Runs the command and inserts its output
Answer: Runs the command and inserts its output. Command substitution runs the command and drops its output into the line.
What does $1 refer to inside a script?
- The first positional argument
- The script name
- The number of arguments
- The exit code
Answer: The first positional argument. $1 is the first argument; $0 is the script name.
Which keyword closes an if block in bash?
- end
- fi
- done
- endif
Answer: fi. An if block is closed with fi.
Which numeric test operator means greater-than-or-equal?
- -gt
- -eq
- -ge
- -le
Answer: -ge. -ge means >= ; -gt is strictly greater than.
Which keyword closes a for or while loop?
- fi
- stop
- loop
- done
Answer: done. for and while loops are closed with done.
What does $? hold after a command runs?
- The exit code of the last command
- The script name
- The first argument
- The current directory
Answer: The exit code of the last command. $? holds the exit code; 0 means success, non-zero means failure.
How do you do arithmetic like adding 1 to a counter in bash?
- count = count + 1
- $((count + 1))
- math(count+1)
- add count 1
Answer: $((count + 1)). $((...)) does arithmetic, e.g. count=$((count + 1)).
Continue this course
- Previous: Text Processing with grep, sed, awk
- Next: Process Management — View running processes, kill tasks, and manage background jobs with & and fg/bg
- Quick reference: Command Line cheat sheet · Bash cheat sheet › Variables
Frequently asked questions
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".
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'.
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.
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.
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.