Mastering Shell Scripting: Build Secure, Reliable, and Efficient Automation Scripts — A Practical Example to Automate File Cleanup Like a Pro

Mastering Shell Scripting: Build Secure, Reliable, and Efficient Automation Scripts — A Practical Example to Automate File Cleanup Like a Pro

Introduction:

Shell scripting is more than just a skill — it’s a superpower for anyone working in IT, DevOps, or software development. Whether you’re automating repetitive tasks, managing production servers, or performing complex system operations, a well-written shell script can save you time, reduce errors, and optimize your workflow.

But what separates a novice from a pro in shell scripting? It’s not just about getting the job done but writing efficient, maintainable, and reliable scripts. This blog explores how to elevate your shell scripting game, focusing on real-world scenarios, best practices, and tips to craft scripts like a seasoned professional. You’ll also learn how to integrate essential features like logging, error handling, user input validation, and flexibility into your scripts.

By the end of this guide, you’ll have all the tools and knowledge you need to write functional, scalable, and industry-ready shell scripts. Whether you’re a beginner or looking to refine your skills, this blog is your step-by-step roadmap to becoming a shell scripting pro.

Here are the key aspects to ensure that your script meets professional standards:

1. Input Validation:

  • Why it’s important: Prevents unintended behaviour caused by invalid or missing inputs.

  • How to do it: Validate user-provided arguments like file paths or numbers using checks ([ -d "$DIR" ] for directories or regex for integers).

  • Example:

if [ -z "$TARGET_DIR" ] || [ ! -d "$TARGET_DIR" ]; then
    echo "Error: Invalid directory. Exiting."
    exit 1
fi

2. Error Handling:

  • Why it’s important: Ensures the script handles unexpected situations gracefully.

3. Logging:

  • Why it’s important: Provides visibility into the script’s execution, making debugging and auditing easier.

  • Example:

echo "$(date): Deleted $file" >> /var/log/script.log

4. Flexibility:

  • Why it’s important: Allows the script to be reused for different scenarios.

  • How to do it: Use command-line arguments (getopts) for configurable inputs like file paths, thresholds, or modes (--dry-run).

  • Example:

./cleanup.sh -d /data/archive -n 30 --dry-run

5. Modular Design:

  • Why it’s important: Makes the script easier to maintain and extend.

  • How to do it: Break functionality into functions with single responsibilities.

6. Security:

  • Why it’s important: Protects sensitive data and prevents misuse.

  • How to do it: Quote variables, avoid executing untrusted input, and run with the least privilege required.

  • Example:

rm -f "$file"  # Quote variables to avoid unintended expansions

7. User-Friendly Features:

  • Why it’s important: Improves usability for others running the script.

  • How to do it: Provide a -h (help) option, meaningful error messages, and dry-run mode.

  • Example:

if [[ "$1" == "-h" ]]; then
    echo "Usage: ./cleanup.sh -d <directory> -n <days> [--dry-run]"
    exit 0
fi

8. Documentation:

  • Why it’s important: Helps others understand and use your script effectively.

  • How to do it: Add comments, a usage guide, and examples within the script.

  • Example:

# Usage:
# ./cleanup.sh -d /path/to/directory -n 30 --dry-run

9. Testing:

  • Why it’s important: Ensures the script works reliably in different environments.

  • How to do it: Test for edge cases, large data sets, and invalid inputs. Automate testing if possible.

Scenario:

Delete files not accessed in the specified directory within a given timeframe.

Problem Statement:

You are managing a production server with limited disk space. To maintain optimal disk usage, you must write a shell script to clean up files from any specified directory. The script should allow the user to define the directory path and the age of files (in days) to target. It must also log the names of deleted files, calculate the total disk space reclaimed, and provide an option to preview the operation without deleting files.

Creating this with a simple script may not guarantee its reliability, reusability, and suitability for production environments.

We will outline the requirements for making this script production-ready:

  • The script should accept the target directory and the number of days as command-line arguments.

  • Identify and delete files in the specified directory that have not been accessed in the user-defined number of days.

  • Provide a --dry-run option to display the files to be deleted and the disk space cleared without performing actual deletions.

  • Log the names of all deleted files and the total disk space reclaimed to a timestamped log file in /var/log.

  • Ensure proper validation of inputs, including directory existence and the validity of the number of days.

  • Display appropriate messages when no files meet the criteria or if the script is run in dry-run mode.

  • Prompt the user for confirmation before proceeding with file deletions.

Example: cleanup_old_files.sh

#!/bin/bash
# Script Name: cleanup_old_files.sh
# Description:
#   Deletes files not accessed over a user-specified number of days from a specified directory.
#   Displays file size and last accessed date for each file before deletion.
#   It supports a dry-run mode to preview files without deleting them and logs the actions to a file.
#   Requires sudo/root permissions to run if accessing files or directories with restricted permissions.
#
# Author: [S.SUBBAREDDY]
# Date: [12-12-2024]
# Version: 1.6
#
# Prerequisites:
#   - Requires write permissions for the target directory and the log file location.
#   - Requires `bash`, `find`, `stat`, and `du` installed on the system.
#
# Usage:
#   sudo ./cleanup_old_files.sh -d <directory> -n <days> [--dry-run] [-h]
#
# Options:
#   -d <directory>   Directory to clean up.
#   -n <days>        Number of days to check for file access.
#   --dry-run        Show files to be deleted without deleting them.
#   -h               Display this help message.
#
# Examples:
#   sudo ./cleanup_old_files.sh -d /data/archive -n 30
#       Deletes files in '/data/archive' not accessed in the last 30 days.
#
#   sudo ./cleanup_old_files.sh -d /data/archive -n 30 --dry-run
#       Previews the files in '/data/archive' that would be deleted without deleting them.
#

# Check if script is run with sudo/root
if [ "$EUID" -ne 0 ]; then
    echo "This script must be run as root. Use sudo."
    exit 1
fi

# Function to display help and usage
usage() {
    echo "Usage: $0 -d <directory> -n <days> [--dry-run] [-h]"
    echo
    echo "Options:"
    echo "  -d <directory>   Directory to clean up."
    echo "  -n <days>        Number of days to check for file access."
    echo "  --dry-run        Show files to be deleted without deleting them."
    echo "  -h               Display this help message."
    echo
    echo "Examples:"
    echo "  sudo $0 -d /data/archive -n 30"
    echo "      Deletes files in '/data/archive' not accessed in the last 30 days."
    echo
    echo "  sudo $0 -d /data/archive -n 30 --dry-run"
    echo "      Previews the files in '/data/archive' that would be deleted without deleting them."
    exit 0
}

# Parse arguments
DRY_RUN=false  # Default is false
while getopts ":d:n:h-:" opt; do
  case $opt in
    d) TARGET_DIR="$OPTARG" ;;
    n) DAYS="$OPTARG" ;;
    h) usage ;;
    -)
      case "$OPTARG" in
        dry-run) DRY_RUN=true ;;
        *) echo "Invalid option --$OPTARG"; usage ;;
      esac ;;
    :)
      echo "Error: Option -$OPTARG requires an argument." >&2
      usage ;;
    *)
      echo "Error: Invalid option." >&2
      usage ;;
  esac
done

# Validate mandatory arguments
if [ -z "$TARGET_DIR" ] || [ -z "$DAYS" ]; then
    echo "Error: Both directory (-d) and number of days (-n) are required."
    usage
fi

# Validate that the directory exists
if [ ! -d "$TARGET_DIR" ]; then
    echo "Error: The directory '$TARGET_DIR' does not exist."
    exit 1
fi

# Validate that the number of days is a positive integer
if ! [[ "$DAYS" =~ ^[0-9]+$ ]]; then
    echo "Error: Days must be a positive integer."
    exit 1
fi

# Set log file to a writable location
LOG_FILE="$HOME/cleanup_$(date +%Y%m%d_%H%M%S).log"

# Check if the log file can be created
if ! touch "$LOG_FILE" &>/dev/null; then
    echo "Error: Cannot write to log file $LOG_FILE. Check permissions."
    exit 1
fi

# Find files not accessed in the last $DAYS days
DELETED_FILES=$(find "$TARGET_DIR" -type f -atime +"$DAYS" -print)

# Check if any files were found
if [ -z "$DELETED_FILES" ]; then
    echo "No files found in '$TARGET_DIR' that have not been accessed in the last $DAYS days."
    exit 0
fi

# Display files to be deleted along with their sizes and last accessed dates
echo "The following files will be deleted:"
TOTAL_SPACE=0

for file in $DELETED_FILES; do
    if [ -r "$file" ]; then
        FILE_SIZE=$(du -h "$file" | cut -f1)  # Human-readable file size
        LAST_ACCESSED=$(stat -c '%x' "$file" | cut -d'.' -f1)  # Last accessed date
        echo "$file (Size: $FILE_SIZE, Last Accessed: $LAST_ACCESSED)"
        FILE_SIZE_KB=$(du -k "$file" | cut -f1)  # File size in KB for total space calculation
        TOTAL_SPACE=$((TOTAL_SPACE + FILE_SIZE_KB))
    else
        echo "$file (Unable to read file details due to permission issues)"
    fi
done

echo "Total space to be cleared: $((TOTAL_SPACE / 1024)) MB"

# Dry-run mode: Show files without deleting
if $DRY_RUN; then
    echo "Dry-run mode enabled: No files will be deleted."
    exit 0
fi

# Confirm deletion
read -p "Do you want to proceed with deletion? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
    echo "Deletion aborted."
    exit 0
fi

# Start logging
echo "Starting cleanup: $(date)" >> "$LOG_FILE"

# Delete the files
for file in $DELETED_FILES; do
    if [ ! -w "$file" ]; then
        echo "Error: Skipping $file due to insufficient permissions." >> "$LOG_FILE"
        echo "Skipping $file (Permission denied)"
        continue
    fi

    FILE_SIZE=$(du -k "$file" | cut -f1)
    if rm -f "$file"; then
        echo "Deleted $file (Size: ${FILE_SIZE}K)" >> "$LOG_FILE"
    else
        echo "Error: Failed to delete $file" >> "$LOG_FILE"
        echo "Failed to delete $file"
    fi
done

# Log the total space cleared
echo "Total space cleared: $((TOTAL_SPACE / 1024)) MB" >> "$LOG_FILE"
echo "Cleanup completed: $(date)" >> "$LOG_FILE"

# End of script

Here’s the logical flow for the script to represent the process clearly.

START
 |
 |--> Check if script is run with sudo/root
 |     |
 |     |--> [No sudo/root] --> Display error --> EXIT
 |
 |--> Parse Command-Line Arguments
 |     |
 |     |--> Validate Inputs
 |           |
 |           |--> [Invalid Directory] --> Display error --> EXIT
 |           |--> [Invalid Days] --> Display error --> EXIT
 |
 |--> Set Up Logging
 |     |
 |     |--> Check if log directory exists
 |           |
 |           |--> [Cannot Create Log Directory] --> Display error --> EXIT
 |
 |--> Find Files
 |     |
 |     |--> [No Files Found] --> Display message --> EXIT
 |
 |--> Display Files (Path, Size, Last Accessed)
 |     |
 |     |--> Calculate Total Space
 |
 |--> Check for Dry-Run Mode
 |     |
 |     |--> [Dry-Run Enabled] --> Display message --> EXIT
 |
 |--> Confirm Deletion
 |     |
 |     |--> [User Declines] --> Display message --> EXIT
 |
 |--> Delete Files
 |     |
 |     |--> Log Successful Deletions
 |
 |--> Log Total Space Cleared
 |
END

Execution Steps:

  1. Start:
  • The script starts by checking if it is run with sudo or as the root user.

  • If not, it exits with an error message.

2. Parse Arguments:

  • The script parses the command-line options:

  • -d <directory>: Target directory for cleanup.

  • -n <days>: Number of days for file access check.

  • --dry-run: Enables preview mode without performing deletion.

  • -h: Displays the help message.

3. Validate Inputs:

i) Checks if:

  • The target directory exists.

  • The number of days is a positive integer.

ii) If validation fails, the script exits with an appropriate error message.

4. Set Up Logging:

  • Sets the log file path to a writable location (default: /tmp or user-defined via $LOG_DIR).

  • Ensures the log directory exists and can be written to.

5. Find Files:

  • Uses the find command to identify files in the target directory that have not been accessed in the last <days> days.

6. Check for Matching Files:

  • If no matching files are found, the script exits with a message indicating no files met the criteria.

7. Preview File:

i) Displays the list of files to be deleted, including:

  • File path.

  • File size (human-readable format).

  • Last accessed date.

ii) Calculates the total disk space that would be cleared.

8. Dry-Run Mode:

  • If --dry-run is enabled, it exits after displaying the file list and total space to be cleared.

9. Confirm Deletion:

  • Asks the user to confirm the deletion operation.

  • If the user declines, the script exits without making any changes.

10. Delete Files:

  • Deletes all files listed (since permission errors are not possible when run as sudo).

  • Logs successful deletions to the log file.

11. Log Results:

  • Logs the total space cleared and a summary of the cleanup operation.

12. End:

  • The script exits after completing the cleanup.

How to Use the Script

Example Commands:

  1. Basic Usage:
sudo ./cleanup_old_files.sh -d /data/archive -n 30

2. Dry-Run Mode:

sudo ./cleanup_old_files.sh -d /data/archive -n 30 --dry-run

3. Help:

sudo ./cleanup_old_files.sh -h

Before running the script, check the status of the desired directory: /data/archive.

Output Examples:

1. Case: Help

2. Case: Dry-Run

3. Case: Deletion Aborted

4. Case: Confirmation and Deletion

The image is created by the author

4. Case: Cleanup Logs

The image is created by the author

Conclusion

Writing standard shell scripts is more than just automating tasks; it involves ensuring reliability, security, and maintainability. Here’s how to accomplish this:

  • Enforce Permissions: Limit execution permissions to authorized users who have sudo privileges to prevent accidental or unauthorized actions.

  • Implement Dry-Run Mode: Users should have the option to preview their actions before finalizing any changes.

  • Dynamic Logging: Utilize a centralized and secure logging system to track and audit the actions of scripts.

  • Add Confirmation Steps: Include prompts for users to prevent accidental deletions.

  • Ensure Flexibility: Create scripts that accept dynamic inputs to enhance scalability and promote reuse.

By adhering to best practices, you can create efficient and resilient shell scripts that fulfill real-world needs while minimizing risks. Mastering these techniques will equip you to tackle any scripting challenge! 🚀

What are your thoughts on this article? Feel free to share your opinions in the comments below — or above, depending on your device! If you enjoyed the story, please consider supporting me by clapping, leaving a comment, and highlighting your favorite parts.

Visit subbutechops.com to explore the fascinating world of technology and data. Get ready for more exciting content. Thank you, and happy learning!