From efeef3d4b16698aa155745d717ae4f69cab75fe3 Mon Sep 17 00:00:00 2001 From: Andrii Syrovatko Date: Tue, 14 Apr 2026 14:27:35 +0300 Subject: [PATCH] feat(utils): add compare-dirs.sh utility - recursive directory comparison based on source folder - support for quiet mode and custom log files - colored output and dependency checking - integrated with main navigation table --- README.md | 1 + tools/linux/compare-dirs/README.md | 37 ++++++ tools/linux/compare-dirs/compare-dirs.sh | 162 +++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 tools/linux/compare-dirs/README.md create mode 100755 tools/linux/compare-dirs/compare-dirs.sh diff --git a/README.md b/README.md index 6ea6361..2beae00 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A collection of battle-tested scripts and utilities for system administrators, D | OS | Stack | Utility Name | Description | Status | | :--- | :--- | :--- | :--- | :--- | | ![Windows](https://img.shields.io/badge/Windows-0078D6?style=flat-square&logo=windows&logoColor=white) | ![Batch](https://img.shields.io/badge/-Batch-4D4D4D?style=flat-square) | [AnyDesk Reset](./tools/windows/apps-reset/anydesk-id-reseter.bat) | Terminate [AnyDesk](https://anydesk.com/en) & clear config files (`ID reset`) | ✅ Stable
🧰 Built-in  | +| ![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat-square&logo=linux&logoColor=black) | ![Bash](https://img.shields.io/badge/-Bash-4EAA25?style=flat-square) | [Directory Comparator](./tools/linux/compare-dirs/compare-dirs.sh) | Recursive file comparison between two directories with detailed diff logging | ✅ Stable
🧰 Built-in  | | ![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat-square&logo=linux&logoColor=black) | ![Bash](https://img.shields.io/badge/-Bash-4EAA25?style=flat-square) | [PSQL NFS Backup](https://github.com/andsyrovatko/s4k-psql-db-backuper) | Automation for [PostgreSQL](https://www.postgresql.org/) dumps with [NFS](https://en.wikipedia.org/wiki/Network_File_System) & [Telegram](https://telegram.org/) | ✅ Stable
📦 Standalone  | | ![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat-square&logo=linux&logoColor=black) | ![Bash](https://img.shields.io/badge/-Bash-4EAA25?style=flat-square) | [Whois Infrastructure Automator (for ISPs)](https://github.com/andsyrovatko/s4k-billing-whois-automator) | Automation for Whois DB update by billing system | ✅ Stable
📦 Standalone  | | ![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat-square&logo=linux&logoColor=black) | ![Bash](https://img.shields.io/badge/-Bash-4EAA25?style=flat-square) | [IP Manager for ISPs (Billing ↔ IPSET)](https://github.com/andsyrovatko/s4k-ip-manager) | Automation IP management by billing or external system for ISPs | ✅ Stable
📦 Standalone  | diff --git a/tools/linux/compare-dirs/README.md b/tools/linux/compare-dirs/README.md new file mode 100644 index 0000000..a80b7c9 --- /dev/null +++ b/tools/linux/compare-dirs/README.md @@ -0,0 +1,37 @@ +# 🛠 Directory Comparator + +A robust Bash utility for recursive file comparison between two directories. It uses DIR_A as the source of truth for file names and checks for their existence and differences in DIR_B. Perfect for auditing configuration changes or verifying backup integrity. + +--- + +### ⚙️ Prerequisites +* **diff:** Used for generating file differences. +* **find:** Required for recursive file discovery. +* **Bash 4+:** Utilizes modern bash features like `set -euo pipefail`. + +### 📊 Parameters Mapping +| Option | Description | +| :--- | :--- | +| -o | Save the full report to a specific file (default: `./compare-dirs.log`). | +| -q | Quiet mode: Show only the final summary without full `diff` output. | +| -h | Show help and usage information. | + +### 🏃 Quick Start +```bash + # Basic comparison (outputs diffs to terminal and compare-dirs.log) + ./compare-dirs.sh /path/to/dir_a /path/to/dir_b + + # Quiet mode (summary only) with custom log file + ./compare-dirs.sh -q -o audit_report.log /etc/nginx /backup/nginx +``` + +### 🛡 Features +* **Safe Discovery:** Uses `-printf '%P\0'` with `find` to safely handle filenames with spaces or special characters. +* **Strict Execution:** Built-in error trapping and strict mode to prevent silent failures. +* **Colorized UI:** Visual feedback for **IDENTICAL**, **DIFFERENT**, and **MISSING** status. +* **Log Rotation Ready:** Outputs to both `STDOUT` and log files simultaneously. + +--- + +### ⚖️ License +MIT [LICENSE](https://github.com/andsyrovatko/s4k-admin-toolbox/blob/main/LICENSE). Free to use and modify. diff --git a/tools/linux/compare-dirs/compare-dirs.sh b/tools/linux/compare-dirs/compare-dirs.sh new file mode 100755 index 0000000..2608e51 --- /dev/null +++ b/tools/linux/compare-dirs/compare-dirs.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# ============================================================================= +# Script Name : compare-dirs.sh +# Description : Compares files with the same names in two folders. +# File names are taken from the first folder (DIR_A). +# Usage : ./compare-dirs.sh [options] +# Options : -o - Save the full report to a file (instead of ./compare-dirs.log) +# -q - Quiet — summary only, no diff output +# -h - Show this help +# Author : syr4ok (Andrii Syrovatko) +# Version : 1.0.0 +# ============================================================================= + +set -euo pipefail +IFS=$'\n\t' + +# Configration +LOG_FILE="./compare-dirs.log" +QUIET=false + +# COLORS +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# Logging +log() { + local level="$1"; shift + local msg="$*" + local ts + ts=$(date '+%Y-%m-%d %H:%M:%S') + echo -e "${ts} [${level}] ${msg}" | tee -a "${LOG_FILE}" +} +log_info() { log "INFO " "${GREEN}${*}${NC}"; } +log_warn() { log "WARN " "${YELLOW}${*}${NC}"; } +log_error() { log "ERROR" "${RED}${*}${NC}"; } + +# Helpers +print_header() { + echo -e "\n${BOLD}${BLUE}========================================${NC}" + echo -e "${BOLD}${BLUE} $1${NC}" + echo -e "${BOLD}${BLUE}========================================${NC}\n" +} + +usage() { + echo -e "${BOLD}Usage:${NC} $0 [options] " + echo "" + echo " Compares files with the same names in two folders." + echo " The list of files is taken from ." + echo "" + echo " Options:" + echo " -o Save the report to the specified file" + echo " -q Summary only (no diff output)" + echo " -h Show this help" + exit 0 +} + +check_deps() { + for cmd in "$@"; do + command -v "${cmd}" &>/dev/null || { log_error "Missing dependency: ${cmd}"; exit 1; } + done +} + +# Trap +cleanup() { + local exit_code=$? + [[ $exit_code -ne 0 ]] && log_error "Script exited with code ${exit_code}" +} +trap cleanup EXIT + +# Argument parsing +parse_args() { + while getopts ":o:qh" opt; do + case $opt in + o) LOG_FILE="${OPTARG}" ;; + q) QUIET=true ;; + h) usage ;; + :) echo -e "${RED}Option -${OPTARG} requires an argument.${NC}"; exit 1 ;; + *) echo -e "${RED}Unknown option: -${OPTARG}${NC}"; exit 1 ;; + esac + done + shift $((OPTIND - 1)) + + if [[ $# -lt 2 ]]; then + echo -e "${RED}Error: need to specify two folders.${NC}" + usage + fi + + DIR_A="$1" + DIR_B="$2" +} + +# --- MAIN --- +main() { + parse_args "$@" + + check_deps diff find + + print_header "Directory Diff: compare-dirs.sh" + log_info "DIR_A (source of file names) : ${DIR_A}" + log_info "DIR_B (comparison target) : ${DIR_B}" + + # Existing check + [[ -d "${DIR_A}" ]] || { log_error "DIR_A does not exist: ${DIR_A}"; exit 1; } + [[ -d "${DIR_B}" ]] || { log_error "DIR_B does not exist: ${DIR_B}"; exit 1; } + + local count_identical=0 + local count_different=0 + local count_missing=0 + local total=0 + + # Recursively take all files from DIR_A + while IFS= read -r -d '' rel_file; do + (( total++ )) || true + + file_a="${DIR_A}/${rel_file}" + file_b="${DIR_B}/${rel_file}" + + echo -e "\n${BOLD}${CYAN}── File: ${rel_file}${NC}" + + # File missing in DIR_B + if [[ ! -f "${file_b}" ]]; then + log_warn "MISSING in DIR_B: ${rel_file}" + (( count_missing++ )) || true + continue + fi + + # Comparing files + if diff --color=always -u "${file_a}" "${file_b}" > /tmp/_cmp_diff_out 2>&1; then + log_info "IDENTICAL: ${rel_file}" + (( count_identical++ )) || true + else + log_warn "DIFFERENT: ${rel_file}" + (( count_different++ )) || true + # Output diff if not quiet mode + if [[ "${QUIET}" == false ]]; then + echo -e "${YELLOW}--- DIR_A/${rel_file}${NC}" + echo -e "${YELLOW}+++ DIR_B/${rel_file}${NC}" + cat /tmp/_cmp_diff_out | tee -a "${LOG_FILE}" + fi + fi + + done < <(find "${DIR_A}" -type f -printf '%P\0' | sort -z) + + rm -f /tmp/_cmp_diff_out + + # --- STDOUT --- + echo "" + print_header "Summary" + echo -e " ${BOLD}Total files checked :${NC} ${total}" + echo -e " ${GREEN}Identical : ${count_identical}${NC}" + echo -e " ${RED}Different : ${count_different}${NC}" + echo -e " ${YELLOW}Missing in DIR_B : ${count_missing}${NC}" + echo "" + log_info "Log saved to: ${LOG_FILE}" +} + +main "$@"