8. shell scripting - mishraxharshit/harshitxmishra.github.io GitHub Wiki
Phase 8 — Shell Scripting
Previous: [Phase 7 — Storage and Filesystems](Phase-7-Storage-and-Filesystems) | Next: [Phase 9 — System Administration](Phase-9-System-Administration)
8.1 What Is a Shell Script?
A shell script is a text file containing a sequence of shell commands. Instead of typing commands one by one, you save them in a file and run the file. Scripts can accept arguments, make decisions, repeat operations, and handle errors.
8.2 Your First Script
# Create the file
nano hello.sh
#!/bin/bash
# The first line is called the shebang. It tells the kernel which interpreter to use.
# Without it, the script runs with the current shell, which may behave differently.
echo "Hello, World"
echo "Today is: $(date)"
echo "You are running this as: $(whoami)"
echo "Your current directory is: $(pwd)"
# Make it executable
chmod +x hello.sh
# Run it
./hello.sh
# The ./ is required because the current directory is not in $PATH
8.3 Variables
#!/bin/bash
# Assign a variable (no spaces around =)
name="Alice"
count=10
pi=3.14
# Use a variable with $
echo "Hello, $name"
echo "Count: $count"
# Curly braces are needed to avoid ambiguity
filename="report"
echo "${filename}_2024.pdf" # outputs: report_2024.pdf
echo "$filename_2024.pdf" # outputs: .pdf (underscore is part of name)
# Command substitution: store command output in a variable
current_user=$(whoami)
today=$(date +%Y-%m-%d) # date in YYYY-MM-DD format
file_count=$(ls /etc | wc -l)
echo "User: $current_user"
echo "Date: $today"
echo "Files in /etc: $file_count"
# Read-only variable
readonly MAX_RETRIES=3
# Unset a variable
unset name
Special variables:
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "PID of this script: $$"
echo "Exit code of last command: $?"
8.4 User Input
#!/bin/bash
# Read input from user
read -p "Enter your name: " username
echo "Hello, $username"
# Read with a timeout (5 seconds)
read -t 5 -p "Answer within 5 seconds: " answer
# Read a password without showing it on screen
read -s -p "Password: " password
echo "" # newline after the hidden input
8.5 Conditionals
#!/bin/bash
age=25
# Basic if statement
if [ $age -ge 18 ]; then
echo "You are an adult"
fi
# If-else
if [ $age -ge 18 ]; then
echo "Adult"
else
echo "Minor"
fi
# If-elif-else
if [ $age -lt 13 ]; then
echo "Child"
elif [ $age -lt 18 ]; then
echo "Teenager"
elif [ $age -lt 65 ]; then
echo "Adult"
else
echo "Senior"
fi
Comparison operators:
# Numeric comparisons
[ $a -eq $b ] # equal
[ $a -ne $b ] # not equal
[ $a -lt $b ] # less than
[ $a -le $b ] # less than or equal
[ $a -gt $b ] # greater than
[ $a -ge $b ] # greater than or equal
# String comparisons
[ "$s1" = "$s2" ] # equal (always quote string variables)
[ "$s1" != "$s2" ] # not equal
[ -z "$s1" ] # true if string is empty
[ -n "$s1" ] # true if string is not empty
# File tests
[ -f "$path" ] # true if file exists and is a regular file
[ -d "$path" ] # true if directory exists
[ -e "$path" ] # true if anything exists at that path
[ -r "$path" ] # true if file is readable
[ -w "$path" ] # true if file is writable
[ -x "$path" ] # true if file is executable
# Combine conditions
[ $age -ge 18 ] && [ $age -lt 65 ] # AND
[ "$day" = "Sat" ] || [ "$day" = "Sun" ] # OR
Practical example: check if a file exists before operating on it:
#!/bin/bash
config_file="/etc/myapp/config.conf"
if [ ! -f "$config_file" ]; then
echo "Error: config file not found at $config_file"
exit 1
fi
echo "Config file found, proceeding..."
8.6 Loops
#!/bin/bash
# for loop: iterate over a list
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
# for loop: iterate over files
for file in /var/log/*.log; do
echo "Log file: $file"
wc -l "$file"
done
# for loop: C-style numeric loop
for (( i=1; i<=5; i++ )); do
echo "Number: $i"
done
# while loop
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$(( count + 1 ))
done
# while loop: read a file line by line
while read line; do
echo "Line: $line"
done < /etc/hosts
# until loop (loop until condition is true)
until [ -f /tmp/ready.flag ]; do
echo "Waiting for ready flag..."
sleep 2
done
echo "Ready flag found!"
# break and continue
for i in 1 2 3 4 5; do
if [ $i -eq 3 ]; then
continue # skip this iteration
fi
if [ $i -eq 5 ]; then
break # exit the loop
fi
echo $i
done
# Output: 1 2 4
8.7 Functions
#!/bin/bash
# Define a function
greet() {
local name="$1" # local: variable only exists inside this function
echo "Hello, $name!"
}
# Call the function
greet "Alice"
greet "Bob"
# Function with return value
# Bash functions return exit codes (0 = success, non-zero = failure)
# To return data, use echo and capture with $()
add() {
local result=$(( $1 + $2 ))
echo $result
}
sum=$(add 10 20)
echo "Sum: $sum" # Sum: 30
# Function checking for errors
check_root() {
if [ "$(id -u)" -ne 0 ]; then
echo "Error: this script must be run as root"
exit 1
fi
}
check_root # call at top of script to enforce
8.8 Error Handling
#!/bin/bash
# Exit immediately if any command fails
set -e
# Treat unset variables as errors
set -u
# In a pipeline, fail if any command fails (not just the last)
set -o pipefail
# Useful for debugging: print each command before executing it
set -x
# Check exit codes explicitly
cp source.txt dest.txt
if [ $? -ne 0 ]; then
echo "Copy failed"
exit 1
fi
# Trap: run cleanup code when script exits or receives a signal
cleanup() {
echo "Cleaning up..."
rm -f /tmp/myapp_lock
}
trap cleanup EXIT
trap cleanup INT TERM # also clean up if Ctrl+C or kill
8.9 Complete Real-World Script Example
Automated backup script:
#!/bin/bash
set -euo pipefail
# Configuration
SOURCE_DIR="/home/alice/projects"
BACKUP_DIR="/mnt/backup"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="${BACKUP_DIR}/projects_${DATE}.tar.gz"
LOG_FILE="/var/log/backup.log"
MAX_BACKUPS=7 # keep last 7 backups
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
check_requirements() {
if [ ! -d "$SOURCE_DIR" ]; then
log "ERROR: Source directory $SOURCE_DIR does not exist"
exit 1
fi
if [ ! -d "$BACKUP_DIR" ]; then
log "Backup directory does not exist, creating..."
mkdir -p "$BACKUP_DIR"
fi
}
create_backup() {
log "Starting backup of $SOURCE_DIR"
tar -czf "$BACKUP_FILE" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)"
local size=$(du -sh "$BACKUP_FILE" | cut -f1)
log "Backup created: $BACKUP_FILE (size: $size)"
}
rotate_backups() {
local count=$(ls "${BACKUP_DIR}"/projects_*.tar.gz 2>/dev/null | wc -l)
if [ "$count" -gt "$MAX_BACKUPS" ]; then
local to_delete=$(( count - MAX_BACKUPS ))
log "Rotating old backups, deleting $to_delete old files"
ls -t "${BACKUP_DIR}"/projects_*.tar.gz | tail -"$to_delete" | xargs rm
fi
}
main() {
log "=== Backup script started ==="
check_requirements
create_backup
rotate_backups
log "=== Backup script completed successfully ==="
}
main "$@"
Phase 8 Exercises
Exercise 1: Write a script that accepts a filename as argument, checks if the file exists, and prints the number of lines, words, and characters in it.
Exercise 2: Write a script that loops through all .log files in /var/log, and for each one prints the filename and the number of lines.
Exercise 3: Write a function called is_number() that returns 0 (success) if its argument is a valid integer, 1 (failure) otherwise. Test it.
Exercise 4: Modify the backup script to send an email or write a different log message if the backup fails (use trap ERR).
Previous: [Phase 7 — Storage and Filesystems](Phase-7-Storage-and-Filesystems) | Next: [Phase 9 — System Administration](Phase-9-System-Administration)