kyx revised this gist 4 hours ago. Go to revision
1 file changed, 136 insertions
replace_hash_text_only.sh(file created)
| @@ -0,0 +1,136 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | set -euo pipefail | |
| 3 | + | ||
| 4 | + | # Recursively scans a folder (including hidden files/folders), but only processes | |
| 5 | + | # human-readable text files. It: | |
| 6 | + | # 1) Lists files containing OLD value | |
| 7 | + | # 2) Asks for confirmation | |
| 8 | + | # 3) Creates .backup copies (AFTER confirmation) | |
| 9 | + | # 4) Replaces OLD -> NEW in those files | |
| 10 | + | # | |
| 11 | + | # Usage: | |
| 12 | + | # ./replace_hash_text_only.sh /path/to/folder | |
| 13 | + | ||
| 14 | + | OLD='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | |
| 15 | + | NEW='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' | |
| 16 | + | ||
| 17 | + | if [[ $# -ne 1 ]]; then | |
| 18 | + | echo "Usage: $0 /path/to/folder" | |
| 19 | + | exit 1 | |
| 20 | + | fi | |
| 21 | + | ||
| 22 | + | ROOT="$1" | |
| 23 | + | if [[ ! -d "$ROOT" ]]; then | |
| 24 | + | echo "Error: '$ROOT' is not a directory." | |
| 25 | + | exit 1 | |
| 26 | + | fi | |
| 27 | + | ||
| 28 | + | for cmd in find grep perl file cp; do | |
| 29 | + | command -v "$cmd" >/dev/null 2>&1 || { | |
| 30 | + | echo "Error: required command not found: $cmd" | |
| 31 | + | exit 1 | |
| 32 | + | } | |
| 33 | + | done | |
| 34 | + | ||
| 35 | + | is_human_readable_text() { | |
| 36 | + | # file --mime output example: | |
| 37 | + | # text/plain; charset=utf-8 | |
| 38 | + | # application/octet-stream; charset=binary | |
| 39 | + | local mime | |
| 40 | + | mime="$(file --mime -b -- "$1" 2>/dev/null || true)" | |
| 41 | + | [[ -n "$mime" ]] || return 1 | |
| 42 | + | [[ "$mime" == *"charset=binary"* ]] && return 1 | |
| 43 | + | return 0 | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | declare -a MATCHED_FILES=() | |
| 47 | + | declare -a MATCH_COUNTS=() | |
| 48 | + | ||
| 49 | + | files_seen=0 | |
| 50 | + | text_files_scanned=0 | |
| 51 | + | non_text_skipped=0 | |
| 52 | + | ||
| 53 | + | while IFS= read -r -d '' file; do | |
| 54 | + | ((files_seen+=1)) | |
| 55 | + | ||
| 56 | + | if ! is_human_readable_text "$file"; then | |
| 57 | + | ((non_text_skipped+=1)) | |
| 58 | + | continue | |
| 59 | + | fi | |
| 60 | + | ||
| 61 | + | ((text_files_scanned+=1)) | |
| 62 | + | ||
| 63 | + | if LC_ALL=C grep -qF -- "$OLD" "$file" 2>/dev/null; then | |
| 64 | + | count="$(LC_ALL=C grep -oF -- "$OLD" "$file" | wc -l | tr -d ' ')" | |
| 65 | + | MATCHED_FILES+=("$file") | |
| 66 | + | MATCH_COUNTS+=("$count") | |
| 67 | + | fi | |
| 68 | + | done < <(find "$ROOT" -type f -print0) | |
| 69 | + | ||
| 70 | + | matched_total="${#MATCHED_FILES[@]}" | |
| 71 | + | ||
| 72 | + | echo "Scanned files (all types): $files_seen" | |
| 73 | + | echo "Text files scanned: $text_files_scanned" | |
| 74 | + | echo "Non-text files skipped: $non_text_skipped" | |
| 75 | + | echo "Files with matches: $matched_total" | |
| 76 | + | ||
| 77 | + | if (( matched_total == 0 )); then | |
| 78 | + | echo "No matches found in text files. Nothing to do." | |
| 79 | + | exit 0 | |
| 80 | + | fi | |
| 81 | + | ||
| 82 | + | echo | |
| 83 | + | echo "Potential changes:" | |
| 84 | + | for i in "${!MATCHED_FILES[@]}"; do | |
| 85 | + | idx=$((i + 1)) | |
| 86 | + | printf " %4d) %s (occurrences: %s)\n" \ | |
| 87 | + | "$idx" "${MATCHED_FILES[$i]}" "${MATCH_COUNTS[$i]}" | |
| 88 | + | done | |
| 89 | + | ||
| 90 | + | echo | |
| 91 | + | read -r -p "Proceed? This will create .backup files, then apply replacements. [y/N]: " confirm | |
| 92 | + | case "$confirm" in | |
| 93 | + | y|Y|yes|YES) ;; | |
| 94 | + | *) | |
| 95 | + | echo "Canceled. No backups created. No files modified." | |
| 96 | + | exit 0 | |
| 97 | + | ;; | |
| 98 | + | esac | |
| 99 | + | ||
| 100 | + | # Safety check: do not overwrite existing backups | |
| 101 | + | for file in "${MATCHED_FILES[@]}"; do | |
| 102 | + | if [[ -e "${file}.backup" ]]; then | |
| 103 | + | echo "Error: backup already exists: ${file}.backup" | |
| 104 | + | echo "Aborting without making changes." | |
| 105 | + | exit 1 | |
| 106 | + | fi | |
| 107 | + | done | |
| 108 | + | ||
| 109 | + | echo | |
| 110 | + | echo "Creating backups..." | |
| 111 | + | for file in "${MATCHED_FILES[@]}"; do | |
| 112 | + | cp -p -- "$file" "${file}.backup" | |
| 113 | + | echo " [backup] ${file}.backup" | |
| 114 | + | done | |
| 115 | + | ||
| 116 | + | echo | |
| 117 | + | echo "Applying replacements..." | |
| 118 | + | updated=0 | |
| 119 | + | total_replacements=0 | |
| 120 | + | ||
| 121 | + | for i in "${!MATCHED_FILES[@]}"; do | |
| 122 | + | file="${MATCHED_FILES[$i]}" | |
| 123 | + | count="${MATCH_COUNTS[$i]}" | |
| 124 | + | ||
| 125 | + | OLD="$OLD" NEW="$NEW" perl -i -pe 's/\Q$ENV{OLD}\E/$ENV{NEW}/g' "$file" | |
| 126 | + | ((updated+=1)) | |
| 127 | + | ((total_replacements+=count)) | |
| 128 | + | echo " [updated] $file" | |
| 129 | + | done | |
| 130 | + | ||
| 131 | + | echo | |
| 132 | + | echo "Done." | |
| 133 | + | echo "Updated files: $updated" | |
| 134 | + | echo "Total replacements: $total_replacements" | |
| 135 | + | echo "Backups created as: <file>.backup" | |
| 136 | + | ||
Newer
Older