What Bash Scripting Is and Why It Matters for Server Administration

Bash stands for Bourne Again Shell. It is the default command-line interpreter on most Linux distributions and macOS. If you manage a server, understanding bash scripting means you can automate repetitive tasks that would otherwise consume hours of manual work. Rather than typing the same sequence of commands every morning, you write a script once and let the server execute it on a schedule or on demand.

Server administrators use bash scripts to automate backups, rotate log files, monitor disk space, deploy applications, and respond to system events. The scripts you write can range from a simple three-line file that clears old log entries to a complex multi-step deployment pipeline. The key is that once the script works correctly, you never need to remember the steps again.

This guide covers the building blocks you need to write useful bash scripts for real server tasks. You do not need to be a programmer to follow along. If you can read and write basic commands in a terminal, you have everything required to start scripting.

Your First Bash Script: The Basics You Must Know

Before writing any script, open a terminal and confirm bash is your active shell.

echo $SHELL

This prints something like /bin/bash. That confirms you are working with bash. Now create your first script file.

touch ~/first_script.sh

Open the file in a text editor such as nano or vim.

nano ~/first_script.sh

Every bash script should begin with a shebang line. This tells the operating system which interpreter to use to execute the file.

#!/bin/bash

Add a simple command below the shebang to test the script works.

#!/bin/bash

echo "Server report generated at $(date)"

Save the file and make it executable.

chmod +x ~/first_script.sh

Run the script.

./first_script.sh

You should see the date and time printed in the terminal. That is your first working bash script.

Variables: Storing and Reusing Information

Variables let you store information inside a script so you can use it later. There is no space around the equals sign when assigning a variable.

server_name="web-prod-01"
backup_dir="/var/backups/mysql"
days_to_keep=7

To use a variable, prefix the name with a dollar sign.

echo "Backing up server: $server_name"
echo "Destination: $backup_dir"
echo "Retention: $days_to_keep days"

Variable names should be descriptive. Use underscores to separate words. Avoid spaces in variable names. A common naming convention uses lowercase for user-defined variables and uppercase for system variables such as $HOME, $USER, and $PATH.

Command substitution lets you capture the output of a command into a variable. Wrap the command in either backticks or the preferred $( ) syntax.

current_date=$(date +%Y-%m-%d)
disk_usage=$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')
hostname=$(hostname)

These three examples capture the current date, the root filesystem usage percentage, and the server hostname. You can build these into a daily monitoring script that collects system state information automatically.

Conditional Logic: Making Decisions in Scripts

Scripts become powerful when they can make decisions. The if statement tests a condition and runs different commands depending on whether the condition is true or false.

if [ $disk_usage -gt 90 ]; then
    echo "WARNING: Disk usage is above 90%"
else
    echo "Disk usage is within normal range"
fi

The square brackets [ ] test conditions. Spaces inside the brackets are required. The -gt operator tests whether one number is greater than another. Common comparison operators include -eq (equal), -ne (not equal), -lt (less than), -le (less than or equal), -gt (greater than), and -ge (greater than or equal).

For string comparisons, use = or != inside single square brackets, or use double brackets which support more advanced patterns.

if [ $backup_status -eq 0 ]; then
    echo "Backup completed successfully"
elif [ $backup_status -eq 1 ]; then
    echo "Backup completed with warnings"
else
    echo "Backup failed"
fi

Testing whether a file or directory exists is a common requirement in server automation scripts.

if [ -f "/path/to/backup.tar.gz" ]; then
    echo "Backup file exists"
else
    echo "Backup file not found"
fi

Key file tests include -f (regular file exists), -d (directory exists), -r (readable), -w (writable), and -x (executable).

Loops: Repeating Actions Efficiently

Loops let you repeat a set of commands for each item in a list or while a condition holds true. The most common loop for server automation is a for loop that iterates over a list of items.

for service in nginx mysql php-fpm docker; do
    systemctl restart "$service"
    echo "Restarted $service"
done

This loop restarts each service in the list sequentially. You can build more dynamic lists using command substitution.

for domain in $(cat /etc/nginx/sites-enabled/*.conf | grep server_name | awk '{print $2}' | sort -u); do
    echo "Checking SSL certificate for $domain"
    certbot --nginx -d "$domain" --dry-run
done

The while loop runs as long as a condition is true. This is useful for monitoring a process or waiting for a service to become available.

counter=0
max_attempts=10

while [ $counter -lt $max_attempts ]; do
    if curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 | grep -q "200"; then
        echo "Service is up"
        break
    fi
    counter=$((counter + 1))
    echo "Waiting for service... attempt $counter of $max_attempts"
    sleep 5
done

The while loop above checks whether a web service returns a 200 status code. If it does, the loop breaks and prints a success message. If not, it waits five seconds and tries again, up to ten attempts total.

Functions: Organizing Reusable Logic

Functions group related commands together so you can call them by name. They make scripts easier to read, test, and reuse across different parts of a script or in multiple scripts.

check_disk_space() {
    usage=$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')
    if [ "$usage" -gt 80 ]; then
        echo "WARNING: Disk usage at ${usage}%"
        return 1
    else
        echo "Disk usage at ${usage}% - OK"
        return 0
    fi
}

Call the function by its name.

check_disk_space

Functions accept arguments just like command-line arguments. Inside the function, $1 is the first argument, $2 is the second, and so on. $# holds the argument count.

rotate_log() {
    log_file="$1"
    max_size_mb="$2"
    
    if [ ! -f "$log_file" ]; then
        echo "Log file $log_file not found"
        return 1
    fi
    
    size_mb=$(du -m "$log_file" | cut -f1)
    
    if [ "$size_mb" -gt "$max_size_mb" ]; then
        mv "$log_file" "${log_file}.$(date +%s)"
        gzip "${log_file}.$(date +%s)"
        echo "Rotated $log_file"
        return 0
    fi
}

You can call this function with different log files and size thresholds.

rotate_log /var/log/nginx/access.log 100
rotate_log /var/log/apache2/error.log 50

Processing Files and Directories in Bulk

Server automation often involves processing many files at once. A practical example is compressing log files older than a certain number of days.

find /var/log -name "*.log" -mtime +30 -exec gzip {} \;

This command finds all .log files in /var/log that have not been modified in the last 30 days and compresses them with gzip. The -exec flag runs the specified command on each file found. The \; at the end tells find where the command ends.

For more complex processing, combine find with a while read loop.

find /var/www -type f -name "*.php" -mtime -7 | while read filepath; do
    echo "Checking file: $filepath"
    php -l "$filepath"
done

This finds all PHP files modified in the last seven days and runs a syntax check on each one using php -l. Any file with a syntax error prints to the console.

Scheduling Scripts with Cron

A script only runs when you execute it unless you schedule it with cron. The cron daemon runs in the background and executes commands at specified intervals. Edit the crontab to add scheduled tasks.

crontab -e

Each cron entry has five time fields followed by the command.

# * * * * * command to execute
# - - - - -
# | | | | |
# | | | | +--- Day of week (0-6, Sunday = 0)
# | | | +----- Month (1-12)
# | | +------- Day of month (1-31)
# | +--------- Hour (0-23)
# +----------- Minute (0-59)

Common examples:

0 3 * * * /root/scripts/daily_backup.sh      # Run at 3 AM every day
0 */6 * * * /root/scripts/check_services.sh  # Run every 6 hours
30 2 * * 0 /root/scripts/weekly_cleanup.sh   # Run at 2:30 AM every Sunday
@reboot /root/scripts/startup_tasks.sh       # Run on system boot

Redirect both standard output and standard error to a log file so you can review results later.

0 3 * * * /root/scripts/daily_backup.sh >> /var/log/backup.log 2>&1

The >> appends output to the log file. 2>&1 redirects standard error to the same destination as standard output. If you want to learn more about scheduling tasks, a guide to cron jobs on Linux servers covers the syntax and practical examples in detail.

Debugging Bash Scripts

When a script does not behave as expected, bash provides tools to help you find the problem. The -x flag prints each command and its arguments as the script runs.

bash -x ~/your_script.sh

You can also enable debug mode inside the script itself for specific sections.

#!/bin/bash

set -x  # Debug mode on from here

for file in /var/log/*.log; do
    echo "Processing $file"
done

set +x  # Debug mode off

Other useful flags include set -e which causes the script to exit immediately if any command fails, and set -u which treats unset variables as errors.

#!/bin/bash

set -e
set -u
set -o pipefail

These three settings make scripts safer and failures more obvious. Always include them at the top of production scripts.

Exit Codes and Error Handling

Every command returns an exit code when it finishes. Zero means success. Any non-zero value means failure. Bash stores this value in the special variable $?.

mysql -u root -psecretpass -e "SELECT 1;"
if [ $? -eq 0 ]; then
    echo "Database connection successful"
else
    echo "Database connection failed"
fi

Check exit codes after commands that might fail, such as database connections, file operations, or network calls. This lets you handle errors gracefully instead of continuing as if nothing went wrong.

Real-World Example: Daily Server Health Report

Combining everything covered in this guide, here is a practical script that generates a daily server health report and emails it if any metric is concerning.

#!/bin/bash

set -e
set -u

report_file="/tmp/health_report_$(date +%Y%m%d).txt"
alert=0

{
    echo "=== Server Health Report: $(hostname) ==="
    echo "Generated: $(date)"
    echo ""
    echo "--- Disk Usage ---"
    df -h | grep -v "tmpfs\|devtmpfs\|loop"
    echo ""
    echo "--- Memory Usage ---"
    free -h
    echo ""
    echo "--- Top 5 Processes by Memory ---"
    ps aux --sort=-%mem | head -6
    echo ""
    echo "--- Failed Login Attempts ---"
    if [ -f /var/log/auth.log ]; then
        grep "Failed password" /var/log/auth.log | tail -5
    elif [ -f /var/log/secure ]; then
        grep "Failed password" /var/log/secure | tail -5
    fi
    echo ""
    echo "--- Service Status ---"
    for service in nginx mysql php-fpm; do
        if systemctl is-active --quiet "$service"; then
            echo "$service: RUNNING"
        else
            echo "$service: NOT RUNNING"
            alert=1
        fi
    done
} > "$report_file"

if [ $alert -eq 1 ]; then
    mail -s "ALERT: Server Health Issues on $(hostname)" [email protected] < "$report_file"
else
    echo "All systems healthy. Report saved to $report_file"
fi

Schedule this script to run every morning before you start work. You get a consistent picture of server health without manually checking each metric. Any service that is down triggers an alert email. The report file provides a historical log you can refer back to if needed.

For more advanced deployment automation using bash scripts, you can learn how to write a bash script that deploys your application reliably. Once you are comfortable with bash basics, combining scripting with version control and CI/CD pipelines becomes a natural next step in streamlining server management.

Common Mistakes to Avoid

Spacing inside square brackets trips up many new bash script writers. Always include spaces inside the brackets.

# Wrong
if [ $value -gt 10]; then

# Correct
if [ $value -gt 10 ]; then

Unquoted variables cause unexpected word splitting and glob expansion. Always quote variables that might contain spaces or special characters.

# Risky
cat $filename

# Safe
cat "$filename"

Using rm -rf with variables is dangerous. Always verify the path before deleting anything on a production server. Accidental deletions can cause significant downtime and data loss.

# Dangerous if $dir is empty or contains unexpected characters
rm -rf "$dir"

# Safer: check the directory exists first
if [ -d "$dir" ]; then
    echo "About to delete: $dir"
    rm -rf "$dir"
else
    echo "Directory does not exist: $dir"
fi

Expanding Your Scripts Over Time

Start with small scripts that solve a single problem. Once a script works reliably, expand it with new features. A log rotation script can grow to include compression, remote upload, and alerting. A backup script can be extended to verify backup integrity and test a restoration process.

Keep production scripts in a dedicated directory such as /root/scripts/ and make them executable. Document what each script does at the top of the file using comments. Version control your scripts with git so you can track changes and roll back if needed.

Bash scripting rewards small, incremental improvements. Each script you write and refine builds your ability to automate more of your server management work. The time invested in writing and testing a script pays back every time it runs without requiring your attention.