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)