#!/usr/bin/env bash set -euo pipefail # Recursively scans a folder (including hidden files/folders), but only processes # human-readable text files. It: # 1) Lists files containing OLD value # 2) Asks for confirmation # 3) Creates .backup copies (AFTER confirmation) # 4) Replaces OLD -> NEW in those files # # Usage: # ./replace_hash_text_only.sh /path/to/folder OLD='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' NEW='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' if [[ $# -ne 1 ]]; then echo "Usage: $0 /path/to/folder" exit 1 fi ROOT="$1" if [[ ! -d "$ROOT" ]]; then echo "Error: '$ROOT' is not a directory." exit 1 fi for cmd in find grep perl file cp; do command -v "$cmd" >/dev/null 2>&1 || { echo "Error: required command not found: $cmd" exit 1 } done is_human_readable_text() { # file --mime output example: # text/plain; charset=utf-8 # application/octet-stream; charset=binary local mime mime="$(file --mime -b -- "$1" 2>/dev/null || true)" [[ -n "$mime" ]] || return 1 [[ "$mime" == *"charset=binary"* ]] && return 1 return 0 } declare -a MATCHED_FILES=() declare -a MATCH_COUNTS=() files_seen=0 text_files_scanned=0 non_text_skipped=0 while IFS= read -r -d '' file; do ((files_seen+=1)) if ! is_human_readable_text "$file"; then ((non_text_skipped+=1)) continue fi ((text_files_scanned+=1)) if LC_ALL=C grep -qF -- "$OLD" "$file" 2>/dev/null; then count="$(LC_ALL=C grep -oF -- "$OLD" "$file" | wc -l | tr -d ' ')" MATCHED_FILES+=("$file") MATCH_COUNTS+=("$count") fi done < <(find "$ROOT" -type f -print0) matched_total="${#MATCHED_FILES[@]}" echo "Scanned files (all types): $files_seen" echo "Text files scanned: $text_files_scanned" echo "Non-text files skipped: $non_text_skipped" echo "Files with matches: $matched_total" if (( matched_total == 0 )); then echo "No matches found in text files. Nothing to do." exit 0 fi echo echo "Potential changes:" for i in "${!MATCHED_FILES[@]}"; do idx=$((i + 1)) printf " %4d) %s (occurrences: %s)\n" \ "$idx" "${MATCHED_FILES[$i]}" "${MATCH_COUNTS[$i]}" done echo read -r -p "Proceed? This will create .backup files, then apply replacements. [y/N]: " confirm case "$confirm" in y|Y|yes|YES) ;; *) echo "Canceled. No backups created. No files modified." exit 0 ;; esac # Safety check: do not overwrite existing backups for file in "${MATCHED_FILES[@]}"; do if [[ -e "${file}.backup" ]]; then echo "Error: backup already exists: ${file}.backup" echo "Aborting without making changes." exit 1 fi done echo echo "Creating backups..." for file in "${MATCHED_FILES[@]}"; do cp -p -- "$file" "${file}.backup" echo " [backup] ${file}.backup" done echo echo "Applying replacements..." updated=0 total_replacements=0 for i in "${!MATCHED_FILES[@]}"; do file="${MATCHED_FILES[$i]}" count="${MATCH_COUNTS[$i]}" OLD="$OLD" NEW="$NEW" perl -i -pe 's/\Q$ENV{OLD}\E/$ENV{NEW}/g' "$file" ((updated+=1)) ((total_replacements+=count)) echo " [updated] $file" done echo echo "Done." echo "Updated files: $updated" echo "Total replacements: $total_replacements" echo "Backups created as: .backup"