Table of Contents
- Why Bash Scripting for Linux Admin?
- Prerequisites
- Anatomy of a Bash Script
- Variables and Data Types in Bash
- Control Structures: Conditionals and Loops
- Input Handling: Arguments and User Input
- Error Handling and Debugging
- Practical Examples of Admin Scripts
- Best Practices for Bash Scripting
- Conclusion
- References
Why Bash Scripting for Linux Admin?
Bash scripting is a cornerstone of Linux administration for several reasons:
- Portability: Bash is preinstalled on nearly all Linux/Unix systems, so your scripts work across distributions without extra dependencies.
- Simplicity: It uses familiar Linux commands (e.g.,
ls,grep,cp), so you don’t need to learn a new language. - Power: Combine commands with pipes (
|), redirects (>,>>), and logic to build complex workflows. - Efficiency: Automate repetitive tasks (e.g., daily backups, user provisioning) to save time and reduce errors.
Prerequisites
Before diving in, ensure you have:
- A Linux system (any distribution: Ubuntu, CentOS, Debian, etc.).
- Basic familiarity with Linux CLI commands (e.g.,
cd,mkdir,chmod). - A text editor (e.g.,
nano,vim, or VS Code with SSH). - Understanding of file permissions (e.g.,
chmod +xto make scripts executable).
Anatomy of a Bash Script
A basic Bash script has three core components: the shebang line, commands, and execution permissions. Let’s break it down with a simple “Hello World” example.
Example 1: Hello World Script
Create a file named hello.sh and add:
#!/bin/bash
# This is a comment: Print "Hello, Admin!" to the terminal
echo "Hello, Admin!"
Explanation:
- Shebang Line (
#!/bin/bash): Tells the system to run the script with the Bash interpreter. Always the first line. - Comments (
#): Lines starting with#are ignored by Bash. Use them to explain logic for readability. - Commands: The actual work (here,
echoprints text to the terminal).
Running the Script
To execute the script:
- Make it executable:
chmod +x hello.sh - Run it:
Output:./hello.shHello, Admin!
Variables and Data Types in Bash
Variables store data for reuse. Bash is loosely typed (no strict int/string), but you’ll mainly work with strings and numbers.
Defining Variables
Use VAR_NAME=value (no spaces around =):
#!/bin/bash
NAME="Alice"
AGE=30
echo "User: $NAME, Age: $AGE" # Use $ to access variables
Output:
User: Alice, Age: 30
Special Variables
Bash provides built-in variables for scripts:
$0: Name of the script (e.g.,./hello.sh→$0=hello.sh).$1, $2, ...: Command-line arguments (e.g.,./script.sh arg1 arg2→$1=arg1,$2=arg2).$#: Number of arguments.$?: Exit code of the last command (0 = success, non-zero = error).$USER: Current username.
Example using arguments:
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Number of arguments: $#"
Run with:
./args.sh "Linux Admin"
Output:
Script name: ./args.sh
First argument: Linux Admin
Number of arguments: 1
Control Structures: Conditionals and Loops
Control structures let you add logic to scripts (e.g., “if disk space is low, send an alert” or “for each user, check their home directory”).
Conditionals (if-else)
Use if-else to run commands based on conditions (e.g., file existence, numeric comparisons).
Syntax:
if [ condition ]; then
# Commands if condition is true
elif [ another_condition ]; then
# Commands if first condition is false, second is true
else
# Commands if all conditions are false
fi
Common Conditions:
-f file: True iffileexists and is a regular file.-d dir: True ifdirexists and is a directory.-z string: True ifstringis empty.$a -eq $b: Numeric equality (e.g., 5 -eq 5).$a -gt $b: Numeric greater than (e.g., 10 -gt 5).string1 == string2: String equality (use=~for regex).
Example 2: Check Disk Space
#!/bin/bash
# Check if /home has less than 10GB free space
FREE_SPACE=$(df -P /home | awk 'NR==2 {print $4}') # Get free blocks (1K)
THRESHOLD=10485760 # 10GB = 10*1024*1024 KB
if [ $FREE_SPACE -lt $THRESHOLD ]; then
echo "Warning: /home has less than 10GB free space!"
else
echo "/home has enough free space."
fi
Explanation:
df -P /homelists disk usage for/homein POSIX format.awk 'NR==2 {print $4}'extracts the 4th column (free blocks) from the second line ofdfoutput.-ltchecks if free space is “less than” the threshold.
Loops
Loops repeat commands for a set of items (e.g., files, users, numbers).
1. for Loop
Iterate over a list (files, arguments, or a range).
Example: Process log files
#!/bin/bash
# Compress all .log files in /var/log that are older than 7 days
LOG_DIR="/var/log"
for logfile in $LOG_DIR/*.log; do
# Check if file exists and is older than 7 days
if [ -f "$logfile" ] && [ $(find "$logfile" -mtime +7) ]; then
gzip "$logfile" # Compress the log file
echo "Compressed: $logfile"
fi
done
2. while Loop
Repeat until a condition is false (e.g., read lines from a file).
Example: Read user list from a file
#!/bin/bash
# Read usernames from users.txt and print their home directories
while IFS= read -r username; do
if id "$username" &>/dev/null; then # Check if user exists
echo "User $username: Home dir = $(getent passwd "$username" | cut -d: -f6)"
else
echo "User $username does not exist."
fi
done < "users.txt" # Input file: users.txt (one username per line)
Input Handling: Arguments and User Input
Scripts often need input from users or command-line arguments.
Command-Line Arguments
We covered special variables like $1 earlier. For scripts with many arguments, use getopts to parse flags (e.g., -u username -f file).
Example: User creation script with arguments
#!/bin/bash
# Usage: ./create_user.sh -u <username> -g <group>
while getopts "u:g:" opt; do
case $opt in
u) USERNAME="$OPTARG" ;; # -u sets USERNAME
g) GROUP="$OPTARG" ;; # -g sets GROUP
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
esac
done
# Validate inputs
if [ -z "$USERNAME" ] || [ -z "$GROUP" ]; then
echo "Error: -u (username) and -g (group) are required." >&2
exit 1
fi
# Create user and add to group
useradd -m "$USERNAME"
usermod -aG "$GROUP" "$USERNAME"
echo "Created user $USERNAME and added to group $GROUP."
Run with:
sudo ./create_user.sh -u "jane" -g "developers"
User Input with read
Use read to prompt the user for input interactively.
Example: Interactive script
#!/bin/bash
echo "Enter your name:"
read -r NAME # -r prevents backslash escapes
echo "Hello, $NAME! Enter your favorite Linux distro:"
read -r DISTRO
echo "Welcome, $NAME! You use $DISTRO. Nice choice!"
Error Handling and Debugging
Unchecked errors can break scripts. Use these techniques to make scripts robust.
Exit on Error with set -e
Add set -e at the top of your script to exit immediately if any command fails (non-zero exit code).
#!/bin/bash
set -e # Exit on any error
cp important_file /backup/ # If cp fails, script exits here
echo "Backup successful" # Only runs if cp succeeded
Check Exit Codes with $?
The $? variable holds the exit code of the last command (0 = success, 1-255 = error).
#!/bin/bash
cp file1.txt /tmp/
if [ $? -ne 0 ]; then # If exit code is not 0 (failure)
echo "Error: Failed to copy file1.txt" >&2 # >&2 sends error to stderr
exit 1 # Exit with non-zero code to indicate failure
fi
Debugging with set -x
Add set -x to print each command before execution (great for debugging).
#!/bin/bash
set -x # Enable debugging
USERNAME="bob"
echo "Creating user $USERNAME"
useradd "$USERNAME"
set +x # Disable debugging
echo "Done"
Practical Examples of Admin Scripts
Let’s build real-world scripts to solve common admin tasks.
1. Backup Automation Script
Automate backups of critical directories (e.g., /etc, /home) to a remote server or external drive.
#!/bin/bash
# Backup script: Archives /etc and /home, saves to /backups with timestamp
set -euo pipefail # Exit on error, unset var, or pipe failure
# Configuration
SOURCE_DIRS="/etc /home"
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/system_backup_$TIMESTAMP.tar.gz"
# Create backup dir if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Archive and compress the directories
echo "Creating backup: $BACKUP_FILE"
tar -czf "$BACKUP_FILE" $SOURCE_DIRS
# Verify backup size > 0
if [ -s "$BACKUP_FILE" ]; then
echo "Backup successful! Size: $(du -sh "$BACKUP_FILE" | awk '{print $1}')"
else
echo "Error: Backup file is empty!" >&2
exit 1
fi
# Optional: Delete backups older than 30 days
find "$BACKUP_DIR" -name "system_backup_*.tar.gz" -mtime +30 -delete
echo "Old backups (30+ days) deleted."
Usage: Run as root (to access /etc and /home):
sudo ./backup.sh
2. User Management Script
Bulk-create users from a CSV file and set up their home directories with default permissions.
#!/bin/bash
# Create users from CSV (format: username,group,shell)
set -euo pipefail
if [ $# -ne 1 ]; then
echo "Usage: $0 <user_list.csv>" >&2
exit 1
fi
CSV_FILE="$1"
# Check if CSV exists
if [ ! -f "$CSV_FILE" ]; then
echo "Error: File $CSV_FILE not found!" >&2
exit 1
fi
# Read CSV (skip header line if present)
tail -n +2 "$CSV_FILE" | while IFS=',' read -r username group shell; do
# Validate fields
if [ -z "$username" ] || [ -z "$group" ] || [ -z "$shell" ]; then
echo "Skipping invalid line: username=$username, group=$group, shell=$shell" >&2
continue
fi
# Create group if it doesn't exist
if ! getent group "$group" &>/dev/null; then
groupadd "$group"
echo "Created group: $group"
fi
# Create user with specified group and shell
if id "$username" &>/dev/null; then
echo "User $username already exists. Skipping."
else
useradd -m -g "$group" -s "$shell" "$username"
echo "Created user: $username (group: $group, shell: $shell)"
# Set initial password (expire on first login)
echo "$username:TempPass123!" | chpasswd
chage -d 0 "$username" # Force password change on first login
fi
done
echo "User creation complete."
CSV Example (users.csv):
username,group,shell
alice,developers,/bin/bash
bob,designers,/bin/zsh
charlie,devops,/bin/bash
Usage:
sudo ./create_users.sh users.csv
3. Log Rotation Script
Compress and archive old logs to save disk space (replace logrotate for custom setups).
#!/bin/bash
# Rotate /var/log/app.log: compress, rename, and truncate
set -euo pipefail
LOG_FILE="/var/log/app.log"
MAX_SIZE_MB=100 # Rotate when log reaches 100MB
BACKUP_DIR="/var/log/app_backups"
# Create backup dir if missing
mkdir -p "$BACKUP_DIR"
# Get current log size in MB
LOG_SIZE_MB=$(du -m "$LOG_FILE" | awk '{print $1}')
if [ "$LOG_SIZE_MB" -ge "$MAX_SIZE_MB" ]; then
echo "Log size $LOG_SIZE_MB MB ≥ $MAX_SIZE_MB MB. Rotating..."
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/app_$TIMESTAMP.log.gz"
# Compress the current log
gzip -c "$LOG_FILE" > "$BACKUP_FILE"
# Truncate the original log (keep permissions)
> "$LOG_FILE"
# Set ownership (match original log)
chown --reference="$LOG_FILE" "$BACKUP_FILE"
echo "Rotated log saved to: $BACKUP_FILE (Size: $(du -sh "$BACKUP_FILE" | awk '{print $1}'))"
else
echo "Log size $LOG_SIZE_MB MB < $MAX_SIZE_MB MB. No rotation needed."
fi
Usage: Add to cron to run daily:
# Edit crontab
crontab -e
# Add: 0 2 * * * /path/to/rotate_logs.sh # Run at 2 AM daily
4. System Monitoring Script
Check CPU, memory, and disk usage; send alerts if thresholds are breached (e.g., CPU > 90%).
#!/bin/bash
# Monitor system resources and alert on high usage
set -euo pipefail
# Thresholds (adjust as needed)
CPU_THRESHOLD=90 # %
MEM_THRESHOLD=90 # %
DISK_THRESHOLD=90 # %
ALERT_EMAIL="[email protected]"
# Check CPU usage (1-minute average)
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')
CPU_USAGE=$(printf "%.0f" "$CPU_USAGE") # Round to integer
# Check memory usage (used %)
MEM_USAGE=$(free | grep Mem | awk '{print $3/$2 * 100.0}' | cut -d. -f1)
# Check disk usage (root partition, used %)
DISK_USAGE=$(df -P / | awk 'NR==2 {print $5}' | sed 's/%//')
# Build alert message
ALERT_MSG=""
if [ "$CPU_USAGE" -ge "$CPU_THRESHOLD" ]; then
ALERT_MSG+="High CPU Usage: $CPU_USAGE% (Threshold: $CPU_THRESHOLD%)\n"
fi
if [ "$MEM_USAGE" -ge "$MEM_THRESHOLD" ]; then
ALERT_MSG+="High Memory Usage: $MEM_USAGE% (Threshold: $MEM_THRESHOLD%)\n"
fi
if [ "$DISK_USAGE" -ge "$DISK_THRESHOLD" ]; then
ALERT_MSG+="High Disk Usage: $DISK_USAGE% (Threshold: $DISK_THRESHOLD%)\n"
fi
# Send alert if any threshold is breached
if [ -n "$ALERT_MSG" ]; then
echo -e "System Alert on $(hostname) at $(date)\n\n$ALERT_MSG" | mail -s "URGENT: System Resource Alert" "$ALERT_EMAIL"
echo "Alert sent to $ALERT_EMAIL:"
echo -e "$ALERT_MSG"
else
echo "All resources within thresholds."
fi
Usage: Install mailutils first, then run:
sudo apt install mailutils # Debian/Ubuntu
./monitor_system.sh
Best Practices for Bash Scripting
To write maintainable, secure scripts:
-
Use
set -euo pipefail:-e: Exit on error.-u: Treat unset variables as errors.-o pipefail: Exit if any command in a pipe fails.
-
Comment Liberally: Explain why (not just what) the code does.
-
Avoid Hardcoded Values: Use variables for paths, thresholds, or emails (e.g.,
BACKUP_DIR="/backups"). -
Validate Inputs: Check if files exist, users exist, or arguments are provided.
-
Use Functions for Reusability:
# Example: Logging function log() { echo "[$(date +%Y%m%d_%H%M%S)] $1" } log "Starting backup..." -
Test Scripts: Run with
bash -n script.shto check for syntax errors before execution. -
Limit Privileges: Avoid running scripts as
rootunless necessary. Usesudofor specific commands instead. -
Secure Sensitive Data: Never hardcode passwords. Use environment variables or encrypted files.
Conclusion
Bash scripting is a superpower for Linux admins, turning tedious manual tasks into automated, reliable workflows. By mastering variables, control structures, and error handling, you can build scripts to handle backups, user management, log rotation, and system monitoring—saving time and reducing errors.
Start small: automate one task (e.g., a daily backup), then expand. Refer to the examples and best practices above, and don’t hesitate to debug with set -x when things go wrong. With practice, you’ll be writing robust scripts that make your admin life easier.
References
- GNU Bash Manual
- TLDP Bash Guide for Beginners
- Advanced Bash-Scripting Guide
- Bash Pitfalls (Avoid common mistakes)
- ShellCheck (Static analysis for Bash scripts)
- Book: Learning the Bash Shell by Cameron Newham (O’Reilly)