📜 Comprehensive Guide to BASH Shell Script Programming

About this Guide: This comprehensive guide covers everything from basic BASH scripting to advanced techniques. Whether you're automating system administration tasks, writing complex deployment scripts, or just learning shell programming, this guide has you covered.

Table of Contents

Introduction to BASH

BASH (Bourne Again SHell) is the default shell on most Linux systems. It's both a command interpreter and a programming language.

Why Learn BASH?

Check Your BASH Version

bash --version
# Or
echo $BASH_VERSION

# Common locations
which bash
# Usually: /bin/bash or /usr/bin/bash

Script Basics

Your First BASH Script

#!/bin/bash
# This is a comment
# The first line is the "shebang" - tells the system which interpreter to use

echo "Hello, World!"
echo "This is my first BASH script"

Creating and Running Scripts

# Create the script file
cat > hello.sh <<'EOF'
#!/bin/bash
echo "Hello, World!"
EOF

# Make it executable
chmod +x hello.sh

# Run the script - three ways:
./hello.sh           # Execute directly (requires chmod +x)
bash hello.sh        # Run with bash interpreter
source hello.sh      # Run in current shell (also: . hello.sh)
Shebang Variations:

Script Structure

#!/bin/bash
#
# Script Name: example.sh
# Description: Brief description of what the script does
# Author: Your Name
# Date: 2025-10-31
# Version: 1.0
#
# Usage: ./example.sh [options] [arguments]
#

# Exit on error
set -e

# Variables
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(dirname "$0")

# Functions go here
function main() {
    echo "Main script logic"
}

# Main execution
main "$@"

Variables and Data Types

Variable Declaration and Assignment

#!/bin/bash

# Basic assignment (no spaces around =)
NAME="John"
AGE=30
PI=3.14159

# Using variables
echo "Name: $NAME"
echo "Age: $AGE"
echo "Pi: ${PI}"

# Command substitution
CURRENT_DATE=$(date)
USER_COUNT=`who | wc -l`  # Old style backticks

echo "Date: $CURRENT_DATE"
echo "Users logged in: $USER_COUNT"

# Read-only variables (constants)
readonly DB_HOST="localhost"
declare -r MAX_CONNECTIONS=100

# Unsetting variables
unset NAME

Variable Types and Attributes

#!/bin/bash

# Integer
declare -i NUMBER=42
NUMBER=NUMBER+10  # Arithmetic without $(())
echo $NUMBER      # Prints: 52

# Array
declare -a FRUITS=("apple" "banana" "orange")

# Associative array (hash/dictionary)
declare -A CAPITALS
CAPITALS[USA]="Washington"
CAPITALS[UK]="London"

# Uppercase
declare -u UPPERCASE="hello"
echo $UPPERCASE  # Prints: HELLO

# Lowercase
declare -l LOWERCASE="WORLD"
echo $LOWERCASE  # Prints: world

# Read-only
declare -r CONSTANT="value"

# Export (make available to child processes)
declare -x EXPORTED_VAR="exported"

Special Variables

Variable Description Example
$0 Script name ./script.sh
$1, $2, ... Positional parameters First, second argument, etc.
$# Number of arguments 3
$@ All arguments (as separate words) "arg1" "arg2" "arg3"
$* All arguments (as single word) "arg1 arg2 arg3"
$? Exit status of last command 0 (success) or non-zero (error)
$$ Process ID (PID) of script 12345
$! PID of last background command 12346
$_ Last argument of previous command filename.txt
$HOME Home directory /home/user
$USER Current username john
$HOSTNAME Computer name server01
$RANDOM Random number (0-32767) 15234
$LINENO Current line number in script 42

Environment Variables

#!/bin/bash

# View all environment variables
printenv
# or
env

# Common environment variables
echo "Path: $PATH"
echo "Shell: $SHELL"
echo "Home: $HOME"
echo "User: $USER"
echo "PWD: $PWD"

# Set environment variable for child processes
export MY_VAR="value"

# Set for this script only
MY_LOCAL_VAR="local value"

Command Line Arguments

Basic Argument Handling

#!/bin/bash

# Simple argument access
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"

# Check if arguments provided
if [ $# -eq 0 ]; then
    echo "Usage: $0 <arg1> <arg2>"
    exit 1
fi

Shift and Process Arguments

#!/bin/bash

# Shift moves arguments down
echo "First arg: $1"
shift
echo "Now first arg (was second): $1"
shift
echo "Now first arg (was third): $1"

# Process all arguments with shift
while [ $# -gt 0 ]; do
    echo "Processing: $1"
    shift
done

Using getopts for Options

#!/bin/bash

# Parse command line options
VERBOSE=false
OUTPUT_FILE=""

while getopts "vo:h" opt; do
    case $opt in
        v)
            VERBOSE=true
            ;;
        o)
            OUTPUT_FILE="$OPTARG"
            ;;
        h)
            echo "Usage: $0 [-v] [-o output_file]"
            echo "  -v              Verbose mode"
            echo "  -o output_file  Specify output file"
            echo "  -h              Show this help"
            exit 0
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            exit 1
            ;;
        :)
            echo "Option -$OPTARG requires an argument" >&2
            exit 1
            ;;
    esac
done

# Shift to get non-option arguments
shift $((OPTIND-1))

if [ "$VERBOSE" = true ]; then
    echo "Verbose mode enabled"
fi

if [ -n "$OUTPUT_FILE" ]; then
    echo "Output file: $OUTPUT_FILE"
fi

echo "Remaining arguments: $@"

Advanced Argument Parsing

#!/bin/bash

# Long options support
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            echo "Help message"
            exit 0
            ;;
        -v|--verbose)
            VERBOSE=true
            shift
            ;;
        -o|--output)
            OUTPUT_FILE="$2"
            shift 2
            ;;
        -c|--config)
            CONFIG_FILE="$2"
            shift 2
            ;;
        -*)
            echo "Unknown option: $1"
            exit 1
            ;;
        *)
            # Positional argument
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
    esac
done

# Restore positional parameters
set -- "${POSITIONAL_ARGS[@]}"

Operators

Arithmetic Operators

#!/bin/bash

# Basic arithmetic - multiple methods
A=10
B=3

# Method 1: $(( ))
SUM=$((A + B))
DIFF=$((A - B))
PROD=$((A * B))
DIV=$((A / B))
MOD=$((A % B))
POWER=$((A ** 2))

echo "Sum: $SUM"
echo "Difference: $DIFF"
echo "Product: $PROD"
echo "Division: $DIV"
echo "Modulo: $MOD"
echo "Power: $POWER"

# Method 2: let
let RESULT=A+B
let A++        # Increment
let B--        # Decrement
let A+=5       # Add and assign

# Method 3: expr (older, external command)
RESULT=$(expr $A + $B)

# Method 4: bc for floating point
RESULT=$(echo "scale=2; 10/3" | bc)
echo "10/3 = $RESULT"

Comparison Operators

Numeric String Description
-eq = or == Equal to
-ne != Not equal to
-gt > Greater than
-ge >= Greater than or equal
-lt < Less than
-le <= Less than or equal
-z String is null (zero length)
-n String is not null
#!/bin/bash

# Numeric comparisons
NUM1=10
NUM2=20

if [ $NUM1 -eq $NUM2 ]; then
    echo "Equal"
fi

if [ $NUM1 -lt $NUM2 ]; then
    echo "$NUM1 is less than $NUM2"
fi

# Modern syntax with (( ))
if (( NUM1 < NUM2 )); then
    echo "$NUM1 is less than $NUM2"
fi

# String comparisons
STR1="hello"
STR2="world"

if [ "$STR1" = "$STR2" ]; then
    echo "Strings are equal"
fi

if [ "$STR1" != "$STR2" ]; then
    echo "Strings are different"
fi

# Check if string is empty
if [ -z "$STR1" ]; then
    echo "String is empty"
fi

if [ -n "$STR1" ]; then
    echo "String is not empty"
fi

Logical Operators

#!/bin/bash

# AND operator: &&
if [ -f file.txt ] && [ -r file.txt ]; then
    echo "File exists and is readable"
fi

# OR operator: ||
if [ "$USER" = "root" ] || [ "$UID" -eq 0 ]; then
    echo "Running as root"
fi

# NOT operator: !
if [ ! -f file.txt ]; then
    echo "File does not exist"
fi

# Combining with [[ ]]
if [[ -f file.txt && -r file.txt ]]; then
    echo "File exists and is readable"
fi

if [[ "$USER" == "root" || "$UID" -eq 0 ]]; then
    echo "Running as root"
fi

File Test Operators

Operator Description
-e file File exists
-f file File exists and is a regular file
-d file File exists and is a directory
-L file File exists and is a symbolic link
-r file File exists and is readable
-w file File exists and is writable
-x file File exists and is executable
-s file File exists and has size > 0
-b file File is a block device
-c file File is a character device
file1 -nt file2 file1 is newer than file2
file1 -ot file2 file1 is older than file2

Conditional Statements

If-Then-Else

#!/bin/bash

# Basic if
if [ condition ]; then
    echo "Condition is true"
fi

# If-else
if [ $# -eq 0 ]; then
    echo "No arguments provided"
else
    echo "Arguments provided: $@"
fi

# If-elif-else
HOUR=$(date +%H)

if [ $HOUR -lt 12 ]; then
    echo "Good morning"
elif [ $HOUR -lt 18 ]; then
    echo "Good afternoon"
else
    echo "Good evening"
fi

# Nested if
if [ -f "$1" ]; then
    if [ -r "$1" ]; then
        echo "File is readable"
        cat "$1"
    else
        echo "File exists but is not readable"
    fi
else
    echo "File does not exist"
fi

Test Command Variations

#!/bin/bash

# Single bracket (POSIX compliant)
if [ -f file.txt ]; then
    echo "Using [ ]"
fi

# Double bracket (BASH extension - recommended)
if [[ -f file.txt ]]; then
    echo "Using [[ ]]"
fi

# Double parentheses for arithmetic
if (( 5 > 3 )); then
    echo "Using (( ))"
fi

# Test command (same as [ ])
if test -f file.txt; then
    echo "Using test command"
fi

# Advantages of [[ ]]
# - No word splitting
# - Pattern matching
# - Regex support
# - Safer for string comparison

STRING="hello world"
if [[ $STRING == "hello"* ]]; then
    echo "Pattern matches"
fi

if [[ $STRING =~ ^hello ]]; then
    echo "Regex matches"
fi

Case Statement

#!/bin/bash

# Basic case statement
read -p "Enter a color (red/green/blue): " COLOR

case $COLOR in
    red)
        echo "You chose red"
        ;;
    green)
        echo "You chose green"
        ;;
    blue)
        echo "You chose blue"
        ;;
    *)
        echo "Unknown color"
        ;;
esac

# Multiple patterns
case $1 in
    start|run|execute)
        echo "Starting..."
        ;;
    stop|halt|end)
        echo "Stopping..."
        ;;
    restart|reload)
        echo "Restarting..."
        ;;
    -h|--help)
        echo "Help message"
        ;;
    *)
        echo "Unknown command: $1"
        exit 1
        ;;
esac

# Pattern matching with case
FILE="$1"
case $FILE in
    *.txt)
        echo "Text file"
        ;;
    *.jpg|*.png|*.gif)
        echo "Image file"
        ;;
    *.tar.gz|*.tgz)
        echo "Compressed archive"
        ;;
    *)
        echo "Unknown file type"
        ;;
esac

Loops

For Loop

#!/bin/bash

# Basic for loop
for i in 1 2 3 4 5; do
    echo "Number: $i"
done

# Range with brace expansion
for i in {1..10}; do
    echo "Number: $i"
done

# Range with step
for i in {0..20..2}; do
    echo "Even number: $i"
done

# C-style for loop
for ((i=0; i<10; i++)); do
    echo "Counter: $i"
done

# Iterate over files
for file in *.txt; do
    echo "Processing: $file"
done

# Iterate over command output
for user in $(cat /etc/passwd | cut -d: -f1); do
    echo "User: $user"
done

# Iterate over array
FRUITS=("apple" "banana" "orange")
for fruit in "${FRUITS[@]}"; do
    echo "Fruit: $fruit"
done

# Iterate over command line arguments
for arg in "$@"; do
    echo "Argument: $arg"
done

While Loop

#!/bin/bash

# Basic while loop
COUNTER=0
while [ $COUNTER -lt 5 ]; do
    echo "Counter: $COUNTER"
    ((COUNTER++))
done

# Read file line by line
while IFS= read -r line; do
    echo "Line: $line"
done < file.txt

# Infinite loop
while true; do
    echo "Press Ctrl+C to stop"
    sleep 1
done

# While with condition from command
while ps aux | grep -q "[m]yprocess"; do
    echo "Process is running"
    sleep 5
done

# Read from pipe
cat file.txt | while read line; do
    echo "Processing: $line"
done

# While with multiple conditions
while [[ $COUNTER -lt 10 && $CONTINUE == "yes" ]]; do
    echo "Counter: $COUNTER"
    ((COUNTER++))
    read -p "Continue? (yes/no): " CONTINUE
done

Until Loop

#!/bin/bash

# Until loop (opposite of while)
COUNTER=0
until [ $COUNTER -ge 5 ]; do
    echo "Counter: $COUNTER"
    ((COUNTER++))
done

# Wait until file exists
until [ -f /tmp/ready.flag ]; do
    echo "Waiting for file..."
    sleep 2
done

# Wait until service is ready
until curl -s http://localhost:8080 > /dev/null; do
    echo "Waiting for service..."
    sleep 5
done

Loop Control

#!/bin/bash

# Break - exit loop
for i in {1..10}; do
    if [ $i -eq 5 ]; then
        echo "Breaking at 5"
        break
    fi
    echo "Number: $i"
done

# Continue - skip to next iteration
for i in {1..10}; do
    if [ $((i % 2)) -eq 0 ]; then
        continue  # Skip even numbers
    fi
    echo "Odd number: $i"
done

# Break out of nested loops
for i in {1..3}; do
    for j in {1..3}; do
        echo "$i,$j"
        if [[ $i -eq 2 && $j -eq 2 ]]; then
            break 2  # Break out of both loops
        fi
    done
done

Select Loop (Menu)

#!/bin/bash

# Create a menu
PS3="Select an option: "
options=("Option 1" "Option 2" "Option 3" "Quit")

select opt in "${options[@]}"; do
    case $opt in
        "Option 1")
            echo "You chose option 1"
            ;;
        "Option 2")
            echo "You chose option 2"
            ;;
        "Option 3")
            echo "You chose option 3"
            ;;
        "Quit")
            echo "Goodbye!"
            break
            ;;
        *)
            echo "Invalid option"
            ;;
    esac
done

Functions

Function Declaration and Calling

#!/bin/bash

# Method 1: function keyword
function greet {
    echo "Hello, World!"
}

# Method 2: Without function keyword (more portable)
greet2() {
    echo "Hello again!"
}

# Call functions
greet
greet2

# Function with parameters
greet_user() {
    local name=$1
    local age=$2
    echo "Hello, $name! You are $age years old."
}

greet_user "John" 30

# Function with return value
add_numbers() {
    local sum=$(($1 + $2))
    echo $sum  # Output to stdout
}

result=$(add_numbers 5 3)
echo "Sum: $result"

# Return exit status
is_valid() {
    if [ $1 -gt 0 ]; then
        return 0  # Success
    else
        return 1  # Failure
    fi
}

if is_valid 5; then
    echo "Valid"
else
    echo "Invalid"
fi

Local vs Global Variables

#!/bin/bash

# Global variable
GLOBAL_VAR="I am global"

my_function() {
    # Local variable (only visible in function)
    local LOCAL_VAR="I am local"
    
    # Modify global variable
    GLOBAL_VAR="Modified global"
    
    echo "Inside function:"
    echo "  Local: $LOCAL_VAR"
    echo "  Global: $GLOBAL_VAR"
}

echo "Before function:"
echo "  Global: $GLOBAL_VAR"

my_function

echo "After function:"
echo "  Global: $GLOBAL_VAR"
# echo "  Local: $LOCAL_VAR"  # This would be empty

Function Arguments and Special Variables

#!/bin/bash

print_args() {
    echo "Function name: $FUNCNAME"
    echo "Number of arguments: $#"
    echo "All arguments: $@"
    echo "All arguments (single string): $*"
    echo "First argument: $1"
    echo "Second argument: $2"
    echo "PID of script: $$"
}

print_args one two three four

# Variable number of arguments
sum_all() {
    local total=0
    for num in "$@"; do
        ((total += num))
    done
    echo $total
}

result=$(sum_all 1 2 3 4 5)
echo "Sum of all: $result"

Recursive Functions

#!/bin/bash

# Factorial function
factorial() {
    local n=$1
    if [ $n -le 1 ]; then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

result=$(factorial 5)
echo "5! = $result"

# Fibonacci
fibonacci() {
    local n=$1
    if [ $n -le 1 ]; then
        echo $n
    else
        local a=$(fibonacci $((n - 1)))
        local b=$(fibonacci $((n - 2)))
        echo $((a + b))
    fi
}

echo "Fibonacci(10) = $(fibonacci 10)"

Arrays

Indexed Arrays

#!/bin/bash

# Declare array
declare -a FRUITS

# Initialize array - multiple ways
FRUITS=("apple" "banana" "orange")
COLORS=(red green blue yellow)
NUMBERS=(1 2 3 4 5)

# Add elements
FRUITS+=("grape")
FRUITS[4]="mango"

# Access elements
echo "First fruit: ${FRUITS[0]}"
echo "Third fruit: ${FRUITS[2]}"
echo "Last fruit: ${FRUITS[-1]}"

# All elements
echo "All fruits: ${FRUITS[@]}"
echo "All fruits: ${FRUITS[*]}"

# Number of elements
echo "Array length: ${#FRUITS[@]}"

# Length of specific element
echo "Length of first fruit: ${#FRUITS[0]}"

# Array indices
echo "Indices: ${!FRUITS[@]}"

# Loop through array
for fruit in "${FRUITS[@]}"; do
    echo "Fruit: $fruit"
done

# Loop with indices
for i in "${!FRUITS[@]}"; do
    echo "Index $i: ${FRUITS[$i]}"
done

# Slice array
echo "Elements 1-3: ${FRUITS[@]:1:3}"

# Remove element
unset FRUITS[2]
echo "After unset: ${FRUITS[@]}"

# Copy array
BACKUP=("${FRUITS[@]}")

# Clear array
unset FRUITS

Associative Arrays (Hash/Dictionary)

#!/bin/bash

# Declare associative array
declare -A CAPITALS

# Initialize
CAPITALS=(
    [USA]="Washington DC"
    [UK]="London"
    [France]="Paris"
    [Japan]="Tokyo"
)

# Add elements
CAPITALS[Germany]="Berlin"
CAPITALS[Italy]="Rome"

# Access elements
echo "Capital of USA: ${CAPITALS[USA]}"
echo "Capital of Japan: ${CAPITALS[Japan]}"

# Check if key exists
if [[ -v CAPITALS[USA] ]]; then
    echo "USA key exists"
fi

# All keys
echo "Countries: ${!CAPITALS[@]}"

# All values
echo "Capitals: ${CAPITALS[@]}"

# Number of elements
echo "Number of countries: ${#CAPITALS[@]}"

# Loop through associative array
for country in "${!CAPITALS[@]}"; do
    echo "$country: ${CAPITALS[$country]}"
done

# Remove element
unset CAPITALS[UK]

Array Operations

#!/bin/bash

# Read file into array
mapfile -t LINES < file.txt
# or
readarray -t LINES < file.txt

# Split string into array
IFS=',' read -ra ITEMS <<< "apple,banana,orange"
echo "${ITEMS[@]}"

# Join array into string
FRUITS=("apple" "banana" "orange")
IFS=',' 
JOINED="${FRUITS[*]}"
echo "Joined: $JOINED"

# Sort array
NUMBERS=(5 2 8 1 9)
IFS=$'\n' SORTED=($(sort -n <<< "${NUMBERS[*]}"))
echo "Sorted: ${SORTED[@]}"

# Unique values
DUPLICATES=(1 2 2 3 3 3 4)
UNIQUE=($(printf "%s\n" "${DUPLICATES[@]}" | sort -u))
echo "Unique: ${UNIQUE[@]}"

# Search in array
FRUITS=("apple" "banana" "orange")
SEARCH="banana"
for fruit in "${FRUITS[@]}"; do
    if [ "$fruit" = "$SEARCH" ]; then
        echo "Found: $SEARCH"
        break
    fi
done

String Manipulation

String Length

#!/bin/bash

STRING="Hello, World!"
echo "Length: ${#STRING}"

# Length of variable
NAME="John Doe"
echo "Name length: ${#NAME}"

Substring Extraction

#!/bin/bash

STRING="Hello, World!"

# Substring: ${string:position:length}
echo "${STRING:0:5}"      # Hello
echo "${STRING:7}"        # World!
echo "${STRING:7:5}"      # World
echo "${STRING: -6}"      # World! (space before minus is important)
echo "${STRING: -6:5}"    # World

String Replacement

#!/bin/bash

STRING="Hello World, Hello Universe"

# Replace first occurrence
echo "${STRING/Hello/Hi}"           # Hi World, Hello Universe

# Replace all occurrences
echo "${STRING//Hello/Hi}"          # Hi World, Hi Universe

# Replace at beginning
echo "${STRING/#Hello/Hi}"          # Hi World, Hello Universe

# Replace at end
echo "${STRING/%Universe/Galaxy}"   # Hello World, Hello Galaxy

# Delete pattern
echo "${STRING//Hello/}"            # World,  Universe

Case Conversion

#!/bin/bash

STRING="Hello World"

# To uppercase
echo "${STRING^^}"          # HELLO WORLD
echo "${STRING^^[aeiou]}"   # HEllO WOrld (specific chars)

# To lowercase
echo "${STRING,,}"          # hello world
echo "${STRING,,[HW]}"      # hello world (specific chars)

# Capitalize first letter
echo "${STRING^}"           # Hello World

# Uncapitalize first letter
echo "${STRING,}"           # hello World

String Trimming

#!/bin/bash

STRING="   Hello World   "

# Trim leading whitespace
STRING="${STRING#"${STRING%%[![:space:]]*}"}"

# Trim trailing whitespace
STRING="${STRING%"${STRING##*[![:space:]]}"}"

# Or use external tools
STRING=$(echo "   Hello World   " | xargs)
echo "Trimmed: '$STRING'"

Pattern Matching and Removal

#!/bin/bash

FILENAME="example.tar.gz"

# Remove shortest match from beginning
echo "${FILENAME#*.}"       # tar.gz

# Remove longest match from beginning
echo "${FILENAME##*.}"      # gz

# Remove shortest match from end
echo "${FILENAME%.*}"       # example.tar

# Remove longest match from end
echo "${FILENAME%%.*}"      # example

# Extract file extension
EXT="${FILENAME##*.}"
echo "Extension: $EXT"

# Extract filename without extension
NAME="${FILENAME%.*}"
echo "Name: $NAME"

# Extract path
FULLPATH="/path/to/file.txt"
DIR="${FULLPATH%/*}"
FILE="${FULLPATH##*/}"
echo "Directory: $DIR"
echo "File: $FILE"

String Concatenation and Splitting

#!/bin/bash

# Concatenation
FIRST="Hello"
LAST="World"
FULL="$FIRST $LAST"
echo $FULL

# Append to string
STRING="Hello"
STRING="${STRING} World"
STRING+=" Again"
echo $STRING

# Split string
IFS=',' read -ra PARTS <<< "apple,banana,orange"
for part in "${PARTS[@]}"; do
    echo "Part: $part"
done

# Join strings
PARTS=("apple" "banana" "orange")
IFS=',' JOINED="${PARTS[*]}"
echo "Joined: $JOINED"

String Comparison

#!/bin/bash

STR1="hello"
STR2="world"

# Equality
if [ "$STR1" = "$STR2" ]; then
    echo "Strings are equal"
fi

# Inequality
if [ "$STR1" != "$STR2" ]; then
    echo "Strings are different"
fi

# Lexicographic comparison
if [[[ "$STR1" < "$STR2" ]]; then
    echo "$STR1 comes before $STR2"
fi

# Check if string contains substring
if [[ "$STR1" == *"ell"* ]]; then
    echo "Contains 'ell'"
fi

# Check if string starts with
if [[ "$STR1" == "hel"* ]]; then
    echo "Starts with 'hel'"
fi

# Check if string ends with
if [[ "$STR1" == *"lo" ]]; then
    echo "Ends with 'lo'"
fi

Input/Output & Redirection

Output

#!/bin/bash

# Print to stdout
echo "Hello World"
echo -n "No newline"
echo -e "Escape sequences:\n\tTab and newline"

# Printf (more control)
printf "Name: %s, Age: %d\n" "John" 30
printf "%.2f\n" 3.14159

# Print to stderr
echo "Error message" >&2
printf "Error: %s\n" "Something went wrong" >&2

Input

#!/bin/bash

# Simple read
read -p "Enter your name: " NAME
echo "Hello, $NAME"

# Read with timeout
read -t 5 -p "Enter something (5 seconds): " INPUT

# Read password (no echo)
read -sp "Enter password: " PASSWORD
echo  # New line after password

# Read into array
read -a WORDS <<< "word1 word2 word3"
echo "${WORDS[@]}"

# Read line by line
while IFS= read -r line; do
    echo "Line: $line"
done < file.txt

# Read with custom delimiter
IFS=',' read -r NAME AGE CITY <<< "John,30,New York"
echo "Name: $NAME, Age: $AGE, City: $CITY"

Redirection

#!/bin/bash

# Redirect stdout to file (overwrite)
echo "Hello" > output.txt

# Redirect stdout to file (append)
echo "World" >> output.txt

# Redirect stderr to file
command 2> error.log

# Redirect both stdout and stderr
command > output.txt 2>&1
# or (BASH 4+)
command &> output.txt

# Redirect stdout and stderr separately
command > output.txt 2> error.txt

# Discard output
command > /dev/null
command 2> /dev/null
command &> /dev/null

# Here document
cat << EOF > file.txt
Line 1
Line 2
Line 3
EOF

# Here document with variable expansion disabled
cat << 'EOF' > file.txt
$HOME will not be expanded
EOF

# Here string
grep "pattern" <<< "string to search"

# Read from file
while read line; do
    echo $line
done < input.txt

# Pipe
cat file.txt | grep "pattern" | sort | uniq

File Descriptors

#!/bin/bash

# Standard file descriptors:
# 0 = stdin
# 1 = stdout
# 2 = stderr

# Redirect file descriptor
exec 3> output.txt
echo "Goes to fd 3" >&3
exec 3>&-  # Close fd 3

# Save and restore stdout
exec 3>&1        # Save stdout
exec > log.txt   # Redirect stdout
echo "To log"
exec 1>&3        # Restore stdout
exec 3>&-        # Close fd 3

# Swap stdout and stderr
command 3>&1 1>&2 2>&3

# Copy file descriptor
exec 3>&1  # fd 3 copies stdout

Process Substitution

#!/bin/bash

# Compare output of two commands
diff <(ls dir1) <(ls dir2)

# Use command output as input file
while read line; do
    echo "Line: $line"
done < <(cat file1 file2)

# Multiple inputs
paste <(seq 1 5) <(seq 6 10)

File Operations

File Testing

#!/bin/bash

FILE="/path/to/file.txt"

# Check if file exists
if [ -e "$FILE" ]; then
    echo "File exists"
fi

# Check if regular file
if [ -f "$FILE" ]; then
    echo "Is a regular file"
fi

# Check if directory
if [ -d "$FILE" ]; then
    echo "Is a directory"
fi

# Check if readable
if [ -r "$FILE" ]; then
    echo "File is readable"
fi

# Check if writable
if [ -w "$FILE" ]; then
    echo "File is writable"
fi

# Check if executable
if [ -x "$FILE" ]; then
    echo "File is executable"
fi

# Check if not empty
if [ -s "$FILE" ]; then
    echo "File is not empty"
fi

# Compare file modification times
if [ file1.txt -nt file2.txt ]; then
    echo "file1.txt is newer than file2.txt"
fi

File Operations

#!/bin/bash

# Create file
touch newfile.txt

# Create multiple files
touch file{1..5}.txt

# Copy file
cp source.txt destination.txt

# Copy with backup
cp -b source.txt destination.txt

# Move/rename file
mv oldname.txt newname.txt

# Remove file
rm file.txt

# Remove multiple files
rm file1.txt file2.txt file3.txt

# Remove with pattern
rm *.tmp

# Remove directory and contents
rm -rf directory/

# Create directory
mkdir newdir

# Create nested directories
mkdir -p path/to/nested/dir

# Remove empty directory
rmdir emptydir

# Find files
find /path -name "*.txt"
find . -type f -mtime -7  # Modified in last 7 days
find . -type f -size +10M  # Larger than 10MB

# Check file size
SIZE=$(stat -f%z "$FILE")  # macOS
SIZE=$(stat -c%s "$FILE")  # Linux
echo "File size: $SIZE bytes"

# Get file modification time
MTIME=$(stat -f%m "$FILE")  # macOS
MTIME=$(stat -c%Y "$FILE")  # Linux

Reading and Writing Files

#!/bin/bash

# Write to file
cat > file.txt << EOF
Line 1
Line 2
Line 3
EOF

# Append to file
cat >> file.txt << EOF
Line 4
Line 5
EOF

# Read entire file into variable
CONTENT=$(<file.txt)
echo "$CONTENT"

# Read file line by line
while IFS= read -r line; do
    echo "Processing: $line"
done < file.txt

# Read file into array
mapfile -t LINES < file.txt

# Count lines
LINE_COUNT=$(wc -l < file.txt)

# Process CSV
while IFS=',' read -r col1 col2 col3; do
    echo "Col1: $col1, Col2: $col2, Col3: $col3"
done < data.csv

File Permissions

#!/bin/bash

# Change permissions (symbolic)
chmod u+x script.sh        # Add execute for user
chmod g-w file.txt         # Remove write for group
chmod o+r file.txt         # Add read for others
chmod a+x script.sh        # Add execute for all

# Change permissions (numeric)
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod 600 private.txt      # rw-------

# Change ownership
chown user:group file.txt
chown -R user:group directory/

# Get file permissions
PERMS=$(stat -f%p file.txt)  # macOS
PERMS=$(stat -c%a file.txt)  # Linux

Temporary Files

#!/bin/bash

# Create temporary file
TMPFILE=$(mktemp)
echo "data" > "$TMPFILE"
# Use the file
rm "$TMPFILE"

# Create temporary directory
TMPDIR=$(mktemp -d)
# Use the directory
rm -rf "$TMPDIR"

# Automatic cleanup with trap
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT
# File will be deleted when script exits

# Create temp file in specific directory
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)

Process Management

Running Commands

#!/bin/bash

# Run command and wait for completion
sleep 5

# Run command in background
sleep 10 &
BG_PID=$!
echo "Background process PID: $BG_PID"

# Wait for background process
wait $BG_PID
echo "Process completed with status: $?"

# Run multiple commands in background
command1 &
command2 &
command3 &
wait  # Wait for all background jobs

# Check if process is running
if ps -p $BG_PID > /dev/null; then
    echo "Process is running"
fi

# Kill process
kill $BG_PID
# or force kill
kill -9 $BG_PID

Process Information

#!/bin/bash

# Get current PID
echo "My PID: $$"

# Get parent PID
echo "Parent PID: $PPID"

# List all processes
ps aux

# Find process by name
pgrep httpd
pidof httpd

# Get process info
ps -p $$ -o pid,ppid,cmd,%cpu,%mem

# Process tree
pstree $$

Job Control

#!/bin/bash

# Start job in background
sleep 100 &

# List background jobs
jobs

# Bring job to foreground
# fg %1

# Send job to background
# bg %1

# Kill job
# kill %1

# Disown job (detach from shell)
# disown %1

Signals and Traps

#!/bin/bash

# Trap signals
trap "echo 'Caught SIGINT'; exit" SIGINT SIGTERM

# Trap exit
trap "echo 'Script exiting'; cleanup" EXIT

# Function to cleanup
cleanup() {
    echo "Cleaning up..."
    rm -f /tmp/myapp.*
    kill $BG_PID 2>/dev/null
}

# Ignore signal
trap '' SIGINT

# Reset trap
trap - SIGINT

# Common signals:
# SIGINT  (2)  - Ctrl+C
# SIGTERM (15) - Termination request
# SIGKILL (9)  - Force kill (cannot be trapped)
# SIGHUP  (1)  - Hangup
# SIGQUIT (3)  - Ctrl+\

# Example: Prevent Ctrl+C during critical section
trap '' SIGINT
echo "Critical section - Ctrl+C disabled"
sleep 5
trap - SIGINT
echo "Ctrl+C enabled again"

Command Execution

#!/bin/bash

# Execute command and capture output
OUTPUT=$(ls -l)
echo "$OUTPUT"

# Execute command and capture exit status
ls /nonexistent > /dev/null 2>&1
STATUS=$?
echo "Exit status: $STATUS"

# Execute command only if previous succeeded
cd /tmp && ls -l

# Execute command only if previous failed
cd /nonexistent || echo "Failed to change directory"

# Always execute second command
cd /tmp ; ls -l

# Conditional execution chain
command1 && command2 && command3 || command4

# Subshell
(cd /tmp; ls -l)  # Directory change doesn't affect parent shell

# Command group
{ cd /tmp; ls -l; }  # Runs in current shell

Regular Expressions

Pattern Matching with [[ ]]

#!/bin/bash

STRING="hello123world"

# Simple pattern matching
if [[ $STRING == *"123"* ]]; then
    echo "Contains 123"
fi

# Regex matching
if [[ $STRING =~ [0-9]+ ]]; then
    echo "Contains numbers"
    echo "Matched: ${BASH_REMATCH[0]}"
fi

# Extract groups
EMAIL="user@example.com"
if [[ $EMAIL =~ ^([a-zA-Z0-9._-]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})$ ]]; then
    echo "Valid email"
    echo "Username: ${BASH_REMATCH[1]}"
    echo "Domain: ${BASH_REMATCH[2]}"
    echo "TLD: ${BASH_REMATCH[3]}"
fi

Using grep

#!/bin/bash

# Basic grep
grep "pattern" file.txt

# Case-insensitive
grep -i "pattern" file.txt

# Invert match
grep -v "pattern" file.txt

# Count matches
grep -c "pattern" file.txt

# Show line numbers
grep -n "pattern" file.txt

# Recursive search
grep -r "pattern" directory/

# Extended regex
grep -E "pattern1|pattern2" file.txt
# or
egrep "pattern1|pattern2" file.txt

# Perl regex
grep -P "(?<=abc)def" file.txt

# Multiple patterns
grep -e "pattern1" -e "pattern2" file.txt

# Match whole word
grep -w "word" file.txt

# Files with matches
grep -l "pattern" *.txt

# Context lines
grep -A 2 "pattern" file.txt  # 2 lines after
grep -B 2 "pattern" file.txt  # 2 lines before
grep -C 2 "pattern" file.txt  # 2 lines before and after

Using sed

#!/bin/bash

# Substitute
sed 's/old/new/' file.txt           # First occurrence
sed 's/old/new/g' file.txt          # All occurrences
sed 's/old/new/2' file.txt          # Second occurrence

# Case-insensitive substitute
sed 's/old/new/gi' file.txt

# Delete lines
sed '/pattern/d' file.txt           # Delete matching lines
sed '1d' file.txt                   # Delete first line
sed '$d' file.txt                   # Delete last line
sed '1,5d' file.txt                 # Delete lines 1-5

# Print specific lines
sed -n '10p' file.txt               # Print line 10
sed -n '10,20p' file.txt            # Print lines 10-20
sed -n '/pattern/p' file.txt        # Print matching lines

# Multiple commands
sed -e 's/old/new/g' -e 's/foo/bar/g' file.txt
# or
sed 's/old/new/g; s/foo/bar/g' file.txt

# In-place editing
sed -i 's/old/new/g' file.txt       # Linux
sed -i '' 's/old/new/g' file.txt    # macOS

# Insert/append lines
sed '5i\New line before line 5' file.txt
sed '5a\New line after line 5' file.txt

# Change line
sed '5c\Replacement for line 5' file.txt

Using awk

#!/bin/bash

# Print specific columns
awk '{print $1, $3}' file.txt

# With custom delimiter
awk -F: '{print $1, $3}' /etc/passwd

# Pattern matching
awk '/pattern/ {print}' file.txt
awk '$3 > 100 {print $1, $3}' file.txt

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {print} END {print "End"}' file.txt

# Sum column
awk '{sum += $3} END {print sum}' file.txt

# Calculate average
awk '{sum += $3; count++} END {print sum/count}' file.txt

# Multiple conditions
awk '$1 == "John" && $3 > 100 {print}' file.txt

# Print line numbers
awk '{print NR, $0}' file.txt

# Print number of fields
awk '{print NF}' file.txt

# Format output
awk '{printf "%-20s %10s\n", $1, $2}' file.txt

Debugging & Error Handling

Set Options

#!/bin/bash

# Exit on error
set -e
# or
set -o errexit

# Exit on undefined variable
set -u
# or
set -o nounset

# Fail on pipe errors
set -o pipefail

# Print commands before execution
set -x
# or
set -o xtrace

# Combine options
set -euo pipefail

# Turn off
set +e
set +x

Error Handling

#!/bin/bash

# Check exit status
if command; then
    echo "Success"
else
    echo "Failed with status: $?"
fi

# Command || fallback
cd /nonexistent || { echo "Failed to cd"; exit 1; }

# Try-catch style
{
    command1
    command2
    command3
} || {
    echo "One of the commands failed"
    exit 1
}

# Custom error function
error_exit() {
    echo "Error: $1" >&2
    exit "${2:-1}"
}

# Usage
[ -f file.txt ] || error_exit "File not found" 2

# Function with error handling
safe_command() {
    if ! command "$@"; then
        echo "Command failed: $*" >&2
        return 1
    fi
}

# Error on undefined variable access
set -u
# This will cause error:
# echo $UNDEFINED_VAR

Debugging Techniques

#!/bin/bash

# Debug mode - entire script
bash -x script.sh

# Debug specific section
set -x
# code to debug
set +x

# Verbose mode
bash -v script.sh

# Check syntax without execution
bash -n script.sh

# Custom debug function
DEBUG=true

debug() {
    if [ "$DEBUG" = true ]; then
        echo "[DEBUG] $*" >&2
    fi
}

debug "Variable value: $VAR"

# Print line numbers
PS4='Line $LINENO: '
set -x

# Trace function calls
set -o functrace
trap 'echo "Calling ${FUNCNAME[0]}"' DEBUG

Logging

#!/bin/bash

LOGFILE="/var/log/myscript.log"

log() {
    local level=$1
    shift
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOGFILE"
}

log "INFO" "Script started"
log "ERROR" "Something went wrong"
log "DEBUG" "Variable: $VAR"

# Log everything
exec > >(tee -a "$LOGFILE")
exec 2>&1

# Syslog
logger -t myscript "Log message"

# Log with levels
log_info() { log "INFO" "$@"; }
log_error() { log "ERROR" "$@"; }
log_debug() { log "DEBUG" "$@"; }

Best Practices

Script Template

#!/usr/bin/env bash
#
# Script: example.sh
# Description: Brief description
# Author: Your Name
# Date: 2025-10-31
# Version: 1.0
#
# Usage: ./example.sh [options] arguments
#

set -euo pipefail

# Constants
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly VERSION="1.0.0"

# Default values
VERBOSE=false
DRY_RUN=false

# Functions
usage() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] ARGUMENTS

Description of what this script does.

OPTIONS:
    -h, --help      Show this help message
    -v, --verbose   Verbose output
    -n, --dry-run   Dry run mode
    -V, --version   Show version

EXAMPLES:
    $SCRIPT_NAME -v input.txt
    $SCRIPT_NAME --dry-run file1 file2

EOF
    exit 0
}

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}

error() {
    log "ERROR: $*"
    exit 1
}

# Parse arguments
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                usage
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -n|--dry-run)
                DRY_RUN=true
                shift
                ;;
            -V|--version)
                echo "$SCRIPT_NAME version $VERSION"
                exit 0
                ;;
            -*)
                error "Unknown option: $1"
                ;;
            *)
                # Positional argument
                break
                ;;
        esac
    done
}

# Main logic
main() {
    parse_args "$@"
    
    # Your code here
    log "Script started"
    
    if [ "$VERBOSE" = true ]; then
        log "Verbose mode enabled"
    fi
    
    if [ "$DRY_RUN" = true ]; then
        log "Dry run mode enabled"
    fi
    
    log "Script completed"
}

# Run main function
main "$@"

Coding Standards

Follow these guidelines:

Common Pitfalls

#!/bin/bash

# ❌ BAD: Unquoted variables
rm -rf $DIR/*  # Dangerous if DIR is empty

# ✓ GOOD: Quoted variables
rm -rf "${DIR:?DIR not set}"/*

# ❌ BAD: Not checking command existence
grep pattern file

# ✓ GOOD: Check command exists
if command -v grep >/dev/null 2>&1; then
    grep pattern file
else
    echo "grep not found" >&2
    exit 1
fi

# ❌ BAD: Parsing ls output
for file in $(ls *.txt); do
    echo "$file"
done

# ✓ GOOD: Use globbing
for file in *.txt; do
    [ -f "$file" ] || continue
    echo "$file"
done

# ❌ BAD: Using 'cd' without checking
cd /some/directory
ls -l

# ✓ GOOD: Check 'cd' success
cd /some/directory || exit 1
ls -l

# ❌ BAD: Word splitting issues
VAR="hello world"
echo $VAR  # Prints: hello world (two arguments)

# ✓ GOOD: Preserve spaces
echo "$VAR"  # Prints: hello world (one argument)

# ❌ BAD: Ignoring exit status
command
echo "Done"

# ✓ GOOD: Check exit status
if command; then
    echo "Done"
else
    echo "Failed" >&2
    exit 1
fi

Security Considerations

#!/bin/bash

# Use absolute paths or validate PATH
PATH="/usr/local/bin:/usr/bin:/bin"

# Validate input
validate_input() {
    local input=$1
    if [[ ! $input =~ ^[a-zA-Z0-9_-]+$ ]]; then
        error "Invalid input: $input"
    fi
}

# Don't trust user input in commands
# ❌ BAD
eval "echo $USER_INPUT"

# ✓ GOOD
echo "$USER_INPUT"

# Use temporary files safely
TMPFILE=$(mktemp) || error "Cannot create temp file"
trap "rm -f $TMPFILE" EXIT

# Check file before operations
if [ -f "$FILE" ] && [ -r "$FILE" ]; then
    cat "$FILE"
fi

# Avoid running as root when possible
if [ "$EUID" -eq 0 ]; then
    error "Do not run this script as root"
fi

# Sanitize filenames
sanitize_filename() {
    echo "$1" | tr -cd '[:alnum:]._-'
}

Advanced Topics

Named Pipes (FIFOs)

#!/bin/bash

# Create named pipe
PIPE="/tmp/mypipe"
mkfifo "$PIPE"

# Writer (in background)
(
    for i in {1..5}; do
        echo "Message $i" > "$PIPE"
        sleep 1
    done
) &

# Reader
while read line; do
    echo "Received: $line"
done < "$PIPE"

# Cleanup
rm "$PIPE"

Process Substitution

#!/bin/bash

# Compare two command outputs
diff <(ls dir1) <(ls dir2)

# Read from multiple sources
while read line; do
    echo "Line: $line"
done < <(cat file1 file2 file3)

# Tee to multiple files
echo "data" | tee >(process1) >(process2) > /dev/null

Co-processes

#!/bin/bash

# Start co-process
coproc bc -l

# Send commands
echo "10 + 20" >&${COPROC[1]}
read result <&${COPROC[0]}
echo "Result: $result"

# Close co-process
echo "quit" >&${COPROC[1]}

Parallel Execution

#!/bin/bash

# Using xargs
cat files.txt | xargs -P 4 -I {} process_file {}

# Using GNU parallel
parallel -j 4 process_file ::: file1 file2 file3 file4

# Manual parallel with background jobs
MAX_JOBS=4
job_count=0

for file in *.txt; do
    process_file "$file" &
    ((job_count++))
    
    if (( job_count >= MAX_JOBS )); then
        wait -n  # Wait for any job
        ((job_count--))
    fi
done

wait  # Wait for remaining jobs

Advanced Parameter Expansion

#!/bin/bash

# Default values
echo "${VAR:-default}"      # Use default if VAR is unset
echo "${VAR:=default}"      # Assign default if VAR is unset
echo "${VAR:?error}"        # Error if VAR is unset
echo "${VAR:+alternate}"    # Use alternate if VAR is set

# Array operations
ARRAY=(one two three)
echo "${ARRAY[@]^}"         # Capitalize first letter
echo "${ARRAY[@]^^}"        # Uppercase all
echo "${ARRAY[@]/o/0}"      # Replace o with 0

# Indirect expansion
VAR="VALUE"
REF="VAR"
echo "${!REF}"              # Prints: VALUE

# Variable name list
declare -A HASH
HASH[key1]="value1"
echo "${!HASH[@]}"          # Prints keys

Script Optimization

#!/bin/bash

# Use built-ins instead of external commands
# ❌ Slow
LENGTH=$(echo "$STRING" | wc -c)

# ✓ Fast
LENGTH=${#STRING}

# Avoid unnecessary subshells
# ❌ Slow
result=$(cat file | grep pattern | wc -l)

# ✓ Fast
result=$(grep -c pattern file)

# Use [[ ]] instead of [ ]
# [[ ]] is a built-in and faster

# Read files efficiently
# ❌ Slow - creates subshell per line
cat file | while read line; do
    echo "$line"
done

# ✓ Fast
while read line; do
    echo "$line"
done < file

# Batch operations
# ❌ Slow
for file in *.txt; do
    mv "$file" "$file.bak"
done

# ✓ Fast (if rename available)
rename 's/\.txt$/.txt.bak/' *.txt

Real-World Examples

Backup Script

#!/bin/bash
set -euo pipefail

# Configuration
SOURCE_DIR="/var/www"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
RETENTION_DAYS=7

# Create backup
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}

log "Starting backup of $SOURCE_DIR"

# Create backup directory if needed
mkdir -p "$BACKUP_DIR"

# Create archive
tar czf "${BACKUP_DIR}/${BACKUP_FILE}" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

# Verify backup
if [ -f "${BACKUP_DIR}/${BACKUP_FILE}" ]; then
    SIZE=$(du -h "${BACKUP_DIR}/${BACKUP_FILE}" | cut -f1)
    log "Backup completed: ${BACKUP_FILE} (${SIZE})"
else
    log "ERROR: Backup failed!"
    exit 1
fi

# Remove old backups
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete
log "Removed backups older than $RETENTION_DAYS days"

log "Backup process completed"

Log Analyzer

#!/bin/bash
set -euo pipefail

LOGFILE="${1:-/var/log/apache2/access.log}"

echo "=== Log Analysis for $LOGFILE ==="
echo

# Total requests
TOTAL=$(wc -l < "$LOGFILE")
echo "Total requests: $TOTAL"

# Unique visitors (IP addresses)
UNIQUE_IPS=$(awk '{print $1}' "$LOGFILE" | sort -u | wc -l)
echo "Unique IPs: $UNIQUE_IPS"

# Top 10 IP addresses
echo
echo "Top 10 IP addresses:"
awk '{print $1}' "$LOGFILE" | sort | uniq -c | sort -rn | head -10

# Top 10 requested pages
echo
echo "Top 10 requested pages:"
awk '{print $7}' "$LOGFILE" | sort | uniq -c | sort -rn | head -10

# HTTP status codes
echo
echo "HTTP status codes:"
awk '{print $9}' "$LOGFILE" | sort | uniq -c | sort -rn

# Requests by hour
echo
echo "Requests by hour:"
awk '{print $4}' "$LOGFILE" | cut -d: -f2 | sort | uniq -c

# 404 errors
echo
echo "404 errors:"
awk '$9 == 404 {print $7}' "$LOGFILE" | sort | uniq -c | sort -rn | head -10

System Health Check

#!/bin/bash
set -euo pipefail

ALERT_THRESHOLD=80

check_disk_space() {
    echo "=== Disk Space ==="
    df -h | grep -v "tmpfs"
    
    # Alert if over threshold
    df -h | grep -v "tmpfs" | awk '{print $5}' | sed 's/%//' | while read usage; do
        if [ "$usage" -gt "$ALERT_THRESHOLD" ]; then
            echo "WARNING: Disk usage over ${ALERT_THRESHOLD}%"
        fi
    done
    echo
}

check_memory() {
    echo "=== Memory Usage ==="
    free -h
    echo
}

check_cpu() {
    echo "=== CPU Load ==="
    uptime
    echo
}

check_services() {
    echo "=== Service Status ==="
    for service in httpd mysql sshd; do
        if systemctl is-active --quiet "$service"; then
            echo "$service: Running"
        else
            echo "$service: NOT RUNNING"
        fi
    done
    echo
}

check_failed_logins() {
    echo "=== Failed Login Attempts (last 10) ==="
    grep "Failed password" /var/log/secure 2>/dev/null | tail -10 || echo "No failed logins found"
    echo
}

# Run all checks
check_disk_space
check_memory
check_cpu
check_services
check_failed_logins

echo "Health check completed at $(date)"

Deployment Script

#!/bin/bash
set -euo pipefail

# Configuration
APP_NAME="myapp"
APP_DIR="/var/www/${APP_NAME}"
REPO_URL="git@github.com:user/myapp.git"
BRANCH="${1:-main}"
BACKUP_DIR="/backup/${APP_NAME}"

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}

error() {
    log "ERROR: $*"
    exit 1
}

# Check if running as correct user
[ "$(whoami)" = "www-data" ] || error "Must run as www-data user"

log "Starting deployment of $APP_NAME (branch: $BRANCH)"

# Backup current version
log "Creating backup"
mkdir -p "$BACKUP_DIR"
tar czf "${BACKUP_DIR}/backup_$(date +%Y%m%d_%H%M%S).tar.gz" -C "$(dirname "$APP_DIR")" "$(basename "$APP_DIR")"

# Pull latest code
log "Pulling latest code"
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"

# Install dependencies
log "Installing dependencies"
if [ -f "package.json" ]; then
    npm install --production
fi

if [ -f "requirements.txt" ]; then
    pip install -r requirements.txt
fi

# Run migrations
log "Running migrations"
if [ -f "manage.py" ]; then
    python manage.py migrate
fi

# Restart services
log "Restarting services"
systemctl restart "$APP_NAME"

# Health check
log "Performing health check"
sleep 5
if curl -sf http://localhost:8000/health > /dev/null; then
    log "Deployment successful!"
else
    error "Health check failed!"
fi

log "Deployment completed"

✓ BASH Guide Complete!

You now have a comprehensive reference for BASH shell scripting covering:

Additional Resources:

Comprehensive Guide to BASH Shell Script Programming
From basics to advanced techniques