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.