Bash Scripting Basics: Automating Linux Tasks

17 min read 3,219 words
Bash Scripting Basics: Automating Server Tasks Without Programming featured image

What Bash Scripting Is and Why Server Administrators Rely on It

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 every week. 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 range from a simple three-line file that clears old log entries to a complex multi-step deployment pipeline. The key advantage is that once a 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 a programming background 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 Core Concepts You Need

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

echo $SHELL

This prints something like /bin/bash, confirming 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 when executing 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. The process stays the same for every script you write going forward: create the file, add commands, make it executable, and run it.

Variables: Storing and Reusing Information

Variables let you store information inside a script so you can reference 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 and 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 captures 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 each morning.

Conditional Logic: Making Decisions in Scripts

Scripts become genuinely useful when they can make decisions based on conditions. The if statement tests a condition and runs different commands depending on whether the result 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 pattern matching.

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. This lets you check for expected files before attempting operations on them.

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 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 to generate the items automatically.

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, waiting for a service to start, or polling an API endpoint.

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 before giving up.

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

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 total 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 depending on what each log needs.

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 to save disk space.

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 to handle each file individually.

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, making it easy to spot problems before they cause runtime issues.

Scheduling Scripts with Cron

A script only runs when you execute it manually unless you schedule it with cron. The cron daemon runs in the background and executes commands at specified intervals. Edit the crontab to add or modify 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 scheduling 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 when troubleshooting.

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 and the different cron expressions, 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 identify the problem. The -x flag prints each command and its arguments as the script runs, showing you exactly what is happening at each step.

bash -x ~/your_script.sh

You can also enable debug mode inside the script itself for specific sections rather than the entire file.

#!/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 rather than treating them as empty strings.

#!/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 that you rely on for important server tasks.

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, which can lead to cascading failures or corrupted data.

Security note: The example above hardcodes credentials for illustration. In production scripts, store database passwords in separate configuration files with restricted permissions, or use environment variables that are not visible in process listings.

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 requires attention.

#!/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)" admin@example.com < "$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 when variables contain spaces or special characters. Always quote variables to prevent this.

# Risky
cat $filename

# Safe
cat "$filename"

Using rm -rf with variables is dangerous if the variable expands to an unexpected value. Always verify the path before deleting anything on a production server. Accidental deletions can cause significant downtime and data loss that may not be recoverable.

# 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

Another common mistake is forgetting to make a script executable. If you schedule a script in cron but forget to run chmod +x, cron will fail silently and the script will not run.

When Bash Scripting Is the Right Tool

Bash scripting excels at automating command-line tasks on Linux servers. It works well for file processing, system monitoring, backup scripts, log rotation, and service management. If a task involves chaining together existing command-line tools, bash is usually the right choice.

For more complex application logic, data processing pipelines, or tasks that require advanced data structures, a general-purpose programming language like Python may be more appropriate. Python has richer standard library support for tasks like working with APIs, parsing complex file formats, or building web interfaces.

The good news is that bash scripts can call Python scripts and vice versa, so you are not locked into one approach. Many production environments use both: bash handles system-level orchestration while Python handles application-specific logic. If you are already using tools like GitHub Actions for deployment, GitOps workflows can coordinate between bash scripts and other automation tools.

Expanding Your Scripts Over Time

Start with small scripts that solve a single problem well. Once a script works reliably in your environment, expand it with new features when the need arises. 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 with proper permissions. 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 a modification causes problems.

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.

Building Your Automation Skills

Bash scripting is a practical skill that improves with every script you write. Start with simple tasks like log rotation or service monitoring. As your confidence grows, move on to deployment scripts, backup automation, and system health checks. Each script you complete becomes a reusable tool in your server management toolkit.

The principles covered here form a solid foundation. Variables, conditionals, loops, and functions work the same way across most bash scripts you will encounter. Understanding these building blocks makes it easier to read scripts written by others, troubleshoot problems in existing automation, and adapt proven patterns to your own needs.

If you manage multiple servers or are looking to reduce the time spent on routine server maintenance, investing in bash scripting knowledge pays off quickly. The scripts you write today will run automatically tomorrow, freeing up your time for more complex challenges.

Frequently Asked Questions

Do I need programming experience to learn bash scripting?
No prior programming experience is required. If you can navigate a terminal and understand basic commands like ls, cd, and cp, you have enough foundation to start writing bash scripts. The concepts build gradually from simple commands to loops and functions.
What is the difference between a bash script and a shell script?
Bash is a specific type of shell that implements the POSIX shell standard with additional features. A shell script can run under any shell interpreter, while a bash script specifically requires the bash interpreter. Using the shebang #!/bin/bash at the top of your script ensures it runs under bash specifically rather than falling back to whatever default shell is configured.
How do I make a bash script run automatically every day?
Use cron to schedule your script. Run crontab -e to edit your crontab file, then add an entry specifying when the script should run. For example, 0 3 * * * /path/to/script.sh runs the script at 3 AM every day. Make sure the script has execute permissions with chmod +x before scheduling it, otherwise cron will fail silently.
Can bash scripts interact with APIs or remote servers?
Yes. Bash scripts can use tools like curl, wget, and ssh to communicate with remote services, fetch data from APIs, and execute commands on remote servers. For complex deployment pipelines, combining bash scripts with CI/CD tools like GitHub Actions is a common approach that scales well as your infrastructure grows.
What happens if a bash script encounters an error?
By default, bash continues executing the remaining commands even if one command fails. Using set -e at the top of your script makes it exit immediately when any command fails. Always checking exit codes after critical commands lets you handle errors gracefully and decide whether the script should continue or stop with a clear error message.
Is bash scripting secure for production use?
Bash scripting is secure when written carefully. Avoid hardcoding passwords or secrets directly in scripts. Use environment variables or configuration files with restricted permissions instead. Be cautious with user input that gets processed by scripts, as improper handling can lead to command injection vulnerabilities. Always test scripts in a non-production environment before deploying them where they can affect real services.
How do I learn what each command does before using it in a script?
Use the man command followed by the command name to read its manual page. For example, man find or man awk. The manual pages explain each option and provide examples. Practice running commands directly in the terminal before incorporating them into scripts, so you understand what they do and what the output looks like.
How should I store sensitive information used by scripts?
Store sensitive information such as database passwords, API keys, and SSH credentials in separate configuration files outside the script directory. Set file permissions so only the owner can read them, for example chmod 600 /root/scripts/config. Reference these values in your scripts using environment variables or by sourcing the configuration file at the start of your script.