funwithlinux guide

Bash Scripting for System Administration: Essential Skills

In the world of system administration, efficiency, reliability, and automation are paramount. Whether you’re managing a single server or a fleet of machines, repetitive tasks like backups, log rotation, service monitoring, and user management can drain your time—unless you automate them. **Bash scripting** is the cornerstone of this automation. Bash (Bourne Again SHell) is the default shell on most Linux and Unix systems, offering a powerful scripting language to automate system tasks. From simple one-liners to complex workflows, bash scripts empower sysadmins to streamline operations, enforce consistency, and respond to issues proactively. This blog will guide you through the essential bash scripting skills every system administrator needs. We’ll cover fundamentals like variables and control structures, advanced topics like error handling and process management, and real-world automation scenarios. By the end, you’ll be equipped to write robust scripts that solve everyday sysadmin challenges.

Table of Contents

  1. Basics of Bash Scripting
  2. Variables and Data Types
  3. Input/Output Handling
  4. Control Structures: Conditionals and Loops
  5. Functions for Reusability
  6. Error Handling and Debugging
  7. Working with Files and Directories
  8. Process Management
  9. Automation Tasks: Cron, Backups, and Log Rotation
  10. Security Best Practices
  11. Advanced Tips and Tools
  12. Conclusion
  13. References

1. Basics of Bash Scripting

A bash script is a text file containing a sequence of commands executed by the bash shell. Let’s start with the absolute fundamentals.

Shebang Line

Every script should start with a “shebang” line to specify the interpreter:

#!/bin/bash

This tells the system to run the script with /bin/bash (not the default sh, which may be a limited shell like dash).

Making Scripts Executable

To run a script, you must make it executable with chmod:

chmod +x script.sh  # Grant execute permission
./script.sh         # Run the script (./ is required if the script is in the current directory)

Hello World Example

The simplest script:

#!/bin/bash
echo "Hello, System Administration!"

2. Variables and Data Types

Variables store data for reuse. Bash is weakly typed, so you don’t declare types (e.g., string, integer).

Declaring Variables

Assign values without spaces around =:

name="SysAdmin"
age=30

Accessing Variables

Use $ to reference a variable:

echo "Name: $name"       # Output: Name: SysAdmin
echo "Age: ${age} years" # Curly braces avoid ambiguity (e.g., ${age}years vs $ageyears)

Special Variables

Bash provides built-in variables for common tasks:

VariablePurposeExample
$0Script nameecho "Script: $0"Script: ./script.sh
$1, $2Positional arguments (1st, 2nd input)./script.sh arg1 arg2$1=arg1
$@All arguments (as separate strings)for arg in "$@"; do echo $arg; done
$#Number of argumentsecho "Args: $#"Args: 2 (for 2 inputs)
$?Exit code of the last command (0 = success)ls non_existent_file; echo $?2 (error)
$$Process ID (PID) of the scriptecho "PID: $$"PID: 12345

Environment Variables

Global variables like PATH, HOME, or USER are set by the system. Use export to make a variable available to child processes:

export BACKUP_DIR="/var/backups"  # Now accessible to commands run by the script

3. Input/Output Handling

Bash scripts interact with input (stdin), output (stdout), and errors (stderr).

Standard Streams

  • stdin (0): Input (e.g., keyboard, piped data).
  • stdout (1): Normal output (default: terminal).
  • stderr (2): Error output (default: terminal).

Redirection

Control where output/errors go:

OperatorActionExample
>Overwrite stdout to a fileecho "Log" > output.log
>>Append stdout to a fileecho "New log" >> output.log
2>Redirect stderr to a filels non_existent 2> errors.log
&>Redirect both stdout and stderrcommand &> combined.log

Pipes (|)

Send stdout of one command to stdin of another:

ps aux | grep "nginx"  # Find nginx processes (ps → grep)

Here Documents

Embed multi-line input directly in the script:

cat << EOF > config.txt
server {
    port 80;
    host localhost;
}
EOF

4. Control Structures: Conditionals and Loops

Control structures let you execute code conditionally or repeatedly.

Conditionals (if-else)

Check conditions with if, elif, and else. Use [ ] (POSIX) or [[ ]] (bash-specific, supports patterns/regex).

Example: Check if a File Exists

file="/data/backup.tar.gz"

if [[ -f "$file" ]]; then  # -f = regular file exists
    echo "$file exists."
elif [[ -d "$file" ]]; then  # -d = directory exists
    echo "$file is a directory."
else
    echo "$file not found."
fi

Case Statements

Simplify multi-condition checks:

read -p "Enter action (start/stop/restart): " action

case $action in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart)
        echo "Restarting service..."
        ;;
    *)  # Default case (any other input)
        echo "Invalid action: $action"
        exit 1  # Exit with error code 1
        ;;
esac

Loops

for Loops

Iterate over lists (files, arguments, ranges):

# Loop through files in /var/log
for logfile in /var/log/*.log; do
    echo "Processing $logfile"
done

# Loop through numbers 1-5
for i in {1..5}; do
    echo "Count: $i"
done

while Loops

Run until a condition fails:

count=1
while [[ $count -le 3 ]]; do  # -le = less than or equal
    echo "Loop $count"
    ((count++))  # Increment count
done

until Loops

Run until a condition succeeds (opposite of while):

count=5
until [[ $count -eq 0 ]]; do  # Stop when count=0
    echo "Countdown: $count"
    ((count--))
done

5. Functions for Reusability

Functions group code into reusable blocks, improving readability and reducing redundancy.

Defining Functions

Two syntaxes:

# Method 1
greet() {
    echo "Hello, $1!"  # $1 = first argument to the function
}

# Method 2 (bash-specific)
function log_message {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")  # local = variable scoped to function
    echo "[$timestamp] $1"
}

Calling Functions

greet "Admin"  # Output: Hello, Admin!
log_message "Backup started"  # Output: [2024-05-20 14:30:00] Backup started

Return Values

Bash functions return exit codes (0-255) with return, or output values via echo (captured with command substitution):

add_numbers() {
    return $(( $1 + $2 ))  # Return sum as exit code (limited to 0-255)
}

sum=$(echo $(( $1 + $2 )) )  # Better: use command substitution for values >255
echo "Sum: $sum"

6. Error Handling and Debugging

Robust scripts must handle errors gracefully. Here’s how to make your scripts resilient.

Exit on Error (set -e)

Add set -e at the top of your script to exit immediately if any command fails:

#!/bin/bash
set -e  # Exit on error

cd /non/existent/dir  # Fails → script exits here
echo "This line never runs"

Treat Unset Variables as Errors (set -u)

set -u prevents silent failures from typos in variable names:

#!/bin/bash
set -u

echo "Name: $nam"  # Typo: $nam instead of $name → script exits with error

Trap Signals

Use trap to run commands on signals (e.g., script exit, Ctrl+C):

#!/bin/bash

# Cleanup temporary files on exit
cleanup() {
    echo "Cleaning up temp files..."
    rm -f /tmp/temp.txt
}

trap cleanup EXIT  # Run cleanup when script exits (normal or error)
trap 'echo "Aborted!"; exit 1' SIGINT  # Handle Ctrl+C (SIGINT)

Debugging Tools

  • set -x: Enable trace mode to print commands before execution:
    #!/bin/bash
    set -x  # Trace on
    echo "Debugging..."
    set +x  # Trace off
  • shellcheck: A linter for bash scripts (install with sudo apt install shellcheck):
    shellcheck script.sh  # Identifies syntax errors and bad practices

7. Working with Files and Directories

Sysadmins spend 90% of their time managing files. Here’s how to automate file operations.

File/Directory Basics

TaskCommand/Example
Create directorymkdir -p /data/logs ( -p creates parent dirs)
Delete directoryrm -rf /old/data ( -r recursive, -f force)
Copy filescp -a /src /dest ( -a archive: preserve permissions)
Move/rename filesmv file.txt /new/location/newname.txt

Checking File Properties

Use conditional operators to validate files:

OperatorPurposeExample
-f $fileIs $file a regular file?if [[ -f "data.txt" ]]; then ...
-d $dirIs $dir a directory?if [[ -d "/data" ]]; then ...
-x $fileIs $file executable?if [[ -x "/usr/bin/script.sh" ]]; then ...
-s $fileIs $file non-empty?if [[ -s "log.txt" ]]; then ...

Searching for Files

Use find to locate files by name, size, or modification time:

# Find all .log files modified in the last 7 days, larger than 100MB
find /var/log -type f -name "*.log" -mtime -7 -size +100M -exec ls -lh {} \;

File Permissions

Modify permissions with chmod and ownership with chown:

chmod 600 secret.txt  # Read/write for owner only (600)
chmod +x script.sh    # Add execute permission
chown www-data:www-data /var/www  # Set owner:group to www-data

8. Process Management

Automate monitoring and controlling running processes.

Checking Processes

  • ps aux: List all processes (user, PID, CPU/memory usage).
  • pgrep: Find PIDs by name:
    pgrep nginx  # Returns PID of nginx (if running)

Killing Processes

  • kill PID: Send a termination signal (SIGTERM) to a process.
  • pkill nginx: Kill all processes named nginx.
  • kill -9 PID: Force-kill (SIGKILL, cannot be ignored).

Service Monitoring Example

Restart a service if it’s down:

#!/bin/bash
service="nginx"

if ! pgrep "$service" >/dev/null; then  # >/dev/null suppresses output
    echo "$service is down! Restarting..."
    systemctl start "$service"
else
    echo "$service is running."
fi

9. Automation Tasks

Bash scripting shines for repetitive tasks like backups, log rotation, and scheduled jobs.

Cron Jobs (Scheduling)

Use cron to run scripts at fixed intervals. Edit cron jobs with crontab -e:

Cron Syntax:

* * * * * command_to_run
| | | | |
| | | | +-- Day of week (0=Sun, 6=Sat)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)

Example: Daily Backup at 2 AM

# Add to crontab -e
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1

Backup Script Example

Use tar or rsync for backups:

#!/bin/bash
set -euo pipefail  # Exit on error, unset var, or pipe failure

backup_dir="/backups"
source="/data"
timestamp=$(date +"%Y%m%d_%H%M%S")
archive="$backup_dir/backup_$timestamp.tar.gz"

# Create backup
echo "Creating backup: $archive"
tar -czf "$archive" "$source"

# Delete backups older than 30 days
find "$backup_dir" -name "backup_*.tar.gz" -mtime +30 -delete

Log Rotation

Prevent log files from filling disks with logrotate (system tool) or a custom script:

Custom Log Rotation Script:

#!/bin/bash
log_file="/var/log/app.log"

# Rotate if log is >100MB
if [[ $(du -m "$log_file" | cut -f1) -gt 100 ]]; then
    mv "$log_file" "$log_file.old"
    gzip "$log_file.old"  # Compress old log
    touch "$log_file"     # Create new log file
    chmod 644 "$log_file"
    systemctl restart app  # Restart app to use new log
fi

10. Security Best Practices

Secure scripts prevent data breaches and system compromise.

Input Validation

Sanitize user input to avoid injection attacks:

read -p "Enter username: " user

# Allow only letters, numbers, and underscores
if [[ ! "$user" =~ ^[a-zA-Z0-9_]+$ ]]; then
    echo "Invalid username!"
    exit 1
fi

Least Privilege

Avoid running scripts as root. Use sudo for specific commands instead:

# Bad: Run entire script as root
# Good: Use sudo for critical commands only
sudo chown www-data:www-data /var/www  # Specific sudo call

Avoid Hardcoded Secrets

Store credentials in environment variables or secure vaults (e.g., HashiCorp Vault), not in scripts:

# Bad: Hardcoded password
# password="secret123"

# Good: Use environment variable
password="$DB_PASSWORD"  # Set via export DB_PASSWORD="secret123"

11. Advanced Tips

Take your scripts to the next level with these pro techniques.

Arrays

Store lists of values:

servers=("server1" "server2" "server3")
echo "First server: ${servers[0]}"  # Output: server1
echo "All servers: ${servers[@]}"   # Output: server1 server2 server3

Associative Arrays (Dictionaries)

Store key-value pairs (bash 4+):

declare -A services=(
    ["web"]="nginx"
    ["db"]="mysql"
)
echo "Web service: ${services[web]}"  # Output: nginx

Arithmetic Operations

Use $(( )) or bc for math:

sum=$(( 2 + 2 ))
echo "Sum: $sum"  # Output: 4

# Floating-point math with bc
average=$(echo "scale=2; (10 + 20)/2" | bc)  # scale=2 = 2 decimal places

Command Substitution

Capture command output into variables:

disk_usage=$(df -h / | awk 'NR==2 {print $5}')  # Get root disk usage (e.g., 30%)
echo "Disk usage: $disk_usage"

12. Conclusion

Bash scripting is the backbone of system administration automation. From backups to log rotation, from process monitoring to security hardening, these skills will save you hours of manual work and reduce human error.

Start small: write a script to clean up logs, then a backup script, then a cron job to run it. Use shellcheck to refine your code, and never stop experimenting. With practice, you’ll build a library of scripts that make you a more efficient, effective sysadmin.

13. References