thelinuxvault guide

How to Write Bash Scripts that Automate Linux Admin Tasks

As a Linux system administrator, you’re no stranger to repetitive tasks: backing up logs, creating user accounts, monitoring disk space, or rotating log files. These tasks, while critical, can eat up hours of your day if done manually. Enter **Bash scripting**—a powerful tool that lets you automate these workflows, reduce human error, and reclaim time for more strategic work. Bash (Bourne Again Shell) is the default shell on most Linux distributions, making it a portable and accessible choice for automation. With Bash, you can chain together Linux commands, add logic (like conditionals and loops), and create reusable scripts to handle everything from simple file management to complex system monitoring. In this guide, we’ll walk through the fundamentals of Bash scripting for Linux admin tasks, from writing your first script to advanced topics like error handling and best practices. By the end, you’ll have the skills to automate routine tasks and build robust, maintainable scripts.

Table of Contents

  1. Why Bash Scripting for Linux Admin?
  2. Prerequisites
  3. Anatomy of a Bash Script
  4. Variables and Data Types in Bash
  5. Control Structures: Conditionals and Loops
  6. Input Handling: Arguments and User Input
  7. Error Handling and Debugging
  8. Practical Examples of Admin Scripts
  9. Best Practices for Bash Scripting
  10. Conclusion
  11. 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 +x to 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:

  1. Shebang Line (#!/bin/bash): Tells the system to run the script with the Bash interpreter. Always the first line.
  2. Comments (#): Lines starting with # are ignored by Bash. Use them to explain logic for readability.
  3. Commands: The actual work (here, echo prints text to the terminal).

Running the Script

To execute the script:

  1. Make it executable:
    chmod +x hello.sh  
  2. Run it:
    ./hello.sh  
    Output:
    Hello, 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 if file exists and is a regular file.
  • -d dir: True if dir exists and is a directory.
  • -z string: True if string is 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 /home lists disk usage for /home in POSIX format.
  • awk 'NR==2 {print $4}' extracts the 4th column (free blocks) from the second line of df output.
  • -lt checks 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:

  1. Use set -euo pipefail:

    • -e: Exit on error.
    • -u: Treat unset variables as errors.
    • -o pipefail: Exit if any command in a pipe fails.
  2. Comment Liberally: Explain why (not just what) the code does.

  3. Avoid Hardcoded Values: Use variables for paths, thresholds, or emails (e.g., BACKUP_DIR="/backups").

  4. Validate Inputs: Check if files exist, users exist, or arguments are provided.

  5. Use Functions for Reusability:

    # Example: Logging function  
    log() {  
      echo "[$(date +%Y%m%d_%H%M%S)] $1"  
    }  
    log "Starting backup..."  
  6. Test Scripts: Run with bash -n script.sh to check for syntax errors before execution.

  7. Limit Privileges: Avoid running scripts as root unless necessary. Use sudo for specific commands instead.

  8. 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