Linux Shell Scripting for Beginners: From Zero to Your First Script

A shell script is a text file containing commands that your terminal would normally run one at a time. Scripting lets you automate repetitive tasks, combine commands in sequence, and add logic like conditions and loops.
If you can run commands in a terminal, you already know the basics — this guide shows you how to put them together.
Your First Shell Script
Create a file, give it the right permissions, and run it:
# Create the script
nano hello.sh
#!/bin/bash
echo "Hello, World!"
echo "Today is: $(date)"
# Make it executable
chmod +x hello.sh
# Run it
./hello.sh
Output:
Hello, World!
Today is: Fri Jun 20 14:23:11 UTC 2026
The shebang line
#!/bin/bash is the shebang — it tells the system which interpreter to use. Without it, the script might run with your current shell (which may work) or fail (which may not be obvious).
Common shebangs:
#!/bin/bash # bash — most compatible
#!/usr/bin/env bash # finds bash in PATH — more portable
#!/bin/sh # POSIX sh — less features, more portable
Variables
#!/bin/bash
name="Alice"
count=42
greeting="Hello, $name!"
echo $name
echo $count
echo "$greeting, you have $count messages."
Rules:
- No spaces around
=in assignments:name="Alice"works,name = "Alice"fails - Access variables with
$prefix:$nameor${name} - Use double quotes to prevent word splitting:
"$name"not$namewhen the value might have spaces
Command substitution
Capture command output into a variable:
current_date=$(date +%Y-%m-%d)
file_count=$(ls | wc -l)
hostname=$(hostname)
echo "Date: $current_date"
echo "Files: $file_count"
echo "Host: $hostname"
Special variables
| Variable | Value |
|---|---|
$0 |
Script name |
$1, $2, ... |
Positional arguments |
$# |
Number of arguments |
$@ |
All arguments as separate words |
$* |
All arguments as one string |
$? |
Exit code of the last command |
$$ |
PID of the current script |
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "All arguments: $@"
echo "Argument count: $#"
Run: ./script.sh foo bar baz
User Input
#!/bin/bash
echo "Enter your name:"
read name
echo "Hello, $name!"
# Read with a prompt on the same line
read -p "Enter filename: " filename
echo "You entered: $filename"
# Read silently (for passwords)
read -s -p "Password: " password
echo # newline after silent input
Conditionals
if/else
#!/bin/bash
age=$1
if [ "$age" -ge 18 ]; then
echo "Adult"
elif [ "$age" -ge 13 ]; then
echo "Teenager"
else
echo "Child"
fi
Comparison operators
Numeric comparisons (use inside [ ] or (( ))):
| Operator | Meaning |
|---|---|
-eq |
equal |
-ne |
not equal |
-lt |
less than |
-le |
less than or equal |
-gt |
greater than |
-ge |
greater than or equal |
String comparisons:
| Operator | Meaning |
|---|---|
= |
equal |
!= |
not equal |
-z |
empty string |
-n |
non-empty string |
File tests:
| Operator | Meaning |
|---|---|
-f file |
exists and is a regular file |
-d dir |
exists and is a directory |
-e path |
exists (any type) |
-r file |
exists and is readable |
-w file |
exists and is writable |
-x file |
exists and is executable |
-s file |
exists and is non-empty |
#!/bin/bash
if [ -f "/etc/nginx/nginx.conf" ]; then
echo "nginx config exists"
fi
if [ ! -d "/tmp/backups" ]; then
mkdir -p /tmp/backups
echo "Created backup directory"
fi
Modern test syntax [[ ]]
[[ ]] is a bash extension that handles edge cases better than [ ]:
# Pattern matching
if [[ "$filename" == *.txt ]]; then
echo "It's a text file"
fi
# Logical operators
if [[ "$status" == "active" && "$count" -gt 0 ]]; then
echo "System is active with items"
fi
# Regex matching
if [[ "$input" =~ ^[0-9]+$ ]]; then
echo "Input is a number"
fi
case statement
#!/bin/bash
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
Loops
for loop
#!/bin/bash
# Loop over a list
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
# Loop over files
for file in *.txt; do
echo "Processing: $file"
wc -l "$file"
done
# C-style for loop
for ((i=1; i<=5; i++)); do
echo "Count: $i"
done
# Loop over command output
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "User: $user"
done
while loop
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
((count++))
done
# Read a file line by line
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
until loop
Runs until a condition becomes true (opposite of while):
until ping -c 1 google.com &>/dev/null; do
echo "Waiting for network..."
sleep 2
done
echo "Network is up"
Functions
#!/bin/bash
# Define a function
greet() {
local name="$1" # local: variable only exists inside the function
echo "Hello, $name!"
}
# Call it
greet "Alice"
greet "Bob"
# Return values
get_file_size() {
local file="$1"
if [ -f "$file" ]; then
wc -c < "$file"
else
echo 0
fi
}
size=$(get_file_size /etc/hosts)
echo "File size: $size bytes"
Functions return exit codes (0 = success, non-zero = failure), not values. To return a value, use echo and capture with $().
Exit Codes and Error Handling
Every command exits with a code. 0 means success; anything else means failure.
#!/bin/bash
# Check if a command succeeded
if cp important.txt /backup/; then
echo "Backup succeeded"
else
echo "Backup failed!"
exit 1
fi
# Or use $? directly
ls /nonexistent
echo "Exit code: $?" # prints 2 (file not found)
set -e: exit on error
#!/bin/bash
set -e # exit the script if any command fails
cp file1.txt /backup/
cp file2.txt /backup/ # if this fails, script stops here
echo "All backed up" # only runs if both copies succeeded
set -u: fail on undefined variables
#!/bin/bash
set -u # treat unset variables as errors
echo $undefined_var # would normally print nothing; now exits with error
Common to see both together: set -eu or set -euo pipefail.
Practical Script Examples
Backup script
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup/$(date +%Y-%m-%d)"
SOURCE_DIR="/var/www/html"
mkdir -p "$BACKUP_DIR"
tar czf "$BACKUP_DIR/website.tar.gz" "$SOURCE_DIR"
echo "Backup complete: $BACKUP_DIR/website.tar.gz"
Check if a service is running
#!/bin/bash
SERVICE="nginx"
if systemctl is-active --quiet "$SERVICE"; then
echo "$SERVICE is running"
else
echo "$SERVICE is NOT running — starting it..."
sudo systemctl start "$SERVICE"
fi
Process a list of files
#!/bin/bash
INPUT_DIR="./reports"
OUTPUT_DIR="./processed"
mkdir -p "$OUTPUT_DIR"
for file in "$INPUT_DIR"/*.csv; do
filename=$(basename "$file")
echo "Processing $filename..."
# example: convert to uppercase
tr '[:lower:]' '[:upper:]' < "$file" > "$OUTPUT_DIR/$filename"
done
echo "Done. Processed files are in $OUTPUT_DIR/"
Debugging Scripts
bash -x script.sh # print each command before executing
bash -n script.sh # check syntax without running
set -x # enable in the script itself
set +x # disable tracing
With bash -x, you see each expanded command before it runs — invaluable for tracking down where things go wrong.
Next Steps
This covers the core of shell scripting. From here, explore:
sedandawkfor text processingcronfor scheduling scriptstrapfor cleanup on exit- Arrays and associative arrays (bash 4+)
The Linux Shell Scripting Basics Guide covers these topics with additional real-world automation examples.
Get The Practical Linux Handbook
Read a free sample
All Linux topics
Enjoyed this article? Share it!
About the Author
Vajo Lukic
Vajo Lukic is a technology leader with 20+ years of experience in software development and system administration. Author of The Practical Linux Handbook, he shares practical, field-tested knowledge to help developers and IT professionals master Linux fundamentals.
Read more about VajoRelated Articles

Linux File Permissions: The Complete Beginner's Guide (chmod, chown, umask)
Learn Linux file permissions from scratch. Understand rwx, chmod numbers, chown, and umask with clear examples that make the permission system click.
Read more →
SSH Remote Access: Step-by-Step Guide for Linux Beginners
Learn to connect to Linux systems remotely with SSH. From your first ssh command to key-based authentication, port forwarding, and secure configuration.
Read more →
10 Essential Linux Commands Every Developer Should Know
Learn these 10 fundamental Linux commands and transform your productivity. From file navigation to system monitoring, discover the tools that every developer needs in their arsenal.
Read more →Ready to Transform Your Life?
Get the complete guide to personal transformation and start your journey today.
Get the Book