Bash โ Complete Beginner Tutorial
6 modules ยท 18 examples ยท Click โถ Run Script to see terminal output
๐ก How to use this page
Read the explanation, study the code, then hit โถ Run Script to see the output.
Terminal & Basic Commands
The terminal is a text interface where you type commands and the computer responds. Before writing Bash scripts, you need to know the essential commands for moving around your file system and working with files. Think of the terminal as a very precise, very fast way to talk to your computer.
Navigating Your File System
The file system is like a tree of folders. pwd shows where you currently are. ls lists what is in the current folder. cd moves you into a different folder. These three commands are the foundation of everything you do in the terminal.
# pwd โ Print Working Directory (where am I right now?)
pwd
# ls โ List files and folders here
ls
# ls -l โ Long format: shows permissions, size, date
ls -l
# ls -la โ Also show hidden files (files starting with a dot)
ls -la
# cd โ Change Directory (move into a folder)
cd Documents
# cd .. โ Go UP one level (to the parent folder)
cd ..
# cd ~ โ Go to your home directory (always works!)
cd ~
# cd /var/log โ Go to an absolute path
cd /var/log~ is a shortcut for your home directory (/home/yourname on Linux, /Users/yourname on Mac). You can always get back to safety with cd ~ no matter how deep in folders you are.
Creating, Copying & Deleting
mkdir creates a new folder. touch creates an empty file. cp copies files or folders. mv moves or renames them. rm deletes files โ permanently, with no recycle bin! Always double-check before using rm.
# mkdir โ Make a new directory (folder)
mkdir my_project
mkdir -p projects/2025/april # -p creates parent folders too
# touch โ Create an empty file
touch hello.sh
touch notes.txt config.json
# cp โ Copy a file
cp hello.sh hello_backup.sh
# cp -r โ Copy a whole folder (recursively)
cp -r my_project my_project_backup
# mv โ Move or rename a file
mv notes.txt Documents/notes.txt # move to folder
mv hello.sh greet.sh # rename
# rm โ Remove (delete) a file โ NO UNDO!
rm old_file.txt
# rm -r โ Remove a whole folder and everything in it
rm -r my_project_backup
# Echo the result
echo "Files in current directory:"
lsโ ๏ธ rm is permanent โ there is no recycle bin in the terminal. rm -rf (recursive + force) is one of the most dangerous commands on a computer. Always double-check your path before running it.
Reading & Searching Files
cat prints the entire contents of a file. less lets you scroll through long files. head shows the first 10 lines. tail shows the last 10 โ great for watching log files. grep searches for text inside files.
# cat โ Print the whole file to the terminal
cat notes.txt
# head โ Show first 10 lines
head server.log
# head -n 5 โ Show first 5 lines
head -n 5 server.log
# tail โ Show last 10 lines
tail server.log
# tail -f โ Follow the file live (great for logs!)
# tail -f /var/log/syslog
# grep โ Search for a pattern in a file
grep "error" server.log
# grep -i โ Case-insensitive search
grep -i "ERROR" server.log
# grep -n โ Show line numbers
grep -n "Alice" users.txt
# wc -l โ Count lines in a file
wc -l server.logtail -f is your best friend for monitoring servers. It keeps printing new lines as they are added to the file โ so you can watch a log update in real time.
Variables & Input
Variables in Bash store text and numbers so you can reuse them. There are no types โ everything is text by default. You use a $ to read a variable's value. Bash also has special built-in variables and a way to capture the output of commands into variables.
Declaring & Using Variables
Assign a variable with name=value โ NO spaces around the equals sign (this is a very common beginner mistake). Read it back with $name or ${name}. Use double quotes around variable expansions to handle spaces safely.
#!/bin/bash
# Assign variables โ NO spaces around =
name="Alice"
age=25
city="Cape Town"
pi=3.14
# Read variables with $
echo $name
echo $age
# Better: use double quotes (handles spaces in values)
echo "Hello, $name!"
echo "You are $age years old."
# Curly braces: useful when variable name is next to text
version=3
echo "Python${version} is great"
echo "Python$version is great"
# Maths with $(( ))
result=$((age + 5))
echo "In 5 years: $result"
score=80
percentage=$((score * 100 / 100))
echo "Score: $percentage%"NEVER put spaces around the = when assigning a variable. name = "Alice" causes an error because Bash thinks "name" is a command. name="Alice" is correct.
Special Variables & Command Substitution
Bash has built-in variables that are always available: $0 is the script name, $1 $2... are arguments passed to the script, $# is how many arguments, $? is the exit code of the last command. Command substitution $(command) runs a command and uses its output as a value.
#!/bin/bash
# Special built-in variables
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Number of arguments: $#"
echo "All arguments: $@"
# $? โ exit code of the last command (0 = success)
ls /home
echo "Exit code of ls: $?"
ls /nonexistent_folder 2>/dev/null
echo "Exit code of failed ls: $?"
# Command substitution โ capture output of a command
current_date=$(date +"%Y-%m-%d")
echo "Today is: $current_date"
current_user=$(whoami)
echo "Running as: $current_user"
file_count=$(ls | wc -l)
echo "Files in folder: $file_count"
# Useful: hostname
machine=$(hostname)
echo "Machine: $machine"Exit code 0 always means success in Unix. Any non-zero code means something went wrong. This is the opposite of most programming languages! Checking $? after important commands is how you detect failures in scripts.
Reading User Input with read
The read command pauses the script and waits for the user to type something. The -p flag lets you show a prompt on the same line. The -s flag hides input โ perfect for passwords.
#!/bin/bash
# Basic input
read -p "What is your name? " name
echo "Hello, $name!"
# Input with default value
read -p "Enter your city (default: Cape Town): " city
city=${city:-"Cape Town"}
echo "City: $city"
# Hidden input (for passwords)
read -s -p "Enter your password: " password
echo ""
echo "Password length: ${#password} characters"
# Read multiple values at once
read -p "Enter first and last name: " first last
echo "First: $first"
echo "Last: $last"
# Read into an array
read -p "Enter three colours: " -a colours
echo "You chose: ${colours[0]}, ${colours[1]}, ${colours[2]}"${variable:-default} is a Bash "default value" expansion. If the variable is empty or unset, it uses the default value. This is very handy for making scripts that work with or without user input.
Conditions & Tests
Bash conditions work differently from other languages. You use square brackets [ ] to test things, and Bash has special operators for comparing numbers (-eq, -gt, -lt) and for testing files (-f, -d, -e). The syntax looks strange at first but becomes natural quickly.
if / elif / else
The if statement runs code when a condition is true. Note the spacing rules: spaces INSIDE the brackets are required. The condition ends with then, and the block ends with fi (if backwards). elif adds extra branches.
#!/bin/bash
score=75
# Basic if/elif/else
if [ $score -ge 80 ]; then
echo "Grade: A โ Excellent!"
elif [ $score -ge 70 ]; then
echo "Grade: B โ Good job!"
elif [ $score -ge 60 ]; then
echo "Grade: C โ Average"
elif [ $score -ge 50 ]; then
echo "Grade: D โ Just passed"
else
echo "Grade: F โ Try again"
fi
# Comparing strings
day="Monday"
if [ "$day" = "Saturday" ] || [ "$day" = "Sunday" ]; then
echo "It is the weekend!"
else
echo "$day is a weekday."
fi
# Checking if a variable is empty
username=""
if [ -z "$username" ]; then
echo "No username provided"
else
echo "Username: $username"
fiSpaces inside [ ] are REQUIRED. [ $x -eq 5 ] works. [$x -eq 5] causes an error. Also always quote string variables: [ "$name" = "Alice" ] โ without quotes, empty variables break the condition.
File Tests โ Checking Files & Folders
Bash has special operators for testing files. -e checks if something exists, -f checks if it is a regular file, -d checks if it is a directory, -r/-w/-x check permissions. These are used constantly in real scripts.
#!/bin/bash
file="notes.txt"
folder="Documents"
# -e exists (file or directory)
if [ -e "$file" ]; then
echo "$file exists"
else
echo "$file does NOT exist"
fi
# -f is a regular file (not a directory)
if [ -f "$file" ]; then
echo "$file is a file"
fi
# -d is a directory
if [ -d "$folder" ]; then
echo "$folder is a directory"
fi
# -r is readable
if [ -r "$file" ]; then
echo "$file is readable"
fi
# -w is writable
if [ -w "$file" ]; then
echo "$file is writable"
fi
# -s is not empty (has content)
if [ -s "$file" ]; then
echo "$file has content"
else
echo "$file is empty"
fi
# Practical: create folder only if it doesn't exist
backup_dir="./backups"
if [ ! -d "$backup_dir" ]; then
mkdir "$backup_dir"
echo "Created $backup_dir"
else
echo "$backup_dir already exists"
fi! flips any test: [ ! -f "$file" ] means "file does NOT exist". This is used constantly โ "if the config file does not exist, create a default one" is a very common script pattern.
case โ Multiple Options
The case statement is like a cleaner version of many if/elif branches. It matches a value against patterns and runs the matching block. Each option ends with ;; and the whole case ends with esac (case backwards).
#!/bin/bash
read -p "Enter a day (Mon/Tue/Wed...): " day
case "$day" in
Mon|Monday)
echo "Start of the work week. Let's go!"
;;
Tue|Tuesday)
echo "Tuesday โ keep the momentum."
;;
Wed|Wednesday)
echo "Midweek already!"
;;
Thu|Thursday)
echo "Nearly Friday..."
;;
Fri|Friday)
echo "TGIF! Almost weekend."
;;
Sat|Saturday|Sun|Sunday)
echo "Weekend! Time to rest."
;;
*)
echo "Unknown day: $day"
;;
esac
# Case with commands
action="backup"
case "$action" in
start) echo "Starting service..." ;;
stop) echo "Stopping service..." ;;
restart) echo "Restarting service..." ;;
backup) echo "Running backup now..." ;;
*) echo "Unknown action: $action" ;;
esacThe * pattern at the end of a case is the "catch-all" โ it matches anything that did not match the earlier patterns. Always include it as your last option to handle unexpected input gracefully.
Loops
Loops in Bash let you repeat commands โ process every file in a folder, count through numbers, or keep asking for input until you get a valid answer. The for loop and while loop cover 95% of real use cases.
for Loops
Bash for loops work over a list of items. You can list items directly, use a range with {1..10}, use seq for more control, or loop over files with a wildcard pattern. The C-style for loop also works if you prefer it.
#!/bin/bash
# Loop over a list of items
for colour in red green blue yellow; do
echo "Colour: $colour"
done
echo "---"
# Loop with a number range {start..end}
for i in {1..5}; do
echo "Count: $i"
done
echo "---"
# Loop with step {start..end..step}
for i in {0..20..5}; do
echo -n "$i "
done
echo
echo "---"
# C-style for loop
for (( i=1; i<=4; i++ )); do
echo "Item $i"
done
echo "---"
# Loop over all .txt files in a directory
for file in *.txt; do
echo "Found text file: $file"
doneLooping over files with *.txt is incredibly useful. You can rename all files at once, process every log, convert every image. This is where Bash automation really shines.
while Loops & break / continue
A while loop runs as long as a condition is true. It is great for reading files line by line, waiting for something to happen, or repeating until the user gives valid input. break exits the loop early, continue skips to the next iteration.
#!/bin/bash
# Basic while loop โ countdown
count=5
while [ $count -gt 0 ]; do
echo "$count..."
count=$((count - 1))
done
echo "Blast off!"
echo "---"
# Validate user input with a loop
while true; do
read -p "Enter a number between 1-10: " num
if [ "$num" -ge 1 ] && [ "$num" -le 10 ] 2>/dev/null; then
echo "Great! You entered: $num"
break
else
echo "Invalid! Please try again."
fi
done
echo "---"
# continue โ skip even numbers
for i in {1..8}; do
remainder=$((i % 2))
if [ $remainder -eq 0 ]; then
continue
fi
echo "Odd: $i"
donewhile true; do ... done creates an infinite loop that only stops when you hit break. This pattern is perfect for menus and input validation โ keep asking until you get something valid.
Functions & Scripts
Functions let you group commands under a name and call them multiple times. Arguments are passed positionally ($1, $2...) just like script arguments. Scripts are just text files with a .sh extension and a special first line called a shebang.
Defining & Calling Functions
Define a function with the function name or just name(). Put the commands inside curly braces. Call it by just typing its name. Bash functions must be defined BEFORE they are called โ put them at the top of your script.
#!/bin/bash
# Define a function
greet() {
echo "Hello, $1!"
echo "Welcome to the terminal."
}
# Call the function with an argument
greet "Alice"
greet "Bob"
echo "---"
# Function with multiple arguments
introduce() {
local name=$1
local age=$2
local city=$3
echo "$name is $age years old and lives in $city."
}
introduce "Alice" 25 "Cape Town"
introduce "Bob" 30 "Johannesburg"
echo "---"
# Function that returns a value via echo
add_numbers() {
local result=$(( $1 + $2 ))
echo $result
}
sum=$(add_numbers 15 27)
echo "15 + 27 = $sum"
double=$(add_numbers $sum $sum)
echo "Doubled: $double"Always use local for variables inside functions. Without local, variables are global and can accidentally overwrite variables in other parts of your script โ a very hard bug to track down.
Writing a Complete Script
A proper Bash script starts with a shebang line (#!/bin/bash), has a description comment, defines functions near the top, and then runs the main logic at the bottom. Make it executable with chmod +x and run it with ./scriptname.sh.
#!/bin/bash
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# backup.sh โ Simple backup script
# Usage: ./backup.sh <source_folder>
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# --- Configuration ---
BACKUP_DIR="$HOME/backups"
DATE=$(date +"%Y-%m-%d_%H-%M")
# --- Functions ---
check_args() {
if [ $# -lt 1 ]; then
echo "Usage: $0 <folder_to_backup>"
exit 1
fi
}
create_backup_dir() {
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
echo "Created backup directory: $BACKUP_DIR"
fi
}
do_backup() {
local source=$1
local dest="$BACKUP_DIR/backup_${DATE}.tar.gz"
echo "Backing up: $source"
echo "Destination: $dest"
tar -czf "$dest" "$source" 2>/dev/null
echo "Backup complete!"
echo "Size: $(du -sh "$dest" | cut -f1)"
}
# --- Main ---
check_args "$@"
create_backup_dir
do_backup "$1"Make a script executable with: chmod +x backup.sh โ then run it with: ./backup.sh. The shebang #!/bin/bash on line 1 tells the system which interpreter to use. Without it, the script might run with the wrong shell.
Files & Text Processing
Bash is incredibly powerful for processing text files. Pipes (|) chain commands together so the output of one becomes the input of the next. grep, sed, and awk are three classic tools that together can transform, search, and analyse almost any text data.
Pipes & Redirection
The pipe | sends output from one command into another. > writes output to a file (overwrites). >> appends to a file. < reads input from a file. 2> redirects errors. These are the plumbing of the Unix world.
#!/bin/bash
# | pipe โ chain commands
# Count how many files are in the folder
ls | wc -l
# Find all .sh files and count them
ls *.sh | wc -l
# Sort the output of ls alphabetically
ls | sort
# Search and count matching lines
grep "error" server.log | wc -l
# Chain multiple pipes
cat server.log | grep "ERROR" | sort | uniq
echo "---"
# > Redirect output to a file (creates or overwrites)
echo "Log started" > output.log
date >> output.log
ls -la >> output.log
# Read file contents
cat output.log
echo "---"
# 2> Redirect errors to a file (suppress them)
ls /nonexistent 2>/dev/null
ls /nonexistent 2>errors.log
# Redirect both output AND errors
ls /home /nonexistent > all.log 2>&12>/dev/null is a very common pattern โ it sends error messages into "the void" so they do not clutter your output. The /dev/null file is a special file that discards everything written to it.
grep & sed โ Search & Replace
grep is the go-to tool for searching text. sed is the "stream editor" โ it finds and replaces text in files or streams without opening them in an editor. Together they handle most text processing tasks.
#!/bin/bash
# โโ GREP โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Basic search
grep "Alice" users.txt
# -i Case insensitive
grep -i "alice" users.txt
# -n Show line numbers
grep -n "error" server.log
# -c Count matching lines only
grep -c "error" server.log
# -v Invert โ show NON-matching lines
grep -v "INFO" server.log
# -r Search recursively in a folder
grep -r "TODO" ./scripts/
# โโ SED โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Replace first occurrence per line
sed 's/error/ERROR/' server.log
# Replace ALL occurrences per line (g flag)
sed 's/http/https/g' urls.txt
# Edit file in place (saves changes to file)
sed -i 's/localhost/production.server.com/g' config.txt
# Delete lines matching a pattern
sed '/^#/d' config.txt
# Print only specific lines (e.g. lines 5-10)
sed -n '5,10p' bigfile.txtsed 's/old/new/g' is the most used sed pattern. The s stands for substitute, the g at the end means global (all occurrences on each line). Without the g it only replaces the first match per line.
File Permissions & chmod
Every file on Linux has permissions for three groups: owner, group, and others. chmod changes these permissions. The +x flag makes a file executable โ required before you can run a script.
#!/bin/bash
# View permissions with ls -l
ls -l myscript.sh
# chmod โ Change file permissions
# Using letters:
chmod +x myscript.sh
chmod +r notes.txt
chmod -w readonly.txt
chmod u+x script.sh
chmod go-w private.sh
# Using numbers (octal):
# 4=read 2=write 1=execute
chmod 755 myscript.sh # rwxr-xr-x
chmod 644 config.txt # rw-r--r--
chmod 600 secret.key # rw-------
ls -l myscript.sh
echo "Script is now executable!"755 is the standard permission for scripts and programs. 644 is standard for regular files. 600 for anything sensitive like SSH keys or passwords.
You finished the Bash tutorial!
You can now navigate the terminal, write scripts with variables, conditions, loops and functions, and process files with grep, sed and pipes. These skills power DevOps, Linux admin, and automation worldwide.