Bash Scripting Beginner Study Guide
A beginner-friendly study guide for learning Bash scripting.
This page explains Bash scripting slowly and clearly, with real-life examples.
Think of Bash scripting like writing a small recipe for the computer.
Instead of clicking things manually, you write steps and the computer follows them.
What is Bash?
Bash is a shell.
A shell is a program where you type commands.
Example:
ls
Bash reads the command and tells Linux what to do.
What is a script?
A script is a file that contains commands.
Instead of typing commands one by one, you put them in a file and run the file.
Real-life example:
Imagine you clean your room every day.
You always do:
Pick up clothes.
Put books on shelf.
Throw trash away.
Make bed.
A Bash script is like writing those steps down once, then telling the computer:
Do these steps for me.
Why use Bash scripts?
Bash scripts are useful for tasks you repeat.
Examples:
backup files
check disk space
restart a service
clean old logs
create folders
check if a website is reachable
show system information
run daily maintenance
If you do something more than once, it might become a script.
First Bash script
Create a file:
nano hello.sh
Put this inside:
#!/bin/bash
echo "Hello, world!"
Save the file.
Make it executable:
chmod +x hello.sh
Run it:
./hello.sh
Output:
Hello, world!
What does #!/bin/bash mean?
This line:
#!/bin/bash
is called a shebang.
It tells Linux:
Run this file using Bash.
Think of it like writing at the top of a recipe:
Use the oven, not the microwave.
The script needs to know which program should read it.
What does echo do?
echo prints text to the screen.
Example:
echo "Hello"
Output:
Hello
Real-life idea:
echo = computer speaking
If you write:
echo "Backup started"
the script tells you:
Backup started
This is useful so you know what the script is doing.
Running a script
There are two common ways.
Run with bash
bash hello.sh
This runs the script using Bash.
Run directly
./hello.sh
For this to work, the file must be executable:
chmod +x hello.sh
What does chmod +x mean?
This command:
chmod +x hello.sh
means:
Allow this file to be executed like a program.
Without it, Linux may say:
Permission denied
Comments
Comments are notes for humans.
Bash ignores comments.
Example:
#!/bin/bash
# This prints a greeting
echo "Hello"
The line starting with # is a comment.
Use comments to explain what your script does.
Good comments:
# Create backup folder if it does not exist
mkdir -p /tmp/backup
Bad comments:
# command
mkdir -p /tmp/backup
The comment should explain why, not just repeat the command.
Variables
A variable is like a labeled box.
You put something inside it and use it later.
Example:
NAME="Randy"
echo "Hello, $NAME"
Output:
Hello, Randy
Real-life example:
Box label: NAME
Box value: Randy
When Bash sees $NAME, it opens the box and uses the value inside.
Variable rules
No spaces around =.
Correct:
NAME="Randy"
Wrong:
NAME = "Randy"
Bash does not like spaces there.
Use quotes around variables
Good:
FILE="my file.txt"
echo "$FILE"
Bad:
echo $FILE
Why?
Because if the value has spaces, Bash can split it into separate pieces.
Example:
FILE="my file.txt"
rm $FILE
Bash may read this like:
rm my file.txt
That can behave differently than expected.
Better:
rm "$FILE"
Rule:
Use quotes around variables unless you have a clear reason not to.
Script example with variables
#!/bin/bash
NAME="server01"
DATE=$(date +"%Y-%m-%d")
echo "Checking $NAME"
echo "Today is $DATE"
Output:
Checking server01
Today is 2026-06-12
Command substitution
This:
DATE=$(date)
means:
Run the command inside $()
Save the output into DATE
Example:
NOW=$(date)
echo "Current time: $NOW"
Real-life idea:
Ask the clock what time it is.
Put the answer in a box called NOW.
Use it later.
Exit codes
Every command gives an exit code when it finishes.
Usually:
0 = success
not 0 = problem
Example:
ls /tmp
echo $?
$? shows the exit code of the last command.
If the command worked:
0
If it failed:
ls /folder/that/does/not/exist
echo $?
You may see:
2
Why exit codes matter
Scripts use exit codes to decide what to do next.
Example:
#!/bin/bash
ping -c 1 example.com
if [ $? -eq 0 ]; then
echo "Network works"
else
echo "Network problem"
fi
This means:
Try ping.
If it worked, say network works.
If it failed, say network problem.
If statements
An if statement lets a script make decisions.
Real-life example:
If it is raining:
take umbrella
Else:
wear sunglasses
Bash example:
#!/bin/bash
if [ -f "/etc/hosts" ]; then
echo "File exists"
else
echo "File does not exist"
fi
Common file tests
-f FILE = file exists and is a regular file
-d DIR = directory exists
-e PATH = file or directory exists
-r FILE = readable
-w FILE = writable
-x FILE = executable
Examples:
if [ -f "/etc/hosts" ]; then
echo "hosts file exists"
fi
if [ -d "/var/log" ]; then
echo "log directory exists"
fi
Test command safely
This:
[ -f "/etc/hosts" ]
is a test.
It asks:
Is /etc/hosts a file?
Important: spaces matter.
Correct:
if [ -f "/etc/hosts" ]; then
Wrong:
if [-f "/etc/hosts"]; then
Bash needs spaces inside [ ].
Comparing text
Example:
NAME="Randy"
if [ "$NAME" = "Randy" ]; then
echo "Hello Randy"
else
echo "You are not Randy"
fi
Use = for string comparison.
Comparing numbers
Common number comparisons:
-eq = equal
-ne = not equal
-gt = greater than
-lt = less than
-ge = greater than or equal
-le = less than or equal
Example:
AGE=40
if [ "$AGE" -ge 18 ]; then
echo "Adult"
else
echo "Not adult"
fi
Loops
A loop repeats something.
Real-life example:
For every plate on the table:
wash the plate
Bash example:
#!/bin/bash
for NAME in Alice Bob Charlie; do
echo "Hello, $NAME"
done
Output:
Hello, Alice
Hello, Bob
Hello, Charlie
Loop over files
#!/bin/bash
for FILE in *.log; do
echo "Found log file: $FILE"
done
This loops over files ending in .log.
Better safe version:
#!/bin/bash
for FILE in ./*.log; do
[ -e "$FILE" ] || continue
echo "Found log file: $FILE"
done
Why?
If there are no .log files, Bash can behave unexpectedly. The check avoids that.
While loops
A while loop repeats while something is true.
Example:
COUNT=1
while [ "$COUNT" -le 5 ]; do
echo "Count is $COUNT"
COUNT=$((COUNT + 1))
done
Output:
Count is 1
Count is 2
Count is 3
Count is 4
Count is 5
Real-life idea:
While there are dishes in the sink:
wash one dish
Arithmetic
Use $(( )) for math.
Example:
A=5
B=3
RESULT=$((A + B))
echo "$RESULT"
Output:
8
More examples:
COUNT=$((COUNT + 1))
TOTAL=$((PRICE * AMOUNT))
LEFT=$((TOTAL - USED))
Reading user input
Use read to ask the user for information.
Example:
#!/bin/bash
echo "What is your name?"
read NAME
echo "Hello, $NAME"
Better:
#!/bin/bash
read -p "What is your name? " NAME
echo "Hello, $NAME"
Hidden password input
Use -s for silent input.
read -s -p "Password: " PASSWORD
echo
echo "Password was entered"
Do not print real passwords.
Do not store passwords in scripts.
Arguments
Arguments are values passed to a script when you run it.
Example script:
#!/bin/bash
echo "First argument: $1"
echo "Second argument: $2"
Run:
./script.sh apple banana
Output:
First argument: apple
Second argument: banana
Useful argument variables
$0 = script name
$1 = first argument
$2 = second argument
$# = number of arguments
$@ = all arguments
Example:
#!/bin/bash
echo "Script name: $0"
echo "Number of arguments: $#"
echo "All arguments: $@"
Check if argument is missing
#!/bin/bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 FILE"
exit 1
fi
FILE="$1"
echo "You gave file: $FILE"
This means:
If user did not give at least one argument:
show how to use the script
stop with error
Exit
Use exit to stop a script.
Example:
exit 0
means success.
Example:
exit 1
means failure.
Use exit 1 when something went wrong.
Functions
A function is a reusable block of code.
Real-life example:
Instead of writing the recipe for making tea every time,
write "make_tea" once and reuse it.
Bash example:
#!/bin/bash
say_hello() {
echo "Hello"
}
say_hello
say_hello
say_hello
Output:
Hello
Hello
Hello
Function with argument
#!/bin/bash
greet() {
NAME="$1"
echo "Hello, $NAME"
}
greet "Alice"
greet "Bob"
Output:
Hello, Alice
Hello, Bob
Good script header
A good script often starts like this:
#!/bin/bash
set -euo pipefail
This makes the script safer.
Meaning:
set -e = stop if a command fails
set -u = stop if using an undefined variable
set -o pipefail = fail if part of a pipe fails
Example with safe header
#!/bin/bash
set -euo pipefail
echo "Starting script"
mkdir -p /tmp/example
echo "Done"
Warning about set -e
set -e is useful, but it can surprise beginners.
It stops the script when a command fails.
Example:
#!/bin/bash
set -e
grep "hello" missing-file.txt
echo "This line may never run"
If grep fails, the script stops.
That can be good or bad depending on what you want.
Pipes
A pipe sends output from one command into another.
Example:
ps aux | grep ssh
Meaning:
ps aux = show processes
| = send output to next command
grep ssh = show only lines containing ssh
Another example:
cat file.txt | grep error
Better:
grep error file.txt
Use simple commands when possible.
Redirection
Redirection sends output to a file.
Overwrite file:
echo "hello" > file.txt
Append to file:
echo "another line" >> file.txt
Redirect errors too:
command > output.log 2>&1
Meaning:
standard output goes to output.log
standard error also goes to output.log
Logging in scripts
A script should explain what it is doing.
Example:
#!/bin/bash
echo "Starting backup"
echo "Creating folder"
mkdir -p /tmp/backup
echo "Backup finished"
Better with date:
#!/bin/bash
DATE=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$DATE] Starting backup"
Log to a file
#!/bin/bash
LOG_FILE="/tmp/script.log"
echo "Script started" >> "$LOG_FILE"
date >> "$LOG_FILE"
Log everything
This sends all output and errors to a log file:
#!/bin/bash
LOG_FILE="/tmp/script.log"
exec >> "$LOG_FILE" 2>&1
echo "Script started"
date
After exec, all normal output and error output go to the log file.
Working with files
Check if file exists
FILE="/tmp/example.txt"
if [ -f "$FILE" ]; then
echo "File exists"
else
echo "File missing"
fi
Create file
touch /tmp/example.txt
Create directory
mkdir -p /tmp/example
Copy file
cp source.txt destination.txt
With variables:
SOURCE="source.txt"
DESTINATION="destination.txt"
cp "$SOURCE" "$DESTINATION"
Remove file safely
FILE="/tmp/example.txt"
if [ -f "$FILE" ]; then
rm "$FILE"
echo "Removed $FILE"
else
echo "File does not exist: $FILE"
fi
Real-life example: check disk space
Goal:
Check if disk usage is high.
If high, print warning.
Script:
#!/bin/bash
set -euo pipefail
LIMIT=80
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
echo "Disk usage is ${USAGE}%"
if [ "$USAGE" -ge "$LIMIT" ]; then
echo "WARNING: disk usage is high"
else
echo "Disk usage is OK"
fi
Explanation:
LIMIT=80
warning level
df /
check disk usage for /
awk 'NR==2 {print $5}'
take the usage column from the second line
tr -d '%'
remove the percent sign
if usage is greater or equal to limit
print warning
Real-life example: check if service is running
Goal:
Check if a service is active.
Script:
#!/bin/bash
set -euo pipefail
SERVICE="sshd"
if systemctl is-active --quiet "$SERVICE"; then
echo "$SERVICE is running"
else
echo "$SERVICE is not running"
fi
Run:
./check-service.sh
Output:
sshd is running
or:
sshd is not running
Version with argument
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "Usage: $0 SERVICE_NAME"
exit 1
fi
SERVICE="$1"
if systemctl is-active --quiet "$SERVICE"; then
echo "$SERVICE is running"
else
echo "$SERVICE is not running"
fi
Run:
./check-service.sh sshd
Real-life example: check website
Goal:
Check if a website responds.
Script:
#!/bin/bash
set -euo pipefail
URL="https://example.com"
if curl -Is "$URL" >/dev/null; then
echo "$URL is reachable"
else
echo "$URL is not reachable"
fi
Explanation:
curl -I = fetch only headers
-s = silent
>/dev/null = hide normal output
if command succeeds = reachable
Real-life example: create backup
Goal:
Create a compressed backup of a folder.
Script:
#!/bin/bash
set -euo pipefail
SOURCE_DIR="/tmp/example-data"
BACKUP_DIR="/tmp/backups"
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/example-backup-$DATE.tar.gz"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_FILE" -C "$SOURCE_DIR" .
echo "Backup created:"
echo "$BACKUP_FILE"
Explanation:
SOURCE_DIR = folder to backup
BACKUP_DIR = where backup goes
DATE = timestamp
tar -czf = create compressed tar archive
-C "$SOURCE_DIR" . = backup contents of source folder
Real-life example: delete old files safely
Goal:
List old files first.
Only delete after review.
List files older than 5 days:
find /opt -type f -mtime +5 -ls
This only lists files.
Safer script:
#!/bin/bash
set -euo pipefail
TARGET_DIR="/opt"
DAYS=5
LIST_FILE="/tmp/files-older-than-${DAYS}-days.txt"
find "$TARGET_DIR" -type f -mtime +"$DAYS" -ls > "$LIST_FILE"
echo "Review this file before deleting:"
echo "$LIST_FILE"
echo
echo "Number of matching files:"
wc -l "$LIST_FILE"
This script does not delete anything.
It only creates a review list.
Delete only after review
After checking the list, a deletion command may look like:
find /opt/app/cache -type f -mtime +5 -delete
Important:
Do not delete from /opt blindly.
Pick the specific directory that is safe to housekeep.
Always test first without -delete.
Real-life example: menu script
A menu lets the user choose.
Example:
#!/bin/bash
while true; do
echo "Choose an option:"
echo "1) Show disk usage"
echo "2) Show memory usage"
echo "3) Show uptime"
echo "4) Exit"
read -p "Choice: " CHOICE
case "$CHOICE" in
1)
df -h
;;
2)
free -h
;;
3)
uptime
;;
4)
echo "Goodbye"
exit 0
;;
*)
echo "Invalid choice"
;;
esac
echo
done
This is like a restaurant menu:
Choose 1 for pizza.
Choose 2 for pasta.
Choose 3 for salad.
The script waits for your choice and runs the matching command.
Case statements
A case statement is good when there are many choices.
Example:
case "$ACTION" in
start)
echo "Starting"
;;
stop)
echo "Stopping"
;;
restart)
echo "Restarting"
;;
*)
echo "Unknown action"
;;
esac
Use case for menus or actions.
Arrays
An array is a list.
Example:
FRUITS=("apple" "banana" "orange")
echo "${FRUITS[0]}"
echo "${FRUITS[1]}"
echo "${FRUITS[2]}"
Output:
apple
banana
orange
Loop over array:
FRUITS=("apple" "banana" "orange")
for FRUIT in "${FRUITS[@]}"; do
echo "Fruit: $FRUIT"
done
Reading a file line by line
Example file:
server01
server02
server03
Script:
#!/bin/bash
while read -r SERVER; do
echo "Checking $SERVER"
done < servers.txt
Better version that skips empty lines:
#!/bin/bash
while read -r SERVER; do
[ -z "$SERVER" ] && continue
echo "Checking $SERVER"
done < servers.txt
Dry run mode
A dry run shows what would happen without actually doing it.
Example:
#!/bin/bash
DRY_RUN=true
FILE="/tmp/example.txt"
if [ "$DRY_RUN" = true ]; then
echo "Would remove $FILE"
else
rm "$FILE"
fi
This is useful for dangerous scripts.
Real-life idea:
Before throwing things away, make a list of what you would throw away.
Using sudo in scripts
Be careful with sudo.
Bad idea:
sudo rm -rf /some/path
Better:
TARGET="/some/safe/path"
echo "About to clean: $TARGET"
read -p "Continue? yes/no: " ANSWER
if [ "$ANSWER" = "yes" ]; then
sudo rm -rf "$TARGET"
else
echo "Cancelled"
fi
Do not put passwords in scripts.
Cron and scripts
Cron can run scripts automatically.
Example cron line:
0 2 * * * /path/to/script.sh >> /path/to/script.log 2>&1
Meaning:
Run every day at 02:00.
Append output and errors to a log file.
Before putting a script in cron:
1. Run it manually.
2. Use full paths.
3. Log output.
4. Make sure permissions are correct.
5. Do not depend on interactive input.
Full paths in scripts
Cron may not have the same environment as your terminal.
Better:
/usr/bin/df -h
/usr/bin/find /opt -type f -mtime +5 -ls
Find command path:
which df
which find
which systemctl
Debugging scripts
Run with Bash debug mode:
bash -x script.sh
This shows each command as Bash runs it.
Example:
+ echo Hello
Hello
Debug inside script:
set -x
echo "debugging"
set +x
Check syntax
bash -n script.sh
This checks syntax without running the script.
Common beginner mistakes
Spaces around =
Wrong:
NAME = "Randy"
Correct:
NAME="Randy"
Forgetting quotes
Risky:
rm $FILE
Better:
rm "$FILE"
Running delete commands too broad
Dangerous:
rm -rf /tmp/*
Safer:
TARGET="/tmp/example-cleanup"
if [ -d "$TARGET" ]; then
rm -rf "$TARGET"/*
fi
Not checking arguments
Bad:
rm "$1"
Better:
if [ "$#" -ne 1 ]; then
echo "Usage: $0 FILE"
exit 1
fi
FILE="$1"
rm "$FILE"
Not checking commands failed
Bad:
cp file.txt /backup/
echo "Backup done"
If copy fails, script still says backup done.
Better:
if cp file.txt /backup/; then
echo "Backup done"
else
echo "Backup failed"
exit 1
fi
Script safety checklist
Before running a script, ask:
What does this script change?
Does it delete anything?
Does it restart services?
Does it use sudo?
Does it touch production?
Does it have logs?
Did I test it with safe data?
Can I undo the change?
Does it use placeholders or real paths?
Safe script template
Use this as a starting point:
#!/bin/bash
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
log() {
echo "[$DATE] [$SCRIPT_NAME] $*"
}
error_exit() {
echo "ERROR: $*" >&2
exit 1
}
log "Starting"
# Check arguments
if [ "$#" -lt 1 ]; then
error_exit "Usage: $0 ARGUMENT"
fi
ARGUMENT="$1"
log "Argument is: $ARGUMENT"
# Main work here
log "Done"
Better logging function
The earlier template uses one fixed date.
This version updates the date each time:
log() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $*"
}
Example:
log "Starting backup"
log "Creating archive"
log "Backup complete"
Study plan
Step 1: Learn commands manually
Practice:
pwd
ls -lah
cd
mkdir
touch
cp
mv
rm
cat
less
grep
find
df -h
free -h
systemctl status
Step 2: Put simple commands in a script
Example:
#!/bin/bash
hostnamectl
uptime
df -h
free -h
Step 3: Add variables
TARGET="/tmp"
echo "Checking $TARGET"
df -h "$TARGET"
Step 4: Add if statements
if [ -d "$TARGET" ]; then
echo "Directory exists"
else
echo "Directory missing"
fi
Step 5: Add arguments
TARGET="$1"
df -h "$TARGET"
Step 6: Add safety checks
if [ "$#" -ne 1 ]; then
echo "Usage: $0 PATH"
exit 1
fi
Step 7: Add logging
log() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $*"
}
Step 8: Add cron later
Only after the script works manually.
Practice exercises
Exercise 1: Hello script
Create a script that prints:
Hello, Linux
Exercise 2: System info script
Create a script that prints:
hostnamectl
uptime
df -h
free -h
Exercise 3: Service checker
Create a script that checks if a service is running.
Input:
./check-service.sh sshd
Output:
sshd is running
or:
sshd is not running
Exercise 4: File checker
Create a script that checks if a file exists.
Input:
./check-file.sh /etc/hosts
Output:
File exists
or:
File missing
Exercise 5: Old file lister
Create a script that lists files older than 5 days in /opt.
Use:
find /opt -type f -mtime +5 -ls
Important:
This exercise should only list files.
Do not delete files.
Exercise 6: Backup script
Create a script that backs up a test folder under /tmp.
Do not test backup scripts first on important data.
Important rules to remember
Quote variables.
Test before deleting.
Use placeholders in public notes.
Use comments.
Log what the script does.
Check arguments.
Do not store passwords in scripts.
Be careful with sudo.
Run manually before cron.
Use bash -n to check syntax.
Use bash -x to debug.