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.