sr-search-replace 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/LICENSE +21 -0
- package/README.md +1951 -0
- package/bin/sr +37 -0
- package/package.json +61 -0
- package/scripts/install.js +40 -0
- package/sr.sh +4186 -0
package/sr.sh
ADDED
|
@@ -0,0 +1,4186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Search and Replace (sr) - Universal text replacement tool
|
|
4
|
+
# Version: 6.1.0
|
|
5
|
+
# Author: Mikhail Deynekin [ Deynekin.com ]
|
|
6
|
+
# REPO: https://github.com/paulmann
|
|
7
|
+
# Description: Recursively replaces text in files with proper escaping
|
|
8
|
+
# Enhanced with: Multi-layer binary detection, session-based rollback, predictable parsing
|
|
9
|
+
# New in 6.1.0: Enhanced configuration, extended tool parameters, improved compatibility
|
|
10
|
+
|
|
11
|
+
set -euo pipefail # Strict mode: exit on error, unset variables, pipe failures
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# ENHANCED CONFIGURABLE DEFAULTS - EDIT THESE TO CHANGE SCRIPT BEHAVIOR
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
# Default behavior settings
|
|
18
|
+
readonly SESSION_VERSION="6.1.0"
|
|
19
|
+
readonly DEFAULT_DEBUG_MODE=false
|
|
20
|
+
readonly DEFAULT_RECURSIVE_MODE=true
|
|
21
|
+
readonly DEFAULT_DRY_RUN=false
|
|
22
|
+
readonly DEFAULT_CREATE_BACKUPS=true
|
|
23
|
+
readonly DEFAULT_BACKUP_IN_FOLDER=true
|
|
24
|
+
readonly DEFAULT_FORCE_BACKUP=false
|
|
25
|
+
readonly DEFAULT_PRESERVE_OWNERSHIP=true
|
|
26
|
+
readonly DEFAULT_MAX_DEPTH=100
|
|
27
|
+
readonly DEFAULT_TIMESTAMP_FORMAT="%Y%m%d_%H%M%S"
|
|
28
|
+
readonly DEFAULT_BACKUP_PREFIX="sr.backup"
|
|
29
|
+
readonly DEFAULT_TEMP_DIR="/tmp"
|
|
30
|
+
readonly DEFAULT_SED_DELIMITER="|"
|
|
31
|
+
readonly DEFAULT_SKIP_HIDDEN_FILES=false
|
|
32
|
+
readonly DEFAULT_SKIP_BINARY_FILES=true
|
|
33
|
+
readonly DEFAULT_MAX_FILE_SIZE_MB=100
|
|
34
|
+
readonly DEFAULT_ENCODING="UTF-8"
|
|
35
|
+
readonly DEFAULT_SEARCH_DIR="."
|
|
36
|
+
readonly DEFAULT_REPLACE_MODE="inplace" # inplace, copy, or backup_only
|
|
37
|
+
|
|
38
|
+
# Binary detection settings (NEW)
|
|
39
|
+
readonly DEFAULT_BINARY_DETECTION_METHOD="multi_layer" # multi_layer, file_only, grep_only
|
|
40
|
+
readonly DEFAULT_BINARY_CHECK_SIZE=1024 # Check first N bytes for binary detection
|
|
41
|
+
readonly DEFAULT_ALLOW_BINARY=false # NEW: Require explicit flag to process binary files
|
|
42
|
+
|
|
43
|
+
# Rollback settings (NEW)
|
|
44
|
+
readonly DEFAULT_ROLLBACK_ENABLED=true
|
|
45
|
+
readonly DEFAULT_MAX_BACKUPS=10 # Keep last N backups
|
|
46
|
+
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# ENHANCED SEARCH/REPLACE PARAMETERS - APPLIED TO ALL OPERATIONS
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
# Additional parameters that will be applied to all search/replace operations
|
|
52
|
+
readonly DEFAULT_IGNORE_CASE=false # Case-insensitive search
|
|
53
|
+
readonly DEFAULT_MULTILINE_MATCH=false # Multi-line mode for regex
|
|
54
|
+
readonly DEFAULT_EXTENDED_REGEX=false # Use extended regular expressions
|
|
55
|
+
readonly DEFAULT_WORD_BOUNDARY=false # Match whole words only
|
|
56
|
+
readonly DEFAULT_LINE_NUMBERS=false # Show line numbers in output
|
|
57
|
+
readonly DEFAULT_DOT_ALL=false # Dot matches newline (sed 's' flag)
|
|
58
|
+
readonly DEFAULT_GLOBAL_REPLACE=true # Replace all occurrences (global)
|
|
59
|
+
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# TOOL CONFIGURATION - BASE COMMANDS AND DEFAULT FLAGS
|
|
62
|
+
# ============================================================================
|
|
63
|
+
|
|
64
|
+
# Base tool commands - can be overridden if needed
|
|
65
|
+
readonly FIND_TOOL="find"
|
|
66
|
+
readonly SED_TOOL="sed"
|
|
67
|
+
readonly GREP_TOOL="grep"
|
|
68
|
+
|
|
69
|
+
# Default tool flags (can be extended via command-line options)
|
|
70
|
+
readonly DEFAULT_FIND_FLAGS="" # Additional find flags
|
|
71
|
+
readonly DEFAULT_SED_FLAGS="" # Additional sed flags
|
|
72
|
+
readonly DEFAULT_GREP_FLAGS="-F" # Default: Fixed string matching
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# COLOR SCHEME CUSTOMIZATION
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
readonly COLOR_INFO='\033[0;34m' # Blue
|
|
79
|
+
readonly COLOR_SUCCESS='\033[0;32m' # Green
|
|
80
|
+
readonly COLOR_WARNING='\033[0;33m' # Yellow
|
|
81
|
+
readonly COLOR_ERROR='\033[0;31m' # Red
|
|
82
|
+
readonly COLOR_DEBUG='\033[0;36m' # Cyan
|
|
83
|
+
readonly COLOR_HEADER='\033[0;35m' # Magenta
|
|
84
|
+
readonly COLOR_RESET='\033[0m'
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# EXCLUSION PATTERNS
|
|
88
|
+
# ============================================================================
|
|
89
|
+
|
|
90
|
+
# File patterns to exclude by default (space-separated)
|
|
91
|
+
readonly DEFAULT_EXCLUDE_PATTERNS=".git .svn .hg .DS_Store *.bak *.backup"
|
|
92
|
+
readonly DEFAULT_EXCLUDE_DIRS="node_modules __pycache__ .cache .idea .vscode"
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# GLOBAL VARIABLES (Initialized with defaults)
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
# Core variables
|
|
99
|
+
declare -g SED_INPLACE_FLAG="-i"
|
|
100
|
+
declare -g DEBUG_MODE="$DEFAULT_DEBUG_MODE"
|
|
101
|
+
declare -g RECURSIVE_MODE="$DEFAULT_RECURSIVE_MODE"
|
|
102
|
+
declare -g FILE_PATTERN=""
|
|
103
|
+
declare -g SEARCH_STRING=""
|
|
104
|
+
declare -g REPLACE_STRING=""
|
|
105
|
+
declare -gi PROCESSED_FILES=0
|
|
106
|
+
declare -gi MODIFIED_FILES=0
|
|
107
|
+
declare -gi TOTAL_REPLACEMENTS=0
|
|
108
|
+
declare -g DRY_RUN="$DEFAULT_DRY_RUN"
|
|
109
|
+
declare -g CREATE_BACKUPS="$DEFAULT_CREATE_BACKUPS"
|
|
110
|
+
declare -g BACKUP_IN_FOLDER="$DEFAULT_BACKUP_IN_FOLDER"
|
|
111
|
+
declare -g FORCE_BACKUP="$DEFAULT_FORCE_BACKUP"
|
|
112
|
+
declare -g PRESERVE_OWNERSHIP="$DEFAULT_PRESERVE_OWNERSHIP"
|
|
113
|
+
declare -g BACKUP_DIR=""
|
|
114
|
+
declare -g FIRST_FILE_OWNER=""
|
|
115
|
+
declare -g FIRST_FILE_GROUP=""
|
|
116
|
+
declare -g MAX_DEPTH="$DEFAULT_MAX_DEPTH"
|
|
117
|
+
declare -g TIMESTAMP_FORMAT="$DEFAULT_TIMESTAMP_FORMAT"
|
|
118
|
+
declare -g BACKUP_PREFIX="$DEFAULT_BACKUP_PREFIX"
|
|
119
|
+
declare -g TEMP_DIR="$DEFAULT_TEMP_DIR"
|
|
120
|
+
declare -g SED_DELIMITER="$DEFAULT_SED_DELIMITER"
|
|
121
|
+
declare -g SKIP_HIDDEN_FILES="$DEFAULT_SKIP_HIDDEN_FILES"
|
|
122
|
+
declare -g SKIP_BINARY_FILES="$DEFAULT_SKIP_BINARY_FILES"
|
|
123
|
+
declare -gi MAX_FILE_SIZE="$((DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024))"
|
|
124
|
+
declare -g EXCLUDE_PATTERNS="$DEFAULT_EXCLUDE_PATTERNS"
|
|
125
|
+
declare -g EXCLUDE_DIRS="$DEFAULT_EXCLUDE_DIRS"
|
|
126
|
+
declare -g SEARCH_DIR="$DEFAULT_SEARCH_DIR"
|
|
127
|
+
declare -g REPLACE_MODE="$DEFAULT_REPLACE_MODE"
|
|
128
|
+
declare -g OUTPUT_DIR=""
|
|
129
|
+
|
|
130
|
+
# Enhanced functionality variables
|
|
131
|
+
declare -g ALLOW_BINARY="$DEFAULT_ALLOW_BINARY"
|
|
132
|
+
declare -g BINARY_DETECTION_METHOD="$DEFAULT_BINARY_DETECTION_METHOD"
|
|
133
|
+
declare -gi BINARY_CHECK_SIZE="$DEFAULT_BINARY_CHECK_SIZE"
|
|
134
|
+
declare -gi MAX_BACKUPS="$DEFAULT_MAX_BACKUPS"
|
|
135
|
+
declare -g VERBOSE_MODE=false
|
|
136
|
+
declare -g SESSION_ID=""
|
|
137
|
+
declare -g SESSION_START_TIME=""
|
|
138
|
+
declare -ga SESSION_INITIAL_ARGS=()
|
|
139
|
+
declare -ga SESSION_MODIFIED_FILES=()
|
|
140
|
+
declare -gi RESTORED_COUNT=0
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# ENHANCED SEARCH/REPLACE VARIABLES (NEW IN 6.1.0)
|
|
144
|
+
# ============================================================================
|
|
145
|
+
|
|
146
|
+
declare -g IGNORE_CASE="$DEFAULT_IGNORE_CASE"
|
|
147
|
+
declare -g MULTILINE_MATCH="$DEFAULT_MULTILINE_MATCH"
|
|
148
|
+
declare -g EXTENDED_REGEX="$DEFAULT_EXTENDED_REGEX"
|
|
149
|
+
declare -g WORD_BOUNDARY="$DEFAULT_WORD_BOUNDARY"
|
|
150
|
+
declare -g LINE_NUMBERS="$DEFAULT_LINE_NUMBERS"
|
|
151
|
+
declare -g DOT_ALL="$DEFAULT_DOT_ALL"
|
|
152
|
+
declare -g GLOBAL_REPLACE="$DEFAULT_GLOBAL_REPLACE"
|
|
153
|
+
|
|
154
|
+
# Tool-specific configuration variables
|
|
155
|
+
declare -g FIND_FLAGS="$DEFAULT_FIND_FLAGS"
|
|
156
|
+
declare -g SED_FLAGS="$DEFAULT_SED_FLAGS"
|
|
157
|
+
declare -g GREP_FLAGS="$DEFAULT_GREP_FLAGS"
|
|
158
|
+
|
|
159
|
+
# ============================================================================
|
|
160
|
+
# LOGGING FUNCTIONS
|
|
161
|
+
# ============================================================================
|
|
162
|
+
|
|
163
|
+
log_info() {
|
|
164
|
+
echo -e "${COLOR_INFO}[INFO]${COLOR_RESET} $*"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log_success() {
|
|
168
|
+
echo -e "${COLOR_SUCCESS}[SUCCESS]${COLOR_RESET} $*"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
log_warning() {
|
|
172
|
+
echo -e "${COLOR_WARNING}[WARNING]${COLOR_RESET} $*"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
log_error() {
|
|
176
|
+
echo -e "${COLOR_ERROR}[ERROR]${COLOR_RESET} $*" >&2
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log_debug() {
|
|
180
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
181
|
+
echo -e "${COLOR_DEBUG}[DEBUG]${COLOR_RESET} $*" >&2
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
log_verbose() {
|
|
186
|
+
if [[ "$VERBOSE_MODE" == true ]]; then
|
|
187
|
+
echo -e "${COLOR_INFO}[VERBOSE]${COLOR_RESET} $*"
|
|
188
|
+
fi
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
log_header() {
|
|
192
|
+
echo -e "${COLOR_HEADER}$*${COLOR_RESET}"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# ============================================================================
|
|
196
|
+
# ENHANCED UTILITY FUNCTIONS
|
|
197
|
+
# ============================================================================
|
|
198
|
+
|
|
199
|
+
# Generate unique session ID
|
|
200
|
+
generate_session_id() {
|
|
201
|
+
date +"%Y%m%d_%H%M%S_%N"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
confirm_action_timeout() {
|
|
205
|
+
local prompt="${1:?Prompt required}"
|
|
206
|
+
local default="${2:-n}"
|
|
207
|
+
local timeout="${3:-0}"
|
|
208
|
+
|
|
209
|
+
# Support yes characters in multiple languages
|
|
210
|
+
local yes_chars='yYндδчμźжןมќอყςяﻱゅッзЗึីུྭᴐउᴕัิೆඇෂხขฺஸᴑॄेxნេхХᴅኔିุើሐჯОعوዪтٸzയႤଂଠ०ுஹՀйЙ'
|
|
211
|
+
local no_chars='nNтТхХுೆೇಿݢৎᴗขნिኔოེỶᴞํԛઊാԺюцࢲັིᴌଂೢັනცತีถูხืΧuዘᴧὴܘᴎτ'
|
|
212
|
+
|
|
213
|
+
local full_prompt="$prompt"
|
|
214
|
+
[[ $timeout -gt 0 ]] && full_prompt+=" (timeout ${timeout}s)"
|
|
215
|
+
|
|
216
|
+
local response
|
|
217
|
+
local format_prompt
|
|
218
|
+
|
|
219
|
+
[[ "$default" == "y" ]] && format_prompt="$full_prompt [Y/n]: " || format_prompt="$full_prompt [y/N]: "
|
|
220
|
+
|
|
221
|
+
if [[ $timeout -gt 0 ]]; then
|
|
222
|
+
read -t "$timeout" -p "$format_prompt" -r -n 1 response
|
|
223
|
+
|
|
224
|
+
if [[ $? -ne 0 ]]; then
|
|
225
|
+
echo
|
|
226
|
+
log_debug "Timeout: using default '$default'"
|
|
227
|
+
response="$default"
|
|
228
|
+
fi
|
|
229
|
+
else
|
|
230
|
+
read -p "$format_prompt" -r -n 1 response
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
echo
|
|
234
|
+
response="${response:-$default}"
|
|
235
|
+
|
|
236
|
+
# Check against multilingual yes/no characters
|
|
237
|
+
if [[ "$yes_chars" == *"$response"* ]]; then
|
|
238
|
+
return 0
|
|
239
|
+
elif [[ "$no_chars" == *"$response"* ]]; then
|
|
240
|
+
return 1
|
|
241
|
+
else
|
|
242
|
+
[[ "$default" == "y" ]] && return 0 || return 1
|
|
243
|
+
fi
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Multi-layer binary file detection
|
|
247
|
+
is_binary_file() {
|
|
248
|
+
local file="$1"
|
|
249
|
+
local method="${2:-$BINARY_DETECTION_METHOD}"
|
|
250
|
+
|
|
251
|
+
[[ ! -f "$file" ]] && {
|
|
252
|
+
log_debug "is_binary_file: $file is not a file"
|
|
253
|
+
return 1
|
|
254
|
+
}
|
|
255
|
+
[[ ! -r "$file" ]] && {
|
|
256
|
+
log_debug "is_binary_file: $file is not readable"
|
|
257
|
+
return 1
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
log_debug "Checking if file is binary: $file, method: $method"
|
|
261
|
+
|
|
262
|
+
case "$method" in
|
|
263
|
+
"file_only")
|
|
264
|
+
# Method 1: Use file utility with MIME type
|
|
265
|
+
if command -v file >/dev/null 2>&1; then
|
|
266
|
+
local mime_type
|
|
267
|
+
mime_type=$(file --mime-type "$file" 2>/dev/null | cut -d: -f2 | xargs)
|
|
268
|
+
if [[ "$mime_type" == text/* ]]; then
|
|
269
|
+
return 1 # Not binary
|
|
270
|
+
elif [[ -n "$mime_type" ]]; then
|
|
271
|
+
log_verbose "Binary detected by file utility: $file ($mime_type)"
|
|
272
|
+
return 0 # Binary
|
|
273
|
+
fi
|
|
274
|
+
fi
|
|
275
|
+
;;
|
|
276
|
+
|
|
277
|
+
"grep_only")
|
|
278
|
+
# Method 2: Use grep -I heuristic (fast and portable)
|
|
279
|
+
if ! head -c "$BINARY_CHECK_SIZE" "$file" 2>/dev/null | grep -qI .; then
|
|
280
|
+
log_verbose "Binary detected by grep heuristic: $file"
|
|
281
|
+
return 0 # Binary
|
|
282
|
+
fi
|
|
283
|
+
;;
|
|
284
|
+
|
|
285
|
+
"multi_layer" | *)
|
|
286
|
+
# Method 3: Multi-layer detection (default)
|
|
287
|
+
# Layer 1: Quick size check
|
|
288
|
+
if [[ ! -s "$file" ]]; then
|
|
289
|
+
return 1 # Empty files are not binary for our purposes
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# Layer 2: Fast grep heuristic (main method)
|
|
293
|
+
if ! head -c "$BINARY_CHECK_SIZE" "$file" 2>/dev/null | grep -qI .; then
|
|
294
|
+
# Layer 3: Verify with file utility if available
|
|
295
|
+
if command -v file >/dev/null 2>&1; then
|
|
296
|
+
local mime_type
|
|
297
|
+
mime_type=$(file --mime-type "$file" 2>/dev/null | cut -d: -f2 | xargs)
|
|
298
|
+
if [[ "$mime_type" == text/* ]]; then
|
|
299
|
+
log_verbose "File utility overrides: $file is text ($mime_type)"
|
|
300
|
+
return 1 # Not binary (file utility says text)
|
|
301
|
+
elif [[ -n "$mime_type" ]]; then
|
|
302
|
+
log_verbose "Binary confirmed by file utility: $file ($mime_type)"
|
|
303
|
+
return 0 # Binary
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
log_verbose "Binary detected by multi-layer check: $file"
|
|
307
|
+
return 0 # Binary
|
|
308
|
+
fi
|
|
309
|
+
;;
|
|
310
|
+
esac
|
|
311
|
+
|
|
312
|
+
return 1 # Not binary
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Session management
|
|
316
|
+
init_session() {
|
|
317
|
+
SESSION_ID=$(generate_session_id)
|
|
318
|
+
SESSION_START_TIME=$(date +"%Y-%m-%d %H:%M:%S")
|
|
319
|
+
|
|
320
|
+
log_debug "Session initialized: $SESSION_ID"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Track modified file and update metadata immediately
|
|
324
|
+
track_modified_file() {
|
|
325
|
+
local file="$1"
|
|
326
|
+
|
|
327
|
+
# Initialize array if not already done
|
|
328
|
+
if ! declare -p SESSION_MODIFIED_FILES &>/dev/null 2>&1; then
|
|
329
|
+
declare -g SESSION_MODIFIED_FILES=()
|
|
330
|
+
log_debug "Initialized SESSION_MODIFIED_FILES array in track_modified_file"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# Check if file is already tracked
|
|
334
|
+
local found=false
|
|
335
|
+
if [[ ${#SESSION_MODIFIED_FILES[@]} -gt 0 ]]; then
|
|
336
|
+
for existing_file in "${SESSION_MODIFIED_FILES[@]}"; do
|
|
337
|
+
if [[ "$existing_file" == "$file" ]]; then
|
|
338
|
+
found=true
|
|
339
|
+
break
|
|
340
|
+
fi
|
|
341
|
+
done
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# Add file to array if not already present
|
|
345
|
+
if [[ "$found" == false ]]; then
|
|
346
|
+
SESSION_MODIFIED_FILES+=("$file")
|
|
347
|
+
log_debug "Tracked modified file: $file (total: ${#SESSION_MODIFIED_FILES[@]})"
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Update backup file list
|
|
351
|
+
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
352
|
+
update_backup_filelist
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Update backup file list
|
|
357
|
+
update_backup_filelist() {
|
|
358
|
+
[[ -z "$BACKUP_DIR" ]] && return 0
|
|
359
|
+
[[ ! -d "$BACKUP_DIR" ]] && return 0
|
|
360
|
+
|
|
361
|
+
local filelist_file="$BACKUP_DIR/.sr_modified_files"
|
|
362
|
+
local temp_file
|
|
363
|
+
temp_file=$(mktemp 2>/dev/null || echo "$BACKUP_DIR/.sr_temp_$$")
|
|
364
|
+
|
|
365
|
+
# Write all tracked files to temp file
|
|
366
|
+
if [[ -n "${SESSION_MODIFIED_FILES+set}" ]] && [[ ${#SESSION_MODIFIED_FILES[@]} -gt 0 ]]; then
|
|
367
|
+
for file in "${SESSION_MODIFIED_FILES[@]}"; do
|
|
368
|
+
local relative_path
|
|
369
|
+
|
|
370
|
+
# Get relative path
|
|
371
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
372
|
+
relative_path=$(realpath --relative-to="." "$file" 2>/dev/null || echo "$file")
|
|
373
|
+
else
|
|
374
|
+
if [[ "$file" == /* ]]; then
|
|
375
|
+
relative_path="${file#$(pwd)/}"
|
|
376
|
+
[[ "$relative_path" == "$file" ]] && relative_path=$(basename "$file")
|
|
377
|
+
else
|
|
378
|
+
relative_path="$file"
|
|
379
|
+
fi
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# Clean up relative path
|
|
383
|
+
relative_path="${relative_path#./}"
|
|
384
|
+
echo "$relative_path" >>"$temp_file"
|
|
385
|
+
done
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# Move temp file to final location
|
|
389
|
+
if [[ -s "$temp_file" ]]; then
|
|
390
|
+
mv "$temp_file" "$filelist_file" 2>/dev/null || cp "$temp_file" "$filelist_file"
|
|
391
|
+
log_debug "Updated backup file list: ${#SESSION_MODIFIED_FILES[@]} files"
|
|
392
|
+
else
|
|
393
|
+
rm -f "$temp_file"
|
|
394
|
+
fi
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Save initial session metadata when backup directory is created
|
|
398
|
+
save_initial_session_metadata() {
|
|
399
|
+
local backup_dir="$1"
|
|
400
|
+
SESSION_METADATA_FILE="$backup_dir/.sr_session_metadata"
|
|
401
|
+
|
|
402
|
+
# Build full command
|
|
403
|
+
local full_command="$0"
|
|
404
|
+
for arg in "${SESSION_INITIAL_ARGS[@]}"; do
|
|
405
|
+
if [[ "$arg" =~ [[:space:]\;\&\|\<\>\(\)\{\}\[\]] ]]; then
|
|
406
|
+
full_command+=" \"$arg\""
|
|
407
|
+
else
|
|
408
|
+
full_command+=" $arg"
|
|
409
|
+
fi
|
|
410
|
+
done
|
|
411
|
+
|
|
412
|
+
cat >"$SESSION_METADATA_FILE" <<EOF
|
|
413
|
+
# Session metadata for sr.sh
|
|
414
|
+
SESSION_ID="$SESSION_ID"
|
|
415
|
+
SESSION_START_TIME="$SESSION_START_TIME"
|
|
416
|
+
SESSION_END_TIME=""
|
|
417
|
+
SESSION_COMMAND="$full_command"
|
|
418
|
+
SESSION_PATTERN="$FILE_PATTERN"
|
|
419
|
+
SESSION_SEARCH="$SEARCH_STRING"
|
|
420
|
+
SESSION_REPLACE="$REPLACE_STRING"
|
|
421
|
+
SESSION_RECURSIVE="$RECURSIVE_MODE"
|
|
422
|
+
SESSION_ALLOW_BINARY="$ALLOW_BINARY"
|
|
423
|
+
SESSION_CREATE_BACKUPS="$CREATE_BACKUPS"
|
|
424
|
+
SESSION_DRY_RUN="$DRY_RUN"
|
|
425
|
+
SESSION_IGNORE_CASE="$IGNORE_CASE"
|
|
426
|
+
SESSION_EXTENDED_REGEX="$EXTENDED_REGEX"
|
|
427
|
+
SESSION_WORD_BOUNDARY="$WORD_BOUNDARY"
|
|
428
|
+
SESSION_MULTILINE="$MULTILINE_MATCH"
|
|
429
|
+
SESSION_LINE_NUMBERS="$LINE_NUMBERS"
|
|
430
|
+
SESSION_MODIFIED_COUNT=0
|
|
431
|
+
SESSION_TOTAL_REPLACEMENTS=0
|
|
432
|
+
SESSION_VERSION="$SESSION_VERSION"
|
|
433
|
+
EOF
|
|
434
|
+
|
|
435
|
+
# Create empty modified files list file
|
|
436
|
+
local filelist_file="$backup_dir/.sr_modified_files"
|
|
437
|
+
>"$filelist_file"
|
|
438
|
+
|
|
439
|
+
# Create file with additional information
|
|
440
|
+
local fileinfo_file="$backup_dir/.sr_file_info"
|
|
441
|
+
cat >"$fileinfo_file" <<EOF
|
|
442
|
+
# File information for session $SESSION_ID
|
|
443
|
+
# Generated: $SESSION_START_TIME
|
|
444
|
+
# Command: $full_command
|
|
445
|
+
# Pattern: $FILE_PATTERN
|
|
446
|
+
# Search: $SEARCH_STRING
|
|
447
|
+
# Replace: $REPLACE_STRING
|
|
448
|
+
# Backup directory: $backup_dir
|
|
449
|
+
#
|
|
450
|
+
# Modified files will be listed below as they are processed:
|
|
451
|
+
EOF
|
|
452
|
+
|
|
453
|
+
log_debug "Initial session metadata saved: $SESSION_METADATA_FILE"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
finalize_session_metadata() {
|
|
457
|
+
[[ -z "$BACKUP_DIR" ]] && return 0
|
|
458
|
+
[[ ! -d "$BACKUP_DIR" ]] && return 0
|
|
459
|
+
|
|
460
|
+
# Build full command
|
|
461
|
+
local full_command="$0"
|
|
462
|
+
for arg in "${SESSION_INITIAL_ARGS[@]}"; do
|
|
463
|
+
if [[ "$arg" =~ [[:space:]\;\&\|\<\>\(\)\{\}\[\]] ]]; then
|
|
464
|
+
full_command+=" \"$arg\""
|
|
465
|
+
else
|
|
466
|
+
full_command+=" $arg"
|
|
467
|
+
fi
|
|
468
|
+
done
|
|
469
|
+
|
|
470
|
+
# Get array size
|
|
471
|
+
local array_size=0
|
|
472
|
+
if [[ -n "${SESSION_MODIFIED_FILES+set}" ]]; then
|
|
473
|
+
array_size=${#SESSION_MODIFIED_FILES[@]}
|
|
474
|
+
log_debug "finalize_session_metadata: SESSION_MODIFIED_FILES has $array_size items"
|
|
475
|
+
else
|
|
476
|
+
log_warning "SESSION_MODIFIED_FILES array is not set, using 0"
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
# Update session metadata
|
|
480
|
+
cat >"$BACKUP_DIR/.sr_session_metadata" <<EOF
|
|
481
|
+
# Session metadata for sr.sh
|
|
482
|
+
SESSION_ID="$SESSION_ID"
|
|
483
|
+
SESSION_START_TIME="$SESSION_START_TIME"
|
|
484
|
+
SESSION_END_TIME="$(date +"%Y-%m-%d %H:%M:%S")"
|
|
485
|
+
SESSION_COMMAND="$full_command"
|
|
486
|
+
SESSION_PATTERN="$FILE_PATTERN"
|
|
487
|
+
SESSION_SEARCH="$SEARCH_STRING"
|
|
488
|
+
SESSION_REPLACE="$REPLACE_STRING"
|
|
489
|
+
SESSION_RECURSIVE="$RECURSIVE_MODE"
|
|
490
|
+
SESSION_ALLOW_BINARY="$ALLOW_BINARY"
|
|
491
|
+
SESSION_CREATE_BACKUPS="$CREATE_BACKUPS"
|
|
492
|
+
SESSION_DRY_RUN="$DRY_RUN"
|
|
493
|
+
SESSION_IGNORE_CASE="$IGNORE_CASE"
|
|
494
|
+
SESSION_EXTENDED_REGEX="$EXTENDED_REGEX"
|
|
495
|
+
SESSION_WORD_BOUNDARY="$WORD_BOUNDARY"
|
|
496
|
+
SESSION_MULTILINE="$MULTILINE_MATCH"
|
|
497
|
+
SESSION_LINE_NUMBERS="$LINE_NUMBERS"
|
|
498
|
+
SESSION_MODIFIED_COUNT=$array_size
|
|
499
|
+
SESSION_TOTAL_REPLACEMENTS=$TOTAL_REPLACEMENTS
|
|
500
|
+
SESSION_VERSION="$SESSION_VERSION"
|
|
501
|
+
EOF
|
|
502
|
+
|
|
503
|
+
# Update file information
|
|
504
|
+
local fileinfo_file="$BACKUP_DIR/.sr_file_info"
|
|
505
|
+
if [[ -f "$fileinfo_file" ]]; then
|
|
506
|
+
cat >>"$fileinfo_file" <<EOF
|
|
507
|
+
|
|
508
|
+
# Processing completed at: $(date +"%Y-%m-%d %H:%M:%S")
|
|
509
|
+
# Total files modified: $array_size
|
|
510
|
+
# Total replacements made: $TOTAL_REPLACEMENTS
|
|
511
|
+
|
|
512
|
+
# List of all modified files:
|
|
513
|
+
EOF
|
|
514
|
+
|
|
515
|
+
if [[ -n "${SESSION_MODIFIED_FILES+set}" ]] && [[ $array_size -gt 0 ]]; then
|
|
516
|
+
for file in "${SESSION_MODIFIED_FILES[@]}"; do
|
|
517
|
+
local relative_path
|
|
518
|
+
|
|
519
|
+
# Get relative path
|
|
520
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
521
|
+
relative_path=$(realpath --relative-to="." "$file" 2>/dev/null || echo "$file")
|
|
522
|
+
else
|
|
523
|
+
if [[ "$file" == /* ]]; then
|
|
524
|
+
relative_path="${file#$(pwd)/}"
|
|
525
|
+
[[ "$relative_path" == "$file" ]] && relative_path=$(basename "$file")
|
|
526
|
+
else
|
|
527
|
+
relative_path="$file"
|
|
528
|
+
fi
|
|
529
|
+
fi
|
|
530
|
+
|
|
531
|
+
relative_path="${relative_path#./}"
|
|
532
|
+
echo "$relative_path" >>"$fileinfo_file"
|
|
533
|
+
log_debug "Added to .sr_file_info: $relative_path"
|
|
534
|
+
done
|
|
535
|
+
else
|
|
536
|
+
echo "# No files were modified in this session" >>"$fileinfo_file"
|
|
537
|
+
fi
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
log_debug "Finalized session metadata with $array_size files"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# ============================================================================
|
|
544
|
+
# ENHANCED CORE UTILITY FUNCTIONS WITH TOOL FLAGS SUPPORT
|
|
545
|
+
# ============================================================================
|
|
546
|
+
|
|
547
|
+
escape_regex() {
|
|
548
|
+
local string="$1"
|
|
549
|
+
echo "$string" | sed -e 's/[][\/.^$*+?{}|()]/\\&/g'
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
escape_replacement() {
|
|
553
|
+
local string="$1"
|
|
554
|
+
echo "$string" | sed -e 's/[\/&]/\\&/g' -e ':a;N;$!ba;s/\n/\\n/g'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# Function to check if a directory exists and is readable
|
|
558
|
+
check_directory() {
|
|
559
|
+
local dir="$1"
|
|
560
|
+
local desc="$2"
|
|
561
|
+
|
|
562
|
+
if [[ ! -d "$dir" ]]; then
|
|
563
|
+
log_error "$desc directory does not exist: $dir"
|
|
564
|
+
return 1
|
|
565
|
+
fi
|
|
566
|
+
|
|
567
|
+
if [[ ! -r "$dir" ]]; then
|
|
568
|
+
log_error "$desc directory is not readable: $dir"
|
|
569
|
+
return 1
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
if [[ ! -x "$dir" ]]; then
|
|
573
|
+
log_error "$desc directory is not accessible: $dir"
|
|
574
|
+
return 1
|
|
575
|
+
fi
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# Function to get absolute path
|
|
579
|
+
get_absolute_path() {
|
|
580
|
+
local path="$1"
|
|
581
|
+
if [[ "$path" == /* ]]; then
|
|
582
|
+
echo "$path"
|
|
583
|
+
else
|
|
584
|
+
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
|
|
585
|
+
fi
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# ============================================================================
|
|
589
|
+
# ENHANCED ENVIRONMENT VALIDATION FUNCTION WITH TOOL CHECKING
|
|
590
|
+
# ============================================================================
|
|
591
|
+
|
|
592
|
+
validate_environment() {
|
|
593
|
+
local required_cmds=("$FIND_TOOL" "$SED_TOOL" "$GREP_TOOL")
|
|
594
|
+
|
|
595
|
+
for cmd in "${required_cmds[@]}"; do
|
|
596
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
597
|
+
log_error "Required command not found: $cmd"
|
|
598
|
+
exit 1
|
|
599
|
+
fi
|
|
600
|
+
done
|
|
601
|
+
|
|
602
|
+
# Check for file utility (used in binary detection)
|
|
603
|
+
if [[ "$BINARY_DETECTION_METHOD" == "file_only" || "$BINARY_DETECTION_METHOD" == "multi_layer" ]]; then
|
|
604
|
+
if ! command -v file >/dev/null 2>&1; then
|
|
605
|
+
log_warning "file utility not found. Binary detection may be limited."
|
|
606
|
+
log_warning "Consider using --binary-method=grep_only or installing file utility."
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
|
|
610
|
+
# Detect sed type (GNU vs BSD)
|
|
611
|
+
if sed --version 2>/dev/null | grep -q "GNU"; then
|
|
612
|
+
log_debug "Using GNU sed"
|
|
613
|
+
else
|
|
614
|
+
SED_INPLACE_FLAG="-i ''"
|
|
615
|
+
log_debug "Using BSD sed"
|
|
616
|
+
fi
|
|
617
|
+
|
|
618
|
+
# Validate MAX_DEPTH
|
|
619
|
+
if ! [[ "$MAX_DEPTH" =~ ^[0-9]+$ ]] || [[ "$MAX_DEPTH" -lt 1 ]]; then
|
|
620
|
+
log_warning "Invalid MAX_DEPTH ($MAX_DEPTH), using default: $DEFAULT_MAX_DEPTH"
|
|
621
|
+
MAX_DEPTH="$DEFAULT_MAX_DEPTH"
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
# Validate TEMP_DIR
|
|
625
|
+
if [[ ! -d "$TEMP_DIR" ]] || [[ ! -w "$TEMP_DIR" ]]; then
|
|
626
|
+
log_warning "Temp directory $TEMP_DIR is not accessible, using /tmp"
|
|
627
|
+
TEMP_DIR="/tmp"
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
# Validate search directory
|
|
631
|
+
if ! check_directory "$SEARCH_DIR" "Search"; then
|
|
632
|
+
exit 1
|
|
633
|
+
fi
|
|
634
|
+
|
|
635
|
+
# Validate output directory if specified
|
|
636
|
+
if [[ -n "$OUTPUT_DIR" ]]; then
|
|
637
|
+
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
|
638
|
+
log_debug "Creating output directory: $OUTPUT_DIR"
|
|
639
|
+
mkdir -p "$OUTPUT_DIR" || {
|
|
640
|
+
log_error "Failed to create output directory: $OUTPUT_DIR"
|
|
641
|
+
exit 1
|
|
642
|
+
}
|
|
643
|
+
fi
|
|
644
|
+
if ! check_directory "$OUTPUT_DIR" "Output"; then
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
# Adjust CREATE_BACKUPS based on replace mode
|
|
650
|
+
if [[ "$REPLACE_MODE" == "backup_only" ]]; then
|
|
651
|
+
CREATE_BACKUPS=true
|
|
652
|
+
FORCE_BACKUP=true
|
|
653
|
+
log_debug "Backup-only mode: backups forced"
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
# Validate binary check size
|
|
657
|
+
if ! [[ "$BINARY_CHECK_SIZE" =~ ^[0-9]+$ ]] || [[ "$BINARY_CHECK_SIZE" -lt 1 ]]; then
|
|
658
|
+
log_warning "Invalid BINARY_CHECK_SIZE ($BINARY_CHECK_SIZE), using default: $DEFAULT_BINARY_CHECK_SIZE"
|
|
659
|
+
BINARY_CHECK_SIZE="$DEFAULT_BINARY_CHECK_SIZE"
|
|
660
|
+
fi
|
|
661
|
+
|
|
662
|
+
# Validate MAX_BACKUPS
|
|
663
|
+
if ! [[ "$MAX_BACKUPS" =~ ^[0-9]+$ ]] || [[ "$MAX_BACKUPS" -lt 0 ]]; then
|
|
664
|
+
log_warning "Invalid MAX_BACKUPS ($MAX_BACKUPS), using default: $DEFAULT_MAX_BACKUPS"
|
|
665
|
+
MAX_BACKUPS="$DEFAULT_MAX_BACKUPS"
|
|
666
|
+
fi
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
# ============================================================================
|
|
670
|
+
# ENHANCED ROLLBACK SYSTEM
|
|
671
|
+
# ============================================================================
|
|
672
|
+
|
|
673
|
+
# Enhanced rollback functionality with step-by-step debugging
|
|
674
|
+
perform_rollback() {
|
|
675
|
+
local target_backup="${1:-latest}"
|
|
676
|
+
local backup_dirs=()
|
|
677
|
+
local selected_backup=""
|
|
678
|
+
local files_to_restore=()
|
|
679
|
+
|
|
680
|
+
# Handle relative paths
|
|
681
|
+
if [[ ! -d "$target_backup" ]] && [[ "$target_backup" != "latest" ]]; then
|
|
682
|
+
if [[ -d "./$target_backup" ]]; then
|
|
683
|
+
target_backup="./$target_backup"
|
|
684
|
+
log_debug "DEBUG: Adjusted backup path to: '$target_backup'"
|
|
685
|
+
fi
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
log_header "=== ROLLBACK SYSTEM ==="
|
|
689
|
+
log_debug "DEBUG [1/10]: Function perform_rollback started with arg: '$target_backup'"
|
|
690
|
+
|
|
691
|
+
# Find all backup directories
|
|
692
|
+
log_debug "DEBUG [2/10]: Searching for backup directories with pattern: ${BACKUP_PREFIX}.*"
|
|
693
|
+
while IFS= read -r -d '' dir; do
|
|
694
|
+
backup_dirs+=("$dir")
|
|
695
|
+
log_debug "DEBUG [2/10]: Found backup directory: $dir"
|
|
696
|
+
done < <(find . -maxdepth 1 -type d -name "${BACKUP_PREFIX}.*" -print0 2>/dev/null | sort -zr)
|
|
697
|
+
|
|
698
|
+
log_debug "DEBUG [2/10]: Total backup directories found: ${#backup_dirs[@]}"
|
|
699
|
+
|
|
700
|
+
if [[ ${#backup_dirs[@]} -eq 0 ]]; then
|
|
701
|
+
log_error "No backup directories found"
|
|
702
|
+
return 1
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
# Select backup
|
|
706
|
+
if [[ "$target_backup" == "latest" ]]; then
|
|
707
|
+
selected_backup="${backup_dirs[0]}"
|
|
708
|
+
log_info "Selected latest backup: $selected_backup"
|
|
709
|
+
log_debug "DEBUG [3/10]: Selected latest backup: $selected_backup"
|
|
710
|
+
else
|
|
711
|
+
# Check if specific backup exists
|
|
712
|
+
if [[ -d "$target_backup" ]]; then
|
|
713
|
+
selected_backup="$target_backup"
|
|
714
|
+
log_info "Selected specified backup: $target_backup"
|
|
715
|
+
else
|
|
716
|
+
log_error "Backup not found: $target_backup"
|
|
717
|
+
log_info "Available backups:"
|
|
718
|
+
for dir in "${backup_dirs[@]}"; do
|
|
719
|
+
echo " $dir"
|
|
720
|
+
done
|
|
721
|
+
return 1
|
|
722
|
+
fi
|
|
723
|
+
fi
|
|
724
|
+
|
|
725
|
+
log_debug "DEBUG [4/10]: Selected backup: $selected_backup"
|
|
726
|
+
log_debug "DEBUG [4/10]: Backup directory exists: $([[ -d "$selected_backup" ]] && echo "YES" || echo "NO")"
|
|
727
|
+
|
|
728
|
+
# Load session metadata
|
|
729
|
+
local session_metadata_file="$selected_backup/.sr_session_metadata"
|
|
730
|
+
local filelist_file="$selected_backup/.sr_modified_files"
|
|
731
|
+
local fileinfo_file="$selected_backup/.sr_file_info"
|
|
732
|
+
files_to_restore=()
|
|
733
|
+
|
|
734
|
+
log_debug "DEBUG [5/10]: Looking for session metadata: $session_metadata_file"
|
|
735
|
+
log_debug "DEBUG [5/10]: Metadata file exists: $([[ -f "$session_metadata_file" ]] && echo "YES" || echo "NO")"
|
|
736
|
+
|
|
737
|
+
if [[ -f "$session_metadata_file" ]]; then
|
|
738
|
+
log_info "Loading session metadata..."
|
|
739
|
+
# Extract info from metadata
|
|
740
|
+
local session_id session_command file_count search_str replace_str
|
|
741
|
+
session_id=$(grep '^SESSION_ID=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
742
|
+
session_command=$(grep '^SESSION_COMMAND=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
743
|
+
file_count=$(grep '^SESSION_MODIFIED_COUNT=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
744
|
+
search_str=$(grep '^SESSION_SEARCH=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
745
|
+
replace_str=$(grep '^SESSION_REPLACE=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
746
|
+
|
|
747
|
+
log_info "Session ID: $session_id"
|
|
748
|
+
log_info "Original command: $session_command"
|
|
749
|
+
log_info "Files in session: ${file_count:-unknown}"
|
|
750
|
+
[[ -n "$search_str" ]] && [[ -n "$replace_str" ]] && log_info "Search: '$search_str' → '$replace_str'"
|
|
751
|
+
|
|
752
|
+
log_debug "DEBUG [5/10]: Metadata loaded: session_id='$session_id', file_count='$file_count'"
|
|
753
|
+
else
|
|
754
|
+
log_warning "No session metadata found, using legacy backup structure"
|
|
755
|
+
fi
|
|
756
|
+
|
|
757
|
+
# Determine which files to restore - PRIORITY 1: file list
|
|
758
|
+
log_debug "DEBUG [6/10]: Looking for file list: $filelist_file"
|
|
759
|
+
log_debug "DEBUG [6/10]: File list exists and readable: $([[ -f "$filelist_file" && -r "$filelist_file" ]] && echo "YES" || echo "NO")"
|
|
760
|
+
|
|
761
|
+
# CRITICAL FIX: Verify both existence AND readability upfront
|
|
762
|
+
if [[ -f "$filelist_file" && -r "$filelist_file" ]]; then
|
|
763
|
+
log_info "Reading modified files list from $filelist_file..."
|
|
764
|
+
|
|
765
|
+
# Create debug log with atomic initialization
|
|
766
|
+
local debug_log="/tmp/sr_rollback_debug_$$.log"
|
|
767
|
+
{
|
|
768
|
+
echo "=== START DEBUG LOG ==="
|
|
769
|
+
echo "Timestamp: $(date -u)"
|
|
770
|
+
echo "File: $filelist_file"
|
|
771
|
+
echo "User: $(whoami)"
|
|
772
|
+
echo "PID: $$"
|
|
773
|
+
echo "Bash version: ${BASH_VERSION}"
|
|
774
|
+
} >"$debug_log"
|
|
775
|
+
|
|
776
|
+
# File diagnostics with error suppression
|
|
777
|
+
{
|
|
778
|
+
echo -e "\n=== FILE INTEGRITY CHECK ==="
|
|
779
|
+
if command -v stat >/dev/null 2>&1; then
|
|
780
|
+
stat --format="Size: %s bytes\nInode: %i\nMode: %a\nUID: %u GID: %g" "$filelist_file" 2>/dev/null || echo "stat command failed"
|
|
781
|
+
else
|
|
782
|
+
echo "stat command not available"
|
|
783
|
+
ls -la "$filelist_file" 2>/dev/null || echo "ls command failed"
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
if command -v file >/dev/null 2>&1; then
|
|
787
|
+
echo -e "\nFile type: $(file -b "$filelist_file" 2>/dev/null || echo "file check unavailable")"
|
|
788
|
+
fi
|
|
789
|
+
|
|
790
|
+
echo -e "\nFile size: $(wc -c <"$filelist_file" 2>/dev/null || echo "0") bytes"
|
|
791
|
+
echo "Line count: $(wc -l <"$filelist_file" 2>/dev/null || echo "0")"
|
|
792
|
+
} >>"$debug_log" 2>&1
|
|
793
|
+
|
|
794
|
+
# CRITICAL FIX: Robust file reading with multiple fallbacks
|
|
795
|
+
local files_to_restore_local=()
|
|
796
|
+
local read_success=false
|
|
797
|
+
local method_used=""
|
|
798
|
+
|
|
799
|
+
# PRIMARY METHOD: Bash mapfile (most efficient)
|
|
800
|
+
if command -v mapfile &>/dev/null; then
|
|
801
|
+
log_debug "DEBUG [6/10]: METHOD 1 - Trying native mapfile"
|
|
802
|
+
echo -e "\n=== METHOD 1: Native mapfile ===" >>"$debug_log"
|
|
803
|
+
if mapfile -t files_to_restore_local <"$filelist_file" 2>>"$debug_log"; then
|
|
804
|
+
read_success=true
|
|
805
|
+
method_used="mapfile"
|
|
806
|
+
echo "SUCCESS: Read ${#files_to_restore_local[@]} lines via mapfile" >>"$debug_log"
|
|
807
|
+
echo "First 3 lines:" >>"$debug_log"
|
|
808
|
+
for ((i = 0; i < ${#files_to_restore_local[@]} && i < 3; i++)); do
|
|
809
|
+
echo " $((i + 1)): '${files_to_restore_local[$i]}'" >>"$debug_log"
|
|
810
|
+
done
|
|
811
|
+
else
|
|
812
|
+
echo "FAILED: mapfile method failed" >>"$debug_log"
|
|
813
|
+
fi
|
|
814
|
+
fi
|
|
815
|
+
|
|
816
|
+
# FALLBACK 1: POSIX-compliant while-read loop
|
|
817
|
+
if ! $read_success; then
|
|
818
|
+
log_debug "DEBUG [6/10]: METHOD 2 - Fallback to POSIX read loop"
|
|
819
|
+
echo -e "\n=== METHOD 2: POSIX read loop ===" >>"$debug_log"
|
|
820
|
+
files_to_restore_local=()
|
|
821
|
+
local line_count=0
|
|
822
|
+
|
|
823
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
824
|
+
files_to_restore_local+=("$line")
|
|
825
|
+
RESTORED_COUNT=$((RESTORED_COUNT + 1))
|
|
826
|
+
if [ $line_count -le 3 ]; then
|
|
827
|
+
echo "Read line $line_count: '$line'" >>"$debug_log"
|
|
828
|
+
fi
|
|
829
|
+
done < <(exec cat "$filelist_file" 2>>"$debug_log") && read_success=true
|
|
830
|
+
|
|
831
|
+
if $read_success; then
|
|
832
|
+
method_used="posix-loop"
|
|
833
|
+
echo "SUCCESS: Read ${#files_to_restore_local[@]} lines via POSIX loop" >>"$debug_log"
|
|
834
|
+
else
|
|
835
|
+
echo "FAILED: POSIX read loop failed" >>"$debug_log"
|
|
836
|
+
fi
|
|
837
|
+
fi
|
|
838
|
+
|
|
839
|
+
# FALLBACK 2: Process substitution with error isolation
|
|
840
|
+
if ! $read_success; then
|
|
841
|
+
log_debug "DEBUG [6/10]: METHOD 3 - Final fallback: process substitution with dd"
|
|
842
|
+
echo -e "\n=== METHOD 3: dd fallback ===" >>"$debug_log"
|
|
843
|
+
files_to_restore_local=()
|
|
844
|
+
local line_count=0
|
|
845
|
+
|
|
846
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
847
|
+
files_to_restore_local+=("$line")
|
|
848
|
+
RESTORED_COUNT=$((RESTORED_COUNT + 1))
|
|
849
|
+
if [ $line_count -le 3 ]; then
|
|
850
|
+
echo "Read line $line_count: '$line'" >>"$debug_log"
|
|
851
|
+
fi
|
|
852
|
+
done < <(dd if="$filelist_file" bs=64K count=100 2>>"$debug_log" || echo "dd command failed") && read_success=true
|
|
853
|
+
|
|
854
|
+
if $read_success; then
|
|
855
|
+
method_used="dd-fallback"
|
|
856
|
+
echo "SUCCESS: Read ${#files_to_restore_local[@]} lines via dd fallback" >>"$debug_log"
|
|
857
|
+
else
|
|
858
|
+
echo "FAILED: All read methods failed" >>"$debug_log"
|
|
859
|
+
log_error "CRITICAL: Unable to read file list after 3 attempts"
|
|
860
|
+
fi
|
|
861
|
+
fi
|
|
862
|
+
|
|
863
|
+
# CRITICAL FIX: Centralized line processing with strict validation
|
|
864
|
+
if $read_success && [ ${#files_to_restore_local[@]} -gt 0 ]; then
|
|
865
|
+
echo -e "\n=== LINE PROCESSING ===" >>"$debug_log"
|
|
866
|
+
local valid_count=0
|
|
867
|
+
files_to_restore=() # Reset main array
|
|
868
|
+
|
|
869
|
+
for raw_line in "${files_to_restore_local[@]}"; do
|
|
870
|
+
# Robust whitespace trimming using POSIX parameter expansion
|
|
871
|
+
local trimmed="${raw_line#"${raw_line%%[![:space:]]*}"}"
|
|
872
|
+
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
873
|
+
|
|
874
|
+
# Skip empty lines and comments
|
|
875
|
+
if [ -z "$trimmed" ]; then
|
|
876
|
+
echo "SKIPPED: Empty line" >>"$debug_log"
|
|
877
|
+
continue
|
|
878
|
+
fi
|
|
879
|
+
|
|
880
|
+
if [[ "$trimmed" =~ ^[[:space:]]*# ]]; then
|
|
881
|
+
echo "SKIPPED: Comment line: '$trimmed'" >>"$debug_log"
|
|
882
|
+
continue
|
|
883
|
+
fi
|
|
884
|
+
|
|
885
|
+
# Validate path sanity before adding
|
|
886
|
+
if [[ "$trimmed" == /* ]] && [[ "$trimmed" =~ ^[[:print:]]+$ ]]; then
|
|
887
|
+
# Additional checks for dangerous patterns
|
|
888
|
+
if [[ "$trimmed" == *".."* ]] || [[ "$trimmed" == /proc/* ]] || [[ "$trimmed" == /sys/* ]] || [[ "$trimmed" == /dev/* ]]; then
|
|
889
|
+
echo "SKIPPED: Dangerous path pattern: '$trimmed'" >>"$debug_log"
|
|
890
|
+
continue
|
|
891
|
+
fi
|
|
892
|
+
|
|
893
|
+
# Reject paths with control characters
|
|
894
|
+
if [[ "$trimmed" =~ [[:cntrl:]] ]]; then
|
|
895
|
+
echo "SKIPPED: Contains control characters: '$trimmed'" >>"$debug_log"
|
|
896
|
+
continue
|
|
897
|
+
fi
|
|
898
|
+
|
|
899
|
+
files_to_restore+=("$trimmed")
|
|
900
|
+
RESTORED_COUNT=$((RESTORED_COUNT + 1))
|
|
901
|
+
echo "ACCEPTED: '$trimmed'" >>"$debug_log"
|
|
902
|
+
else
|
|
903
|
+
echo "SKIPPED: Invalid path format: '$trimmed'" >>"$debug_log"
|
|
904
|
+
fi
|
|
905
|
+
done
|
|
906
|
+
|
|
907
|
+
echo -e "\n=== PROCESSING RESULTS ===" >>"$debug_log"
|
|
908
|
+
echo "Valid paths found: $valid_count" >>"$debug_log"
|
|
909
|
+
echo "Total paths processed: ${#files_to_restore_local[@]}" >>"$debug_log"
|
|
910
|
+
echo "Method used: $method_used" >>"$debug_log"
|
|
911
|
+
else
|
|
912
|
+
echo "ERROR: All read methods failed for $filelist_file" >>"$debug_log"
|
|
913
|
+
log_error "CRITICAL: Unable to read file list after 3 attempts"
|
|
914
|
+
fi
|
|
915
|
+
|
|
916
|
+
# Final debug summary
|
|
917
|
+
{
|
|
918
|
+
echo -e "\n=== FINAL DEBUG SUMMARY ==="
|
|
919
|
+
echo "Files to restore: ${#files_to_restore[@]}"
|
|
920
|
+
if [ ${#files_to_restore[@]} -gt 0 ]; then
|
|
921
|
+
echo "First 5 files:"
|
|
922
|
+
for ((i = 0; i < ${#files_to_restore[@]} && i < 5; i++)); do
|
|
923
|
+
echo " $((i + 1)): '${files_to_restore[$i]}'"
|
|
924
|
+
done
|
|
925
|
+
if [ ${#files_to_restore[@]} -gt 5 ]; then
|
|
926
|
+
echo " ... (and $((${#files_to_restore[@]} - 5)) more)"
|
|
927
|
+
fi
|
|
928
|
+
else
|
|
929
|
+
echo "WARNING: No valid files found for restoration"
|
|
930
|
+
fi
|
|
931
|
+
} >>"$debug_log"
|
|
932
|
+
|
|
933
|
+
log_info "Found ${#files_to_restore[@]} valid file(s) in file list"
|
|
934
|
+
log_debug "DEBUG [6/10]: File list processing complete. See full debug at: $debug_log"
|
|
935
|
+
|
|
936
|
+
# Show summary of debug log in main log
|
|
937
|
+
log_debug "DEBUG [6/10]: First 15 lines of debug log:"
|
|
938
|
+
head -15 "$debug_log" | while IFS= read -r log_line; do
|
|
939
|
+
log_debug "DEBUG [6/10]: $log_line"
|
|
940
|
+
done
|
|
941
|
+
|
|
942
|
+
else
|
|
943
|
+
log_debug "DEBUG [6/10]: Primary file list unavailable or unreadable"
|
|
944
|
+
# PRIORITY 2: Fallback to fileinfo with same robustness
|
|
945
|
+
if [[ -f "$fileinfo_file" && -r "$fileinfo_file" ]]; then
|
|
946
|
+
log_info "Extracting file list from $fileinfo_file..."
|
|
947
|
+
log_debug "DEBUG [6/10]: Starting to read file info..."
|
|
948
|
+
|
|
949
|
+
# Create debug log for fileinfo processing
|
|
950
|
+
local debug_log_info="/tmp/sr_rollback_debug_info_$$.log"
|
|
951
|
+
{
|
|
952
|
+
echo "=== START FILEINFO DEBUG LOG ==="
|
|
953
|
+
echo "Timestamp: $(date -u)"
|
|
954
|
+
echo "File: $fileinfo_file"
|
|
955
|
+
echo "User: $(whoami)"
|
|
956
|
+
echo "PID: $$"
|
|
957
|
+
echo "Bash version: ${BASH_VERSION}"
|
|
958
|
+
} >"$debug_log_info"
|
|
959
|
+
|
|
960
|
+
{
|
|
961
|
+
echo -e "\n=== FILE INTEGRITY CHECK ==="
|
|
962
|
+
if command -v stat >/dev/null 2>&1; then
|
|
963
|
+
stat --format="Size: %s bytes\nInode: %i\nMode: %a\nUID: %u GID: %g" "$fileinfo_file" 2>/dev/null || echo "stat command failed"
|
|
964
|
+
else
|
|
965
|
+
echo "stat command not available"
|
|
966
|
+
ls -la "$fileinfo_file" 2>/dev/null || echo "ls command failed"
|
|
967
|
+
fi
|
|
968
|
+
} >>"$debug_log_info" 2>&1
|
|
969
|
+
|
|
970
|
+
local in_section=false
|
|
971
|
+
local line_num=0
|
|
972
|
+
files_to_restore=() # Reset array
|
|
973
|
+
|
|
974
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
975
|
+
line_num=$((line_num + 1))
|
|
976
|
+
echo "Processing line $line_num: '$line'" >>"$debug_log_info"
|
|
977
|
+
|
|
978
|
+
if [[ "$line" == "# List of all modified files:" ]]; then
|
|
979
|
+
in_section=true
|
|
980
|
+
echo "SECTION START: Found file list section at line $line_num" >>"$debug_log_info"
|
|
981
|
+
continue
|
|
982
|
+
elif [[ "$in_section" == true ]] && [[ "$line" =~ ^[[:space:]]*#$ ]]; then
|
|
983
|
+
# Empty comment line in section - continue
|
|
984
|
+
continue
|
|
985
|
+
elif [[ "$in_section" == true ]] && [[ "$line" =~ ^# ]]; then
|
|
986
|
+
# Non-empty comment line likely indicates end of section
|
|
987
|
+
echo "SECTION END: Comment line suggests end of section at line $line_num" >>"$debug_log_info"
|
|
988
|
+
break
|
|
989
|
+
elif [[ "$in_section" == true ]] && [[ -n "$line" ]]; then
|
|
990
|
+
# Clean up the line
|
|
991
|
+
local clean_line="${line#"${line%%[![:space:]]*}"}" # Trim leading whitespace
|
|
992
|
+
clean_line="${clean_line%"${clean_line##*[![:space:]]}"}" # Trim trailing whitespace
|
|
993
|
+
|
|
994
|
+
if [[ -z "$clean_line" ]]; then
|
|
995
|
+
echo "SKIPPED: Empty after trim at line $line_num" >>"$debug_log_info"
|
|
996
|
+
continue
|
|
997
|
+
fi
|
|
998
|
+
|
|
999
|
+
echo "CANDIDATE: '$clean_line' at line $line_num" >>"$debug_log_info"
|
|
1000
|
+
|
|
1001
|
+
# Validate path format - same robust method as above
|
|
1002
|
+
if [[ "$clean_line" == /* ]] && [[ "$clean_line" =~ ^[[:print:]]+$ ]]; then
|
|
1003
|
+
if [[ "$clean_line" == *".."* ]] || [[ "$clean_line" == /proc/* ]] || [[ "$clean_line" == /sys/* ]] || [[ "$clean_line" == /dev/* ]]; then
|
|
1004
|
+
echo "SKIPPED: Dangerous path pattern at line $line_num: '$clean_line'" >>"$debug_log_info"
|
|
1005
|
+
continue
|
|
1006
|
+
fi
|
|
1007
|
+
|
|
1008
|
+
if [[ "$clean_line" =~ [[:cntrl:]] ]]; then
|
|
1009
|
+
echo "SKIPPED: Contains control characters at line $line_num: '$clean_line'" >>"$debug_log_info"
|
|
1010
|
+
continue
|
|
1011
|
+
fi
|
|
1012
|
+
|
|
1013
|
+
files_to_restore+=("$clean_line")
|
|
1014
|
+
echo "ACCEPTED: '$clean_line' at line $line_num" >>"$debug_log_info"
|
|
1015
|
+
else
|
|
1016
|
+
echo "SKIPPED: Invalid path format at line $line_num: '$clean_line'" >>"$debug_log_info"
|
|
1017
|
+
fi
|
|
1018
|
+
fi
|
|
1019
|
+
done <"$fileinfo_file" 2>>"$debug_log_info"
|
|
1020
|
+
|
|
1021
|
+
# Final summary for fileinfo processing
|
|
1022
|
+
{
|
|
1023
|
+
echo -e "\n=== FILEINFO PROCESSING SUMMARY ==="
|
|
1024
|
+
echo "Files found in section: ${#files_to_restore[@]}"
|
|
1025
|
+
if [ ${#files_to_restore[@]} -gt 0 ]; then
|
|
1026
|
+
echo "First 3 files:"
|
|
1027
|
+
for ((i = 0; i < ${#files_to_restore[@]} && i < 3; i++)); do
|
|
1028
|
+
echo " $((i + 1)): '${files_to_restore[$i]}'"
|
|
1029
|
+
done
|
|
1030
|
+
fi
|
|
1031
|
+
} >>"$debug_log_info"
|
|
1032
|
+
|
|
1033
|
+
log_info "Found ${#files_to_restore[@]} file(s) in file info"
|
|
1034
|
+
log_debug "DEBUG [6/10]: File info processing complete. Debug log: $debug_log_info"
|
|
1035
|
+
|
|
1036
|
+
# Show summary of debug log in main log
|
|
1037
|
+
log_debug "DEBUG [6/10]: First 10 lines of fileinfo debug log:"
|
|
1038
|
+
head -10 "$debug_log_info" | while IFS= read -r log_line; do
|
|
1039
|
+
log_debug "DEBUG [6/10]: $log_line"
|
|
1040
|
+
done
|
|
1041
|
+
else
|
|
1042
|
+
log_debug "DEBUG [6/10]: File info not found or unreadable: $fileinfo_file"
|
|
1043
|
+
log_error "ERROR: Neither file list ($filelist_file) nor file info ($fileinfo_file) is available for restoration"
|
|
1044
|
+
return 1
|
|
1045
|
+
fi
|
|
1046
|
+
fi
|
|
1047
|
+
|
|
1048
|
+
# Final validation and logging
|
|
1049
|
+
if [ ${#files_to_restore[@]} -eq 0 ]; then
|
|
1050
|
+
log_warning "WARNING: No valid files were found for restoration"
|
|
1051
|
+
log_debug "DEBUG [6/10]: files_to_restore array is empty after all processing"
|
|
1052
|
+
else
|
|
1053
|
+
log_debug "DEBUG [6/10]: Files to restore array contents:"
|
|
1054
|
+
for ((i = 0; i < ${#files_to_restore[@]}; i++)); do
|
|
1055
|
+
log_debug "DEBUG [6/10]: [$i] '${files_to_restore[$i]}'"
|
|
1056
|
+
done
|
|
1057
|
+
fi
|
|
1058
|
+
|
|
1059
|
+
# DEBUG [7/10]: Processing find output...
|
|
1060
|
+
log_debug "DEBUG [7/10]: Starting directory scan..."
|
|
1061
|
+
|
|
1062
|
+
# Get absolute path of backup directory
|
|
1063
|
+
local backup_dir_abs
|
|
1064
|
+
backup_dir_abs=$(cd "$selected_backup" && pwd 2>/dev/null || echo "$selected_backup")
|
|
1065
|
+
log_debug "DEBUG [7/10]: Absolute backup path: '$backup_dir_abs'"
|
|
1066
|
+
|
|
1067
|
+
# Create temporary file list
|
|
1068
|
+
local temp_files_list
|
|
1069
|
+
temp_files_list=$(mktemp 2>/dev/null || echo "/tmp/sr_files_list_$$")
|
|
1070
|
+
|
|
1071
|
+
# Find all non-metadata files in backup
|
|
1072
|
+
log_debug "DEBUG [7/10]: Executing: find '$backup_dir_abs' -type f -not -name '.sr_*'"
|
|
1073
|
+
find "$backup_dir_abs" -type f -not -name ".sr_*" 2>/dev/null >"$temp_files_list"
|
|
1074
|
+
|
|
1075
|
+
# Process found files
|
|
1076
|
+
local file_count=0
|
|
1077
|
+
if [[ -s "$temp_files_list" ]]; then
|
|
1078
|
+
file_count=$(wc -l <"$temp_files_list" 2>/dev/null || echo "0")
|
|
1079
|
+
log_debug "DEBUG [7/10]: Found $file_count file(s)"
|
|
1080
|
+
|
|
1081
|
+
# Log all found files in debug mode
|
|
1082
|
+
if [[ "$file_count" -gt 0 ]]; then
|
|
1083
|
+
log_debug "DEBUG [7/10]: All files found:"
|
|
1084
|
+
cat "$temp_files_list" | while IFS= read -r file; do
|
|
1085
|
+
log_debug "DEBUG [7/10]: '$file'"
|
|
1086
|
+
done
|
|
1087
|
+
fi
|
|
1088
|
+
fi
|
|
1089
|
+
|
|
1090
|
+
# Add files from find command to restore list
|
|
1091
|
+
if [[ "$file_count" -gt 0 ]]; then
|
|
1092
|
+
local added=0
|
|
1093
|
+
|
|
1094
|
+
# Create temp file for processed paths
|
|
1095
|
+
local processed_paths_file
|
|
1096
|
+
processed_paths_file=$(mktemp 2>/dev/null || echo "/tmp/sr_processed_$$")
|
|
1097
|
+
|
|
1098
|
+
# Process each file found
|
|
1099
|
+
while IFS= read -r full_path || [[ -n "$full_path" ]]; do
|
|
1100
|
+
# Skip empty lines
|
|
1101
|
+
[[ -z "$full_path" ]] && continue
|
|
1102
|
+
|
|
1103
|
+
log_debug "DEBUG [7/10]: Processing: '$full_path'"
|
|
1104
|
+
|
|
1105
|
+
# Get relative path
|
|
1106
|
+
local relative_path="${full_path#$backup_dir_abs/}"
|
|
1107
|
+
|
|
1108
|
+
# Handle edge cases
|
|
1109
|
+
if [[ "$relative_path" == "$full_path" ]]; then
|
|
1110
|
+
log_debug "DEBUG [7/10]: Conversion failed (not inside backup directory)"
|
|
1111
|
+
|
|
1112
|
+
# Try alternative method
|
|
1113
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
1114
|
+
relative_path=$(realpath --relative-to="." "$full_path" 2>/dev/null || echo "$full_path")
|
|
1115
|
+
fi
|
|
1116
|
+
|
|
1117
|
+
# Clean up path
|
|
1118
|
+
relative_path="${relative_path#$selected_backup/}"
|
|
1119
|
+
log_debug "DEBUG [7/10]: Using alternative: '$relative_path'"
|
|
1120
|
+
fi
|
|
1121
|
+
|
|
1122
|
+
# Normalize path
|
|
1123
|
+
relative_path="${relative_path#./}"
|
|
1124
|
+
[[ "$relative_path" != "/" ]] && relative_path="${relative_path%/}"
|
|
1125
|
+
|
|
1126
|
+
# Process the path if it meets all validation criteria
|
|
1127
|
+
if [[ -n "$relative_path" && "$relative_path" != "$full_path" ]] &&
|
|
1128
|
+
[[ ! "$relative_path" =~ \.sr_ ]] && [[ ! "${relative_path##*/}" =~ ^\.sr_ ]]; then
|
|
1129
|
+
|
|
1130
|
+
# Ensure 'added' variable is numeric before incrementing
|
|
1131
|
+
if [[ ! "$added" =~ ^[0-9]+$ ]]; then
|
|
1132
|
+
# Initialize or reset 'added' if it's not a valid number
|
|
1133
|
+
added=0
|
|
1134
|
+
log_debug "DEBUG [7/10]: Initialized/reset 'added' counter to 0"
|
|
1135
|
+
fi
|
|
1136
|
+
|
|
1137
|
+
# Safely append the path to the file
|
|
1138
|
+
if printf '%s\n' "$relative_path" >>"$processed_paths_file" 2>/dev/null; then
|
|
1139
|
+
# Perform the increment only if 'added' is confirmed to be numeric
|
|
1140
|
+
added=$((added + 1))
|
|
1141
|
+
log_debug "DEBUG [7/10]: [$added] Added: '$relative_path'"
|
|
1142
|
+
else
|
|
1143
|
+
log_error "CRITICAL: Failed to write path '$relative_path' to list file '$processed_paths_file'."
|
|
1144
|
+
return 1
|
|
1145
|
+
fi
|
|
1146
|
+
|
|
1147
|
+
else
|
|
1148
|
+
# Log the specific reason for skipping the path
|
|
1149
|
+
if [[ -z "$relative_path" ]]; then
|
|
1150
|
+
log_debug "DEBUG [7/10]: Skipping empty path."
|
|
1151
|
+
elif [[ "$relative_path" == "$full_path" ]]; then
|
|
1152
|
+
log_debug "DEBUG [7/10]: Skipping path as it did not resolve to a relative path: '$full_path'."
|
|
1153
|
+
elif [[ "$relative_path" =~ \.sr_ ]] || [[ "${relative_path##*/}" =~ ^\.sr_ ]]; then
|
|
1154
|
+
log_debug "DEBUG [7/10]: Skipping metadata path: '$relative_path'."
|
|
1155
|
+
fi
|
|
1156
|
+
fi
|
|
1157
|
+
|
|
1158
|
+
done <"$temp_files_list"
|
|
1159
|
+
|
|
1160
|
+
# Read processed paths and add to array
|
|
1161
|
+
if [[ "$added" -gt 0 ]] && [[ -s "$processed_paths_file" ]]; then
|
|
1162
|
+
log_debug "DEBUG [7/10]: Reading $added processed path(s) from temp file..."
|
|
1163
|
+
|
|
1164
|
+
local processed_count=0
|
|
1165
|
+
while IFS= read -r relative_path || [[ -n "$relative_path" ]]; do
|
|
1166
|
+
# Skip empty lines
|
|
1167
|
+
[[ -z "$relative_path" ]] && continue
|
|
1168
|
+
|
|
1169
|
+
# Validate and add to array
|
|
1170
|
+
if [[ -n "$relative_path" ]]; then
|
|
1171
|
+
files_to_restore+=("$relative_path")
|
|
1172
|
+
# Increment only after successful array operation
|
|
1173
|
+
processed_count=$((processed_count + 1))
|
|
1174
|
+
|
|
1175
|
+
if [[ $processed_count -le 3 ]]; then
|
|
1176
|
+
log_debug "DEBUG [7/10]: [$processed_count] Loaded to array: '$relative_path'"
|
|
1177
|
+
fi
|
|
1178
|
+
fi
|
|
1179
|
+
done <"$processed_paths_file"
|
|
1180
|
+
|
|
1181
|
+
log_info "Found $processed_count file(s) in backup directory"
|
|
1182
|
+
else
|
|
1183
|
+
log_warning "No valid files found in backup directory after processing"
|
|
1184
|
+
fi
|
|
1185
|
+
|
|
1186
|
+
# Clean up temp files
|
|
1187
|
+
rm -f "$processed_paths_file" 2>/dev/null
|
|
1188
|
+
|
|
1189
|
+
else
|
|
1190
|
+
log_warning "No files found in backup directory"
|
|
1191
|
+
fi
|
|
1192
|
+
|
|
1193
|
+
# Clean up temporary files
|
|
1194
|
+
rm -f "$temp_files_list" 2>/dev/null
|
|
1195
|
+
|
|
1196
|
+
# DEBUG [7.5/10]: TRANSITION CHECK - After directory scan
|
|
1197
|
+
log_debug "DEBUG [7.5/10]: TRANSITION CHECK - After directory scan"
|
|
1198
|
+
log_debug "DEBUG [7.5/10]: files_to_restore array status:"
|
|
1199
|
+
log_debug "DEBUG [7.5/10]: Array is set: $([[ -n "${files_to_restore+set}" ]] && echo "YES" || echo "NO")"
|
|
1200
|
+
log_debug "DEBUG [7.5/10]: Array size: ${#files_to_restore[@]}"
|
|
1201
|
+
|
|
1202
|
+
if [[ ${#files_to_restore[@]} -gt 0 ]]; then
|
|
1203
|
+
log_debug "DEBUG [7.5/10]: First 5 files in array:"
|
|
1204
|
+
for i in {0..4}; do
|
|
1205
|
+
[[ $i -lt ${#files_to_restore[@]} ]] &&
|
|
1206
|
+
log_debug "DEBUG [7.5/10]: [$i] '${files_to_restore[$i]}'"
|
|
1207
|
+
done
|
|
1208
|
+
else
|
|
1209
|
+
log_debug "DEBUG [7.5/10]: WARNING: files_to_restore array is empty!"
|
|
1210
|
+
|
|
1211
|
+
# Debug: list backup directory contents
|
|
1212
|
+
log_debug "DEBUG [7.5/10]: Listing backup directory:"
|
|
1213
|
+
find "$selected_backup" -type f 2>/dev/null | head -10 | while read -r f; do
|
|
1214
|
+
log_debug "DEBUG [7.5/10]: $f"
|
|
1215
|
+
done
|
|
1216
|
+
fi
|
|
1217
|
+
|
|
1218
|
+
# Check permissions
|
|
1219
|
+
log_debug "DEBUG [7.5/10]: Current directory write permission: $([[ -w "." ]] && echo "YES" || echo "NO")"
|
|
1220
|
+
log_debug "DEBUG [7.5/10]: Current directory: $(pwd)"
|
|
1221
|
+
|
|
1222
|
+
# Check session modified files
|
|
1223
|
+
if [[ -n "${SESSION_MODIFIED_FILES+set}" ]]; then
|
|
1224
|
+
log_debug "DEBUG [7.5/10]: SESSION_MODIFIED_FILES size: ${#SESSION_MODIFIED_FILES[@]}"
|
|
1225
|
+
else
|
|
1226
|
+
log_debug "DEBUG [7.5/10]: SESSION_MODIFIED_FILES is NOT set"
|
|
1227
|
+
fi
|
|
1228
|
+
|
|
1229
|
+
# DEBUG [8/10]: Verifying backup files...
|
|
1230
|
+
log_debug "DEBUG [8/10]: Verifying backup files..."
|
|
1231
|
+
|
|
1232
|
+
# Ensure files_to_restore array is set
|
|
1233
|
+
if [[ -z "${files_to_restore+set}" ]]; then
|
|
1234
|
+
log_error "CRITICAL: files_to_restore array is not set! Initializing empty array."
|
|
1235
|
+
files_to_restore=()
|
|
1236
|
+
fi
|
|
1237
|
+
|
|
1238
|
+
# Debug array details
|
|
1239
|
+
log_debug "DEBUG [8/10]: Array details:"
|
|
1240
|
+
log_debug "DEBUG [8/10]: Reference: ${!files_to_restore[@]}"
|
|
1241
|
+
log_debug "DEBUG [8/10]: Size: ${#files_to_restore[@]}"
|
|
1242
|
+
log_debug "DEBUG [8/10]: Content (first 5):"
|
|
1243
|
+
for i in {0..4}; do
|
|
1244
|
+
if [[ $i -lt ${#files_to_restore[@]} ]]; then
|
|
1245
|
+
log_debug "DEBUG [8/10]: [$i] = '${files_to_restore[$i]}'"
|
|
1246
|
+
fi
|
|
1247
|
+
done
|
|
1248
|
+
|
|
1249
|
+
if [[ ${#files_to_restore[@]} -eq 0 ]]; then
|
|
1250
|
+
log_warning "WARNING: No files to restore after directory scan"
|
|
1251
|
+
|
|
1252
|
+
# List backup directory contents
|
|
1253
|
+
log_debug "DEBUG [8/10]: Backup directory '$selected_backup' contents:"
|
|
1254
|
+
if [[ -d "$selected_backup" ]]; then
|
|
1255
|
+
find "$selected_backup" -maxdepth 2 -type f 2>/dev/null | while read -r f; do
|
|
1256
|
+
log_debug "DEBUG [8/10]: $f"
|
|
1257
|
+
done
|
|
1258
|
+
else
|
|
1259
|
+
log_debug "DEBUG [8/10]: Backup directory does not exist!"
|
|
1260
|
+
fi
|
|
1261
|
+
|
|
1262
|
+
# Try alternative method to get files
|
|
1263
|
+
log_debug "DEBUG [8/10]: Trying alternative method to get files..."
|
|
1264
|
+
|
|
1265
|
+
# Use find with null terminator
|
|
1266
|
+
local alt_files=()
|
|
1267
|
+
while IFS= read -r -d '' file; do
|
|
1268
|
+
[[ "$file" == *".sr_"* ]] && continue
|
|
1269
|
+
local rel_path="${file#$selected_backup/}"
|
|
1270
|
+
rel_path="${rel_path#./}"
|
|
1271
|
+
[[ -n "$rel_path" ]] && alt_files+=("$rel_path")
|
|
1272
|
+
done < <(find "$selected_backup" -type f -print0 2>/dev/null)
|
|
1273
|
+
|
|
1274
|
+
if [[ ${#alt_files[@]} -gt 0 ]]; then
|
|
1275
|
+
log_debug "DEBUG [8/10]: Alternative method found ${#alt_files[@]} files"
|
|
1276
|
+
files_to_restore=("${alt_files[@]}")
|
|
1277
|
+
else
|
|
1278
|
+
log_error "ERROR: No files could be found to restore"
|
|
1279
|
+
return 1
|
|
1280
|
+
fi
|
|
1281
|
+
fi
|
|
1282
|
+
|
|
1283
|
+
log_debug "DEBUG [8/10]: Will verify ${#files_to_restore[@]} file(s)"
|
|
1284
|
+
|
|
1285
|
+
log_debug "DEBUG [8/10]: Preparing to verify backup files..."
|
|
1286
|
+
|
|
1287
|
+
# Final array validation before processing
|
|
1288
|
+
if [[ -z "${files_to_restore+set}" ]]; then
|
|
1289
|
+
log_error "ERROR: files_to_restore array is not set!"
|
|
1290
|
+
files_to_restore=()
|
|
1291
|
+
fi
|
|
1292
|
+
|
|
1293
|
+
if [[ ${#files_to_restore[@]} -eq 0 ]]; then
|
|
1294
|
+
log_warning "WARNING: No files to restore after processing"
|
|
1295
|
+
|
|
1296
|
+
# Check backup directory contents
|
|
1297
|
+
log_debug "DEBUG [8/10]: Checking backup directory contents..."
|
|
1298
|
+
if [[ -d "$selected_backup" ]]; then
|
|
1299
|
+
find "$selected_backup" -type f 2>/dev/null | head -10 | while read f; do
|
|
1300
|
+
log_debug "DEBUG [8/10]: Found: $f"
|
|
1301
|
+
done
|
|
1302
|
+
fi
|
|
1303
|
+
|
|
1304
|
+
return 1
|
|
1305
|
+
fi
|
|
1306
|
+
|
|
1307
|
+
log_debug "DEBUG [8/10]: Will verify ${#files_to_restore[@]} file(s)"
|
|
1308
|
+
log_debug "DEBUG [8/10]: Final files_to_restore count: ${#files_to_restore[@]}"
|
|
1309
|
+
|
|
1310
|
+
# Verify files exist in backup
|
|
1311
|
+
log_debug "DEBUG [9/10]: Verifying backup files exist..."
|
|
1312
|
+
local existing_files=()
|
|
1313
|
+
local missing_files=()
|
|
1314
|
+
|
|
1315
|
+
for file in "${files_to_restore[@]}"; do
|
|
1316
|
+
local backup_file="$selected_backup/$file"
|
|
1317
|
+
log_debug "DEBUG [9/10]: Checking: $file -> $backup_file"
|
|
1318
|
+
|
|
1319
|
+
# Check if file exists and is readable
|
|
1320
|
+
if [[ -f "$backup_file" ]] && [[ -r "$backup_file" ]]; then
|
|
1321
|
+
existing_files+=("$file")
|
|
1322
|
+
log_debug "DEBUG [9/10]: ✓ File exists and readable"
|
|
1323
|
+
else
|
|
1324
|
+
missing_files+=("$file")
|
|
1325
|
+
log_warning "Backup file not found or not readable: $backup_file"
|
|
1326
|
+
log_debug "DEBUG [9/10]: ✗ File missing or not readable"
|
|
1327
|
+
fi
|
|
1328
|
+
done
|
|
1329
|
+
|
|
1330
|
+
log_debug "DEBUG [9/10]: Verification complete. Existing: ${#existing_files[@]}, Missing: ${#missing_files[@]}"
|
|
1331
|
+
|
|
1332
|
+
if [[ ${#existing_files[@]} -eq 0 ]]; then
|
|
1333
|
+
log_error "No existing files found in backup to restore"
|
|
1334
|
+
return 1
|
|
1335
|
+
fi
|
|
1336
|
+
|
|
1337
|
+
files_to_restore=("${existing_files[@]}")
|
|
1338
|
+
|
|
1339
|
+
# Report missing files
|
|
1340
|
+
if [[ ${#missing_files[@]} -gt 0 ]]; then
|
|
1341
|
+
log_warning "${#missing_files[@]} file(s) not found in backup and will be skipped"
|
|
1342
|
+
fi
|
|
1343
|
+
|
|
1344
|
+
# Confirm rollback
|
|
1345
|
+
log_info "Found ${#files_to_restore[@]} file(s) to restore"
|
|
1346
|
+
log_debug "DEBUG [10/10]: Preparing confirmation prompt..."
|
|
1347
|
+
|
|
1348
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
1349
|
+
echo ""
|
|
1350
|
+
echo "The following files will be restored from backup:"
|
|
1351
|
+
echo "Backup location: $selected_backup"
|
|
1352
|
+
echo ""
|
|
1353
|
+
|
|
1354
|
+
local display_count=0
|
|
1355
|
+
local total_files=${#files_to_restore[@]}
|
|
1356
|
+
# Validate total_files is numeric
|
|
1357
|
+
if [[ ! "$total_files" =~ ^[0-9]+$ ]]; then
|
|
1358
|
+
total_files=0
|
|
1359
|
+
fi
|
|
1360
|
+
|
|
1361
|
+
local limit=20
|
|
1362
|
+
if [[ $total_files -lt $limit ]]; then
|
|
1363
|
+
limit=$total_files
|
|
1364
|
+
fi
|
|
1365
|
+
|
|
1366
|
+
for ((i = 0; i < limit; i++)); do
|
|
1367
|
+
local file="${files_to_restore[$i]}"
|
|
1368
|
+
local backup_file="$selected_backup/$file"
|
|
1369
|
+
local file_size=""
|
|
1370
|
+
|
|
1371
|
+
# Get file size
|
|
1372
|
+
if [[ -f "$backup_file" ]]; then
|
|
1373
|
+
file_size=$(stat -c%s "$backup_file" 2>/dev/null || stat -f%z "$backup_file" 2>/dev/null || echo "0")
|
|
1374
|
+
# Validate file_size is numeric
|
|
1375
|
+
if [[ ! "$file_size" =~ ^[0-9]+$ ]]; then
|
|
1376
|
+
file_size=0
|
|
1377
|
+
fi
|
|
1378
|
+
|
|
1379
|
+
if [[ $file_size -gt 1048576 ]]; then
|
|
1380
|
+
file_size="$((file_size / 1048576)) MB"
|
|
1381
|
+
elif [[ $file_size -gt 1024 ]]; then
|
|
1382
|
+
file_size="$((file_size / 1024)) KB"
|
|
1383
|
+
else
|
|
1384
|
+
file_size="${file_size} bytes"
|
|
1385
|
+
fi
|
|
1386
|
+
else
|
|
1387
|
+
file_size="missing"
|
|
1388
|
+
fi
|
|
1389
|
+
|
|
1390
|
+
# Validate i is numeric before arithmetic
|
|
1391
|
+
if [[ ! "$i" =~ ^[0-9]+$ ]]; then
|
|
1392
|
+
i=0
|
|
1393
|
+
fi
|
|
1394
|
+
printf " %3d. %-60s (%s)\n" $((i + 1)) "$file" "$file_size"
|
|
1395
|
+
display_count=$((display_count + 1))
|
|
1396
|
+
done
|
|
1397
|
+
|
|
1398
|
+
if [[ $total_files -gt 20 ]]; then
|
|
1399
|
+
local remaining=$((total_files - 20))
|
|
1400
|
+
echo " ... and $remaining more file(s)"
|
|
1401
|
+
fi
|
|
1402
|
+
|
|
1403
|
+
echo ""
|
|
1404
|
+
echo "Session information:"
|
|
1405
|
+
[[ -n "$session_id" ]] && echo " Session ID: $session_id"
|
|
1406
|
+
[[ -n "$search_str" ]] && [[ -n "$replace_str" ]] && echo " Search: '$search_str' → '$replace_str'"
|
|
1407
|
+
echo ""
|
|
1408
|
+
|
|
1409
|
+
# Validate missing_files count
|
|
1410
|
+
local missing_count=${#missing_files[@]}
|
|
1411
|
+
if [[ ! "$missing_count" =~ ^[0-9]+$ ]]; then
|
|
1412
|
+
missing_count=0
|
|
1413
|
+
fi
|
|
1414
|
+
if [[ $missing_count -gt 0 ]]; then
|
|
1415
|
+
echo "⚠ Warning: $missing_count file(s) will not be restored (not found in backup)"
|
|
1416
|
+
fi
|
|
1417
|
+
|
|
1418
|
+
echo ""
|
|
1419
|
+
log_debug "DEBUG [10/10]: ABOUT TO PROMPT USER FOR CONFIRMATION"
|
|
1420
|
+
if ! confirm_action_timeout "Continue with rollback?" "n" 30; then
|
|
1421
|
+
log_info "Rollback cancelled by user"
|
|
1422
|
+
return 0
|
|
1423
|
+
fi
|
|
1424
|
+
log_debug "DEBUG [10/10]: User confirmed, proceeding with rollback..."
|
|
1425
|
+
else
|
|
1426
|
+
local dry_run_count=${#files_to_restore[@]}
|
|
1427
|
+
if [[ ! "$dry_run_count" =~ ^[0-9]+$ ]]; then
|
|
1428
|
+
dry_run_count=0
|
|
1429
|
+
fi
|
|
1430
|
+
log_info "[DRY-RUN] Would restore $dry_run_count file(s) from $selected_backup"
|
|
1431
|
+
fi
|
|
1432
|
+
|
|
1433
|
+
# Perform rollback
|
|
1434
|
+
local restored_count=0
|
|
1435
|
+
local skipped_count=0
|
|
1436
|
+
local failed_count=0
|
|
1437
|
+
|
|
1438
|
+
log_info "Starting rollback from $selected_backup..."
|
|
1439
|
+
|
|
1440
|
+
for relative_path in "${files_to_restore[@]}"; do
|
|
1441
|
+
local backup_file="$selected_backup/$relative_path"
|
|
1442
|
+
local original_file="$relative_path"
|
|
1443
|
+
|
|
1444
|
+
log_debug "DEBUG [10/10]: Restoring file: $relative_path"
|
|
1445
|
+
|
|
1446
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1447
|
+
log_info "[DRY-RUN] Would restore: $backup_file -> $original_file"
|
|
1448
|
+
restored_count=$((restored_count + 1))
|
|
1449
|
+
else
|
|
1450
|
+
# Create directory if needed
|
|
1451
|
+
local dest_dir
|
|
1452
|
+
dest_dir=$(dirname "$original_file")
|
|
1453
|
+
if [[ ! -d "$dest_dir" ]] && [[ "$dest_dir" != "." ]]; then
|
|
1454
|
+
mkdir -p "$dest_dir" 2>/dev/null || {
|
|
1455
|
+
log_warning "Cannot create directory: $dest_dir"
|
|
1456
|
+
failed_count=$((failed_count + 1))
|
|
1457
|
+
continue
|
|
1458
|
+
}
|
|
1459
|
+
fi
|
|
1460
|
+
|
|
1461
|
+
# Check if target file already exists and handle permissions
|
|
1462
|
+
if [[ -f "$original_file" ]]; then
|
|
1463
|
+
log_debug "Target file already exists: $original_file"
|
|
1464
|
+
log_debug "File permissions: $(ls -la "$original_file" 2>/dev/null | awk 'NR==1')"
|
|
1465
|
+
|
|
1466
|
+
# Check if we can write to the file
|
|
1467
|
+
if [[ ! -w "$original_file" ]]; then
|
|
1468
|
+
log_warning "Cannot write to existing file (permission denied): $original_file"
|
|
1469
|
+
|
|
1470
|
+
# Try to fix permissions if running as root
|
|
1471
|
+
if [[ $(id -u) -eq 0 ]]; then
|
|
1472
|
+
log_debug "Running as root, attempting to fix permissions..."
|
|
1473
|
+
|
|
1474
|
+
# Backup current permissions
|
|
1475
|
+
local current_perms
|
|
1476
|
+
current_perms=$(stat -c "%a" "$original_file" 2>/dev/null || stat -f "%p" "$original_file" 2>/dev/null | sed 's/^[0-9]*//')
|
|
1477
|
+
|
|
1478
|
+
# Try to make writable
|
|
1479
|
+
if chmod +w "$original_file" 2>/dev/null; then
|
|
1480
|
+
log_debug "Made file writable temporarily"
|
|
1481
|
+
# We'll restore permissions after copy
|
|
1482
|
+
local restore_perms=true
|
|
1483
|
+
else
|
|
1484
|
+
log_error "Failed to make file writable: $original_file"
|
|
1485
|
+
failed_count=$((failed_count + 1))
|
|
1486
|
+
continue
|
|
1487
|
+
fi
|
|
1488
|
+
else
|
|
1489
|
+
log_error "Permission denied and not running as root"
|
|
1490
|
+
failed_count=$((failed_count + 1))
|
|
1491
|
+
continue
|
|
1492
|
+
fi
|
|
1493
|
+
fi
|
|
1494
|
+
fi
|
|
1495
|
+
|
|
1496
|
+
# Restore file
|
|
1497
|
+
if cp --preserve=all "$backup_file" "$original_file" 2>/dev/null || cp "$backup_file" "$original_file" 2>/dev/null; then
|
|
1498
|
+
log_success "Restored: $original_file"
|
|
1499
|
+
|
|
1500
|
+
# Extract preserve ownership setting from session metadata
|
|
1501
|
+
local session_preserve_ownership=""
|
|
1502
|
+
if [[ -f "$session_metadata_file" ]]; then
|
|
1503
|
+
# Read from metadata file (fallback to current script setting if not found)
|
|
1504
|
+
session_preserve_ownership=$(grep '^SESSION_PRESERVE_OWNERSHIP=' "$session_metadata_file" 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "")
|
|
1505
|
+
fi
|
|
1506
|
+
|
|
1507
|
+
# Determine if ownership/permissions should be restored
|
|
1508
|
+
local should_restore_perms_ownership="$PRESERVE_OWNERSHIP" # Default to current script setting
|
|
1509
|
+
|
|
1510
|
+
# Override default if session metadata was found and explicitly set
|
|
1511
|
+
if [[ -n "$session_preserve_ownership" ]]; then # Check if variable is not empty
|
|
1512
|
+
if [[ "$session_preserve_ownership" == "false" ]]; then
|
|
1513
|
+
should_restore_perms_ownership="false"
|
|
1514
|
+
log_debug "Session metadata indicated ownership was NOT preserved originally, skipping restore for $original_file"
|
|
1515
|
+
else
|
|
1516
|
+
# If it was "true" or any other non-"false" value, confirm restoration
|
|
1517
|
+
should_restore_perms_ownership="true"
|
|
1518
|
+
log_debug "Session metadata indicated ownership WAS preserved originally, attempting restore for $original_file"
|
|
1519
|
+
fi
|
|
1520
|
+
else
|
|
1521
|
+
# session_preserve_ownership was empty (variable was empty after grep/cut/tr)
|
|
1522
|
+
# Fallback: Use the current script's setting
|
|
1523
|
+
log_debug "Session metadata for ownership missing or empty, using current script setting ($PRESERVE_OWNERSHIP) for $original_file"
|
|
1524
|
+
fi
|
|
1525
|
+
|
|
1526
|
+
# Now perform the restoration based on the determined setting
|
|
1527
|
+
if [[ "$should_restore_perms_ownership" == "true" ]]; then
|
|
1528
|
+
# Get permissions and ownership from the backup file itself
|
|
1529
|
+
local backup_perms backup_owner backup_group
|
|
1530
|
+
backup_perms=$(stat -c "%a" "$backup_file" 2>/dev/null)
|
|
1531
|
+
backup_owner=$(stat -c "%u" "$backup_file" 2>/dev/null)
|
|
1532
|
+
backup_group=$(stat -c "%g" "$backup_file" 2>/dev/null)
|
|
1533
|
+
|
|
1534
|
+
if [[ -n "$backup_perms" ]]; then
|
|
1535
|
+
chmod "$backup_perms" "$original_file" 2>/dev/null &&
|
|
1536
|
+
log_debug "Restored permissions from backup: $backup_perms for $original_file"
|
|
1537
|
+
fi
|
|
1538
|
+
if [[ -n "$backup_owner" ]] && [[ -n "$backup_group" ]]; then
|
|
1539
|
+
chown "$backup_owner:$backup_group" "$original_file" 2>/dev/null &&
|
|
1540
|
+
log_debug "Restored ownership from backup: $backup_owner:$backup_group for $original_file"
|
|
1541
|
+
fi
|
|
1542
|
+
else
|
|
1543
|
+
log_debug "Ownership/permissions restore disabled for $original_file (based on session or current setting)"
|
|
1544
|
+
fi
|
|
1545
|
+
|
|
1546
|
+
restored_count=$((restored_count + 1))
|
|
1547
|
+
else
|
|
1548
|
+
log_warning "Failed to restore: $original_file"
|
|
1549
|
+
failed_count=$((failed_count + 1))
|
|
1550
|
+
fi
|
|
1551
|
+
fi
|
|
1552
|
+
done
|
|
1553
|
+
|
|
1554
|
+
log_info "Rollback completed:"
|
|
1555
|
+
log_info " Successfully restored: $restored_count"
|
|
1556
|
+
log_info " Failed: $failed_count"
|
|
1557
|
+
log_info " Skipped: $skipped_count"
|
|
1558
|
+
|
|
1559
|
+
if [[ $failed_count -gt 0 ]]; then
|
|
1560
|
+
log_warning "Some files failed to restore. Check permissions and try again."
|
|
1561
|
+
fi
|
|
1562
|
+
|
|
1563
|
+
# Cleanup old backups if configured
|
|
1564
|
+
if [[ "$MAX_BACKUPS" -gt 0 ]] && [[ "$DRY_RUN" != true ]]; then
|
|
1565
|
+
cleanup_old_backups
|
|
1566
|
+
fi
|
|
1567
|
+
|
|
1568
|
+
log_debug "DEBUG [10/10]: Rollback function completed successfully"
|
|
1569
|
+
return 0
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
# Cleanup old backups
|
|
1573
|
+
cleanup_old_backups() {
|
|
1574
|
+
local backup_dirs=()
|
|
1575
|
+
|
|
1576
|
+
# Find all backup directories sorted by time (newest first)
|
|
1577
|
+
while IFS= read -r -d '' dir; do
|
|
1578
|
+
backup_dirs+=("$dir")
|
|
1579
|
+
done < <(find . -maxdepth 1 -type d -name "${BACKUP_PREFIX}.*" -print0 2>/dev/null | sort -zr)
|
|
1580
|
+
|
|
1581
|
+
local count=${#backup_dirs[@]}
|
|
1582
|
+
|
|
1583
|
+
if [[ $count -gt $MAX_BACKUPS ]]; then
|
|
1584
|
+
log_info "Cleaning up old backups (keeping $MAX_BACKUPS)..."
|
|
1585
|
+
for ((i = MAX_BACKUPS; i < count; i++)); do
|
|
1586
|
+
local dir_to_remove="${backup_dirs[$i]}"
|
|
1587
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1588
|
+
log_info "[DRY-RUN] Would remove: $dir_to_remove"
|
|
1589
|
+
else
|
|
1590
|
+
rm -rf "$dir_to_remove"
|
|
1591
|
+
log_verbose "Removed old backup: $dir_to_remove"
|
|
1592
|
+
fi
|
|
1593
|
+
done
|
|
1594
|
+
fi
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
# List available backups with session info
|
|
1598
|
+
list_backups() {
|
|
1599
|
+
local backup_dirs=()
|
|
1600
|
+
|
|
1601
|
+
log_header "=== AVAILABLE BACKUPS ==="
|
|
1602
|
+
|
|
1603
|
+
# Find all backup directories
|
|
1604
|
+
while IFS= read -r -d '' dir; do
|
|
1605
|
+
backup_dirs+=("$dir")
|
|
1606
|
+
done < <(find . -maxdepth 1 -type d -name "${BACKUP_PREFIX}.*" -print0 2>/dev/null | sort -zr)
|
|
1607
|
+
|
|
1608
|
+
if [[ ${#backup_dirs[@]} -eq 0 ]]; then
|
|
1609
|
+
log_info "No backup directories found"
|
|
1610
|
+
return 0
|
|
1611
|
+
fi
|
|
1612
|
+
|
|
1613
|
+
log_info "Found ${#backup_dirs[@]} backup(s):"
|
|
1614
|
+
echo
|
|
1615
|
+
|
|
1616
|
+
for dir in "${backup_dirs[@]}"; do
|
|
1617
|
+
local file_count=0
|
|
1618
|
+
local size_kb=0
|
|
1619
|
+
local modified_files_count=0
|
|
1620
|
+
|
|
1621
|
+
# Count files and size
|
|
1622
|
+
if [[ -d "$dir" ]]; then
|
|
1623
|
+
file_count=$(find "$dir" -type f -not -name ".sr_*" 2>/dev/null | wc -l)
|
|
1624
|
+
size_kb=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
|
1625
|
+
fi
|
|
1626
|
+
|
|
1627
|
+
# Get session metadata
|
|
1628
|
+
local session_info=""
|
|
1629
|
+
local session_id=""
|
|
1630
|
+
if [[ -f "$dir/.sr_session_metadata" ]]; then
|
|
1631
|
+
session_id=$(grep '^SESSION_ID=' "$dir/.sr_session_metadata" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
1632
|
+
local pattern search replace
|
|
1633
|
+
pattern=$(grep '^SESSION_PATTERN=' "$dir/.sr_session_metadata" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
1634
|
+
search=$(grep '^SESSION_SEARCH=' "$dir/.sr_session_metadata" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
1635
|
+
replace=$(grep '^SESSION_REPLACE=' "$dir/.sr_session_metadata" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
1636
|
+
modified_files_count=$(grep '^SESSION_MODIFIED_COUNT=' "$dir/.sr_session_metadata" 2>/dev/null | cut -d= -f2 | tr -d '"')
|
|
1637
|
+
|
|
1638
|
+
session_info="Session: $session_id"
|
|
1639
|
+
[[ -n "$pattern" ]] && session_info+=" | Pattern: $pattern"
|
|
1640
|
+
[[ -n "$search" ]] && session_info+=" | '$search' → '$replace'"
|
|
1641
|
+
[[ -n "$modified_files_count" ]] && session_info+=" | Files: $modified_files_count"
|
|
1642
|
+
fi
|
|
1643
|
+
|
|
1644
|
+
printf " %-40s %6d files %6d KB\n" "$dir" "$file_count" "$size_kb"
|
|
1645
|
+
if [[ -n "$session_info" ]]; then
|
|
1646
|
+
echo " $session_info"
|
|
1647
|
+
fi
|
|
1648
|
+
echo
|
|
1649
|
+
done
|
|
1650
|
+
|
|
1651
|
+
echo
|
|
1652
|
+
log_info "Commands:"
|
|
1653
|
+
log_info " Restore latest backup: $0 --rollback"
|
|
1654
|
+
log_info " Restore specific backup: $0 --rollback=BACKUP_DIR_NAME"
|
|
1655
|
+
log_info " Show this list again: $0 --rollback-list"
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
# ============================================================================
|
|
1659
|
+
# ENHANCED PERFORM_REPLACE FUNCTION WITH TOOL FLAGS SUPPORT
|
|
1660
|
+
# ============================================================================
|
|
1661
|
+
|
|
1662
|
+
# Function to perform the actual search and replace operation with enhanced flags
|
|
1663
|
+
perform_replace() {
|
|
1664
|
+
local file="$1"
|
|
1665
|
+
local search_escaped="$2"
|
|
1666
|
+
local replace_escaped="$3"
|
|
1667
|
+
local timestamp="$4"
|
|
1668
|
+
|
|
1669
|
+
local file_owner file_group file_perms
|
|
1670
|
+
local before_count after_count replacements_in_file=0
|
|
1671
|
+
|
|
1672
|
+
log_debug "perform_replace: Processing file: $file"
|
|
1673
|
+
|
|
1674
|
+
# Check if file exists
|
|
1675
|
+
if [[ ! -f "$file" ]]; then
|
|
1676
|
+
log_error "File not found: $file"
|
|
1677
|
+
return 2
|
|
1678
|
+
fi
|
|
1679
|
+
|
|
1680
|
+
# Check file permissions
|
|
1681
|
+
if [[ ! -r "$file" ]]; then
|
|
1682
|
+
log_error "Cannot read file (permission denied): $file"
|
|
1683
|
+
return 3
|
|
1684
|
+
fi
|
|
1685
|
+
|
|
1686
|
+
# Skip binary files unless explicitly allowed
|
|
1687
|
+
if [[ "$SKIP_BINARY_FILES" == true ]] && is_binary_file "$file"; then
|
|
1688
|
+
if [[ "$ALLOW_BINARY" != true ]]; then
|
|
1689
|
+
log_verbose "Skipping binary file: $file (use --binary to process)"
|
|
1690
|
+
return 5
|
|
1691
|
+
fi
|
|
1692
|
+
fi
|
|
1693
|
+
|
|
1694
|
+
# Count occurrences before replacement with enhanced grep flags
|
|
1695
|
+
before_count=$(count_occurrences "$file" "$SEARCH_STRING")
|
|
1696
|
+
if [[ "$before_count" -eq 0 ]]; then
|
|
1697
|
+
log_debug "Search string not found in file: $file"
|
|
1698
|
+
return 6
|
|
1699
|
+
fi
|
|
1700
|
+
|
|
1701
|
+
# Get file metadata for backup
|
|
1702
|
+
if [[ "$PRESERVE_OWNERSHIP" == true ]]; then
|
|
1703
|
+
file_owner=$(stat -c "%u" "$file" 2>/dev/null)
|
|
1704
|
+
file_group=$(stat -c "%g" "$file" 2>/dev/null)
|
|
1705
|
+
file_perms=$(stat -c "%a" "$file" 2>/dev/null)
|
|
1706
|
+
|
|
1707
|
+
# Store first file owner for backup directory
|
|
1708
|
+
if [[ -z "$FIRST_FILE_OWNER" ]] && [[ -n "$file_owner" ]] && [[ -n "$file_group" ]]; then
|
|
1709
|
+
FIRST_FILE_OWNER="$file_owner"
|
|
1710
|
+
FIRST_FILE_GROUP="$file_group"
|
|
1711
|
+
log_debug "Set first file owner: $FIRST_FILE_OWNER:$FIRST_FILE_GROUP"
|
|
1712
|
+
fi
|
|
1713
|
+
fi
|
|
1714
|
+
|
|
1715
|
+
# Create backup if required
|
|
1716
|
+
if [[ "$CREATE_BACKUPS" == true ]] || [[ "$FORCE_BACKUP" == true ]]; then
|
|
1717
|
+
if ! create_backup "$file" "$timestamp" "$file_owner" "$file_group" "$file_perms"; then
|
|
1718
|
+
log_error "Failed to create backup for: $file"
|
|
1719
|
+
return 4
|
|
1720
|
+
fi
|
|
1721
|
+
fi
|
|
1722
|
+
|
|
1723
|
+
# Skip actual replacement in dry-run mode
|
|
1724
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1725
|
+
log_info "[DRY-RUN] Would replace $before_count occurrence(s) in: $file"
|
|
1726
|
+
TOTAL_REPLACEMENTS=$((TOTAL_REPLACEMENTS + before_count))
|
|
1727
|
+
return 0
|
|
1728
|
+
fi
|
|
1729
|
+
|
|
1730
|
+
# Handle different replace modes
|
|
1731
|
+
case "$REPLACE_MODE" in
|
|
1732
|
+
"inplace")
|
|
1733
|
+
# Build enhanced sed command with flags
|
|
1734
|
+
local sed_cmd_flags="$SED_FLAGS"
|
|
1735
|
+
|
|
1736
|
+
# Add extended regex flag if needed
|
|
1737
|
+
if [[ "$EXTENDED_REGEX" == true ]]; then
|
|
1738
|
+
# Check if we're using GNU sed or BSD sed
|
|
1739
|
+
if sed --version 2>/dev/null | grep -q "GNU"; then
|
|
1740
|
+
sed_cmd_flags+=" -r" # GNU sed extended regex
|
|
1741
|
+
else
|
|
1742
|
+
sed_cmd_flags+=" -E" # BSD sed extended regex
|
|
1743
|
+
fi
|
|
1744
|
+
fi
|
|
1745
|
+
|
|
1746
|
+
# Build sed pattern with enhanced flags
|
|
1747
|
+
local sed_pattern="s${SED_DELIMITER}${search_escaped}${SED_DELIMITER}${replace_escaped}${SED_DELIMITER}"
|
|
1748
|
+
|
|
1749
|
+
# Add global replace flag if enabled
|
|
1750
|
+
if [[ "$GLOBAL_REPLACE" == true ]]; then
|
|
1751
|
+
sed_pattern+="g"
|
|
1752
|
+
fi
|
|
1753
|
+
|
|
1754
|
+
# Add ignore case flag if enabled (GNU sed only)
|
|
1755
|
+
if [[ "$IGNORE_CASE" == true ]]; then
|
|
1756
|
+
if sed --version 2>/dev/null | grep -q "GNU"; then
|
|
1757
|
+
sed_pattern+="i"
|
|
1758
|
+
else
|
|
1759
|
+
log_warning "Ignore case (-i) not supported for BSD sed. Consider using GNU sed."
|
|
1760
|
+
fi
|
|
1761
|
+
fi
|
|
1762
|
+
|
|
1763
|
+
# Perform in-place replacement with enhanced flags
|
|
1764
|
+
local sed_command="$SED_TOOL $SED_INPLACE_FLAG $sed_cmd_flags \"$sed_pattern\" \"$file\""
|
|
1765
|
+
log_debug "Executing sed command: $sed_command"
|
|
1766
|
+
|
|
1767
|
+
if eval "$sed_command" 2>/dev/null; then
|
|
1768
|
+
# Count replacements after operation
|
|
1769
|
+
after_count=$(count_occurrences "$file" "$SEARCH_STRING")
|
|
1770
|
+
replacements_in_file=$((before_count - after_count))
|
|
1771
|
+
|
|
1772
|
+
if [[ "$replacements_in_file" -gt 0 ]]; then
|
|
1773
|
+
log_success "Replaced $replacements_in_file occurrence(s) in: $file"
|
|
1774
|
+
TOTAL_REPLACEMENTS=$((TOTAL_REPLACEMENTS + replacements_in_file))
|
|
1775
|
+
|
|
1776
|
+
# Track modified file
|
|
1777
|
+
track_modified_file "$file"
|
|
1778
|
+
return 0
|
|
1779
|
+
else
|
|
1780
|
+
log_warning "No replacements made in file (possible sed error): $file"
|
|
1781
|
+
return 1
|
|
1782
|
+
fi
|
|
1783
|
+
else
|
|
1784
|
+
log_error "sed command failed for file: $file"
|
|
1785
|
+
return 1
|
|
1786
|
+
fi
|
|
1787
|
+
;;
|
|
1788
|
+
|
|
1789
|
+
"copy")
|
|
1790
|
+
# Copy mode - create modified file in output directory
|
|
1791
|
+
if [[ -z "$OUTPUT_DIR" ]]; then
|
|
1792
|
+
log_error "Output directory not specified for copy mode"
|
|
1793
|
+
return 1
|
|
1794
|
+
fi
|
|
1795
|
+
|
|
1796
|
+
# Build sed command for copy mode
|
|
1797
|
+
local sed_cmd_flags="$SED_FLAGS"
|
|
1798
|
+
|
|
1799
|
+
if [[ "$EXTENDED_REGEX" == true ]]; then
|
|
1800
|
+
if sed --version 2>/dev/null | grep -q "GNU"; then
|
|
1801
|
+
sed_cmd_flags+=" -r"
|
|
1802
|
+
else
|
|
1803
|
+
sed_cmd_flags+=" -E"
|
|
1804
|
+
fi
|
|
1805
|
+
fi
|
|
1806
|
+
|
|
1807
|
+
local sed_pattern="s${SED_DELIMITER}${search_escaped}${SED_DELIMITER}${replace_escaped}${SED_DELIMITER}"
|
|
1808
|
+
|
|
1809
|
+
if [[ "$GLOBAL_REPLACE" == true ]]; then
|
|
1810
|
+
sed_pattern+="g"
|
|
1811
|
+
fi
|
|
1812
|
+
|
|
1813
|
+
if [[ "$IGNORE_CASE" == true ]]; then
|
|
1814
|
+
if sed --version 2>/dev/null | grep -q "GNU"; then
|
|
1815
|
+
sed_pattern+="i"
|
|
1816
|
+
fi
|
|
1817
|
+
fi
|
|
1818
|
+
|
|
1819
|
+
local modified_content
|
|
1820
|
+
modified_content=$($SED_TOOL $sed_cmd_flags "$sed_pattern" "$file" 2>/dev/null)
|
|
1821
|
+
|
|
1822
|
+
if [[ -n "$modified_content" ]]; then
|
|
1823
|
+
if create_output_file "$file" "$SEARCH_DIR" "$OUTPUT_DIR" "$modified_content" >/dev/null; then
|
|
1824
|
+
after_count=$(count_occurrences "$file" "$SEARCH_STRING")
|
|
1825
|
+
replacements_in_file=$((before_count - after_count))
|
|
1826
|
+
|
|
1827
|
+
if [[ "$replacements_in_file" -gt 0 ]]; then
|
|
1828
|
+
log_success "Created modified copy with $replacements_in_file replacement(s): $file"
|
|
1829
|
+
TOTAL_REPLACEMENTS=$((TOTAL_REPLACEMENTS + replacements_in_file))
|
|
1830
|
+
return 0
|
|
1831
|
+
else
|
|
1832
|
+
log_warning "No replacements in copy mode for file: $file"
|
|
1833
|
+
return 6
|
|
1834
|
+
fi
|
|
1835
|
+
else
|
|
1836
|
+
log_error "Failed to create output file: $file"
|
|
1837
|
+
return 1
|
|
1838
|
+
fi
|
|
1839
|
+
else
|
|
1840
|
+
log_error "Failed to modify content for file: $file"
|
|
1841
|
+
return 1
|
|
1842
|
+
fi
|
|
1843
|
+
;;
|
|
1844
|
+
|
|
1845
|
+
"backup_only")
|
|
1846
|
+
# Backup only mode - just create backup, no replacement
|
|
1847
|
+
log_verbose "Backup created (no replacement): $file"
|
|
1848
|
+
return 0
|
|
1849
|
+
;;
|
|
1850
|
+
|
|
1851
|
+
*)
|
|
1852
|
+
log_error "Unknown replace mode: $REPLACE_MODE"
|
|
1853
|
+
return 1
|
|
1854
|
+
;;
|
|
1855
|
+
esac
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
# ============================================================================
|
|
1859
|
+
# ENHANCED COUNT_OCCURRENCES FUNCTION WITH GREP FLAGS SUPPORT
|
|
1860
|
+
# ============================================================================
|
|
1861
|
+
|
|
1862
|
+
# Function to count occurrences with enhanced grep flags
|
|
1863
|
+
count_occurrences() {
|
|
1864
|
+
local file="$1"
|
|
1865
|
+
local pattern="$2"
|
|
1866
|
+
local count=0
|
|
1867
|
+
|
|
1868
|
+
# Build grep command with enhanced flags
|
|
1869
|
+
local grep_cmd="$GREP_TOOL $GREP_FLAGS"
|
|
1870
|
+
|
|
1871
|
+
# Add ignore case flag if enabled
|
|
1872
|
+
if [[ "$IGNORE_CASE" == true ]]; then
|
|
1873
|
+
grep_cmd+=" -i"
|
|
1874
|
+
fi
|
|
1875
|
+
|
|
1876
|
+
# Add extended regex flag if enabled
|
|
1877
|
+
if [[ "$EXTENDED_REGEX" == true ]]; then
|
|
1878
|
+
grep_cmd+=" -E"
|
|
1879
|
+
fi
|
|
1880
|
+
|
|
1881
|
+
# Add word boundary flag if enabled
|
|
1882
|
+
if [[ "$WORD_BOUNDARY" == true ]]; then
|
|
1883
|
+
grep_cmd+=" -w"
|
|
1884
|
+
fi
|
|
1885
|
+
|
|
1886
|
+
# Add line numbers flag if enabled
|
|
1887
|
+
if [[ "$LINE_NUMBERS" == true ]]; then
|
|
1888
|
+
grep_cmd+=" -n"
|
|
1889
|
+
fi
|
|
1890
|
+
|
|
1891
|
+
# Always add count flag for counting
|
|
1892
|
+
grep_cmd+=" -c"
|
|
1893
|
+
|
|
1894
|
+
# Use -a flag to treat binary files as text
|
|
1895
|
+
grep_cmd+=" -a"
|
|
1896
|
+
|
|
1897
|
+
log_debug "Counting occurrences with command: $grep_cmd \"$pattern\" \"$file\""
|
|
1898
|
+
|
|
1899
|
+
if count=$(eval "$grep_cmd \"\$pattern\" \"\$file\" 2>/dev/null"); then
|
|
1900
|
+
echo "$count"
|
|
1901
|
+
else
|
|
1902
|
+
echo "0"
|
|
1903
|
+
fi
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
# ============================================================================
|
|
1907
|
+
# ENHANCED USAGE AND HELP FUNCTIONS
|
|
1908
|
+
# ============================================================================
|
|
1909
|
+
|
|
1910
|
+
show_usage() {
|
|
1911
|
+
cat <<EOF
|
|
1912
|
+
Usage: ${0##*/} [OPTIONS] <file_pattern> <search_string> <replace_string>
|
|
1913
|
+
|
|
1914
|
+
Search and replace text in files recursively with proper escaping of special characters.
|
|
1915
|
+
|
|
1916
|
+
IMPORTANT: Options must come BEFORE positional arguments for predictable parsing.
|
|
1917
|
+
|
|
1918
|
+
Positional arguments (required, in this order):
|
|
1919
|
+
<file_pattern> File pattern to match (e.g., *.html, *.txt)
|
|
1920
|
+
<search_string> Text to search for
|
|
1921
|
+
<replace_string> Text to replace with
|
|
1922
|
+
|
|
1923
|
+
Options (must come before positional arguments):
|
|
1924
|
+
Core functionality:
|
|
1925
|
+
-d, --debug Enable debug output
|
|
1926
|
+
-nr, --no-recursive Process only current directory (non-recursive)
|
|
1927
|
+
--binary Allow processing of binary files (REQUIRED for binary files)
|
|
1928
|
+
-v, --verbose Enable verbose output (less detailed than debug)
|
|
1929
|
+
--rollback[=BACKUP_DIR] Restore from backup (latest or specified)
|
|
1930
|
+
--rollback-list List available backups with session info
|
|
1931
|
+
|
|
1932
|
+
Search/replace enhancements (NEW in 6.1.0):
|
|
1933
|
+
-i, --ignore-case Case-insensitive search and replace
|
|
1934
|
+
-E, --extended-regex Use extended regular expressions (ERE)
|
|
1935
|
+
-w, --word-boundary Match whole words only (word boundaries)
|
|
1936
|
+
-m, --multiline Enable multi-line mode for regex
|
|
1937
|
+
-n, --line-numbers Show line numbers in debug output
|
|
1938
|
+
--dot-all Dot matches newline (sed 's' flag)
|
|
1939
|
+
--no-global Replace only first occurrence in each line (not global)
|
|
1940
|
+
|
|
1941
|
+
Tool-specific options (NEW in 6.1.0):
|
|
1942
|
+
--find-opts="FLAGS" Additional flags to pass to find command
|
|
1943
|
+
--sed-opts="FLAGS" Additional flags to pass to sed command
|
|
1944
|
+
--grep-opts="FLAGS" Additional flags to pass to grep command
|
|
1945
|
+
|
|
1946
|
+
Backup control:
|
|
1947
|
+
-nb, --no-backup Do not create backup files
|
|
1948
|
+
-fb, --force-backup Force backup creation even if disabled
|
|
1949
|
+
-nbf, --no-backup-folder Create backup files in same directory as original
|
|
1950
|
+
|
|
1951
|
+
Safety features:
|
|
1952
|
+
--binary-method=METHOD Binary detection method: multi_layer, file_only, grep_only
|
|
1953
|
+
--binary-check-size=N Bytes to check for binary detection (default: $DEFAULT_BINARY_CHECK_SIZE)
|
|
1954
|
+
--no-binary-skip DEPRECATED: Use --binary instead
|
|
1955
|
+
--max-backups=N Keep only N latest backups (default: $DEFAULT_MAX_BACKUPS)
|
|
1956
|
+
|
|
1957
|
+
Advanced options:
|
|
1958
|
+
-md, --max-depth NUM Maximum directory depth for recursive search
|
|
1959
|
+
-dry-run, --dry-run Show what would be changed without making modifications
|
|
1960
|
+
-no-preserve, --no-preserve-ownership Do not attempt to preserve file ownership
|
|
1961
|
+
-delim, --delimiter CHAR Use custom delimiter for sed
|
|
1962
|
+
-e, --encoding ENC File encoding
|
|
1963
|
+
-xh, --exclude-hidden Exclude hidden files and directories
|
|
1964
|
+
-xb, --exclude-binary DEPRECATED: Binary files are always excluded unless --binary is used
|
|
1965
|
+
-xs, --max-size MB Maximum file size in MB
|
|
1966
|
+
-xp, --exclude-patterns Exclude file patterns (space-separated)
|
|
1967
|
+
-xd, --exclude-dirs Exclude directory names (space-separated)
|
|
1968
|
+
-sd, --search-dir DIR Directory to search in
|
|
1969
|
+
-od, --output-dir DIR Directory to save modified files (instead of in-place)
|
|
1970
|
+
-mode, --replace-mode MODE Replacement mode: inplace, copy, or backup_only
|
|
1971
|
+
|
|
1972
|
+
Information:
|
|
1973
|
+
-h, --help Show this help message and exit
|
|
1974
|
+
-V, --version Show version information
|
|
1975
|
+
|
|
1976
|
+
Examples (note option order):
|
|
1977
|
+
${0##*/} -v "*.html" "old text" "new text"
|
|
1978
|
+
${0##*/} --binary "*.bin" "foo" "bar"
|
|
1979
|
+
${0##*/} --rollback
|
|
1980
|
+
${0##*/} --rollback="sr.backup.20231215_143022"
|
|
1981
|
+
${0##*/} -nr --verbose "*.txt" "find" "replace"
|
|
1982
|
+
${0##*/} --dry-run --binary-method=multi_layer "*.html" "search" "replace"
|
|
1983
|
+
${0##*/} -i -E --find-opts="-type f -name" "*.txt" "search" "replace" # Enhanced example
|
|
1984
|
+
|
|
1985
|
+
Tool flag examples (NEW):
|
|
1986
|
+
${0##*/} --find-opts="-type f -mtime -7" "*.log" "error" "warning"
|
|
1987
|
+
${0##*/} --sed-opts="-e 's/foo/bar/' -e 's/baz/qux/'" "*.txt" "find" "replace"
|
|
1988
|
+
${0##*/} --grep-opts="-v '^#'" "*.conf" "port" "8080"
|
|
1989
|
+
|
|
1990
|
+
Safety notes:
|
|
1991
|
+
- Binary files are SKIPPED by default for safety
|
|
1992
|
+
- Use --binary flag to explicitly allow binary file processing
|
|
1993
|
+
- Backups are created by default (use -nb to disable)
|
|
1994
|
+
- Use --dry-run to test before making changes
|
|
1995
|
+
- Use --rollback to restore from backup if something goes wrong
|
|
1996
|
+
- Each session creates a backup directory with metadata and file list
|
|
1997
|
+
|
|
1998
|
+
Session tracking:
|
|
1999
|
+
- Each run creates a unique session ID
|
|
2000
|
+
- All modified files are tracked in the session
|
|
2001
|
+
- Rollback restores ALL files modified in that session
|
|
2002
|
+
- Session metadata includes command, pattern, search/replace strings
|
|
2003
|
+
|
|
2004
|
+
Predictable parsing:
|
|
2005
|
+
Options -> Pattern -> Search -> Replace (in this exact order)
|
|
2006
|
+
EOF
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
show_help() {
|
|
2010
|
+
show_usage
|
|
2011
|
+
cat <<EOF
|
|
2012
|
+
|
|
2013
|
+
Enhanced Features in v6.1.0:
|
|
2014
|
+
1. Multi-layer binary file detection (grep + file utility)
|
|
2015
|
+
2. Session-based rollback system with backup management
|
|
2016
|
+
3. Predictable argument parsing (options before positional args)
|
|
2017
|
+
4. Enhanced safety: binary files require explicit --binary flag
|
|
2018
|
+
5. Real-time file tracking for reliable rollback
|
|
2019
|
+
6. Extended search/replace options: ignore case, word boundaries, extended regex
|
|
2020
|
+
7. Tool-specific parameter passing: --find-opts, --sed-opts, --grep-opts
|
|
2021
|
+
8. Configurable tool commands and default flags
|
|
2022
|
+
9. Improved compatibility with GNU/BSD sed variations
|
|
2023
|
+
|
|
2024
|
+
Binary Detection Methods:
|
|
2025
|
+
multi_layer (default): Use grep -I heuristic, verify with file utility
|
|
2026
|
+
file_only: Rely only on file --mime-type command
|
|
2027
|
+
grep_only: Use only grep -I heuristic (fastest)
|
|
2028
|
+
|
|
2029
|
+
Search/Replace Enhancements:
|
|
2030
|
+
-i, --ignore-case: Case-insensitive matching (grep -i, sed 'i' flag for GNU sed)
|
|
2031
|
+
-E, --extended-regex: Use extended regular expressions (grep -E, sed -r/-E)
|
|
2032
|
+
-w, --word-boundary: Match whole words only (grep -w)
|
|
2033
|
+
-m, --multiline: Multi-line mode for regex (affects sed pattern matching)
|
|
2034
|
+
-n, --line-numbers: Show line numbers in output (grep -n)
|
|
2035
|
+
--dot-all: Dot matches newline in regex (sed 's' flag)
|
|
2036
|
+
--no-global: Replace only first occurrence per line (disable 'g' flag)
|
|
2037
|
+
|
|
2038
|
+
Tool Configuration:
|
|
2039
|
+
Base tools can be configured via variables at script top:
|
|
2040
|
+
FIND_TOOL, SED_TOOL, GREP_TOOL - tool commands
|
|
2041
|
+
FIND_FLAGS, SED_FLAGS, GREP_FLAGS - default flags
|
|
2042
|
+
|
|
2043
|
+
Command-line overrides:
|
|
2044
|
+
--find-opts: Additional flags for find command
|
|
2045
|
+
--sed-opts: Additional flags for sed command
|
|
2046
|
+
--grep-opts: Additional flags for grep command
|
|
2047
|
+
|
|
2048
|
+
Rollback System:
|
|
2049
|
+
- Automatically creates metadata for each session
|
|
2050
|
+
- Tracks files in real-time during processing
|
|
2051
|
+
- List backups with --rollback-list
|
|
2052
|
+
- Restore with --rollback (latest) or --rollback=BACKUP_DIR
|
|
2053
|
+
- Automatic cleanup of old backups with --max-backups
|
|
2054
|
+
|
|
2055
|
+
Exit Codes:
|
|
2056
|
+
0 - Success
|
|
2057
|
+
1 - Invalid arguments or insufficient permissions
|
|
2058
|
+
2 - No files found matching the pattern
|
|
2059
|
+
3 - Search string not found in any files
|
|
2060
|
+
4 - Critical error during processing
|
|
2061
|
+
5 - Backup creation failed
|
|
2062
|
+
6 - Binary file detected without --binary flag
|
|
2063
|
+
7 - Rollback failed
|
|
2064
|
+
|
|
2065
|
+
Configuration:
|
|
2066
|
+
Default settings can be modified in the CONFIGURABLE DEFAULTS section
|
|
2067
|
+
Environment variables override script defaults
|
|
2068
|
+
Command-line options override both defaults and environment variables
|
|
2069
|
+
|
|
2070
|
+
Environment Variables (override defaults):
|
|
2071
|
+
SR_DEBUG Set to 'true' to enable debug mode
|
|
2072
|
+
SR_DRY_RUN Set to 'true' to enable dry-run mode
|
|
2073
|
+
SR_NO_BACKUP Set to 'true' to disable backups
|
|
2074
|
+
SR_FORCE_BACKUP Set to 'true' to force backup creation
|
|
2075
|
+
SR_MAX_DEPTH Maximum directory depth
|
|
2076
|
+
SR_MAX_FILE_SIZE_MB Maximum file size in MB
|
|
2077
|
+
SR_EXCLUDE_PATTERNS Space-separated exclude patterns
|
|
2078
|
+
SR_EXCLUDE_DIRS Space-separated exclude directories
|
|
2079
|
+
SR_SEARCH_DIR Directory to search in
|
|
2080
|
+
SR_OUTPUT_DIR Directory for output files
|
|
2081
|
+
SR_REPLACE_MODE Replacement mode
|
|
2082
|
+
SR_ALLOW_BINARY Set to 'true' to allow binary file processing
|
|
2083
|
+
SR_BINARY_METHOD Binary detection method
|
|
2084
|
+
SR_BINARY_CHECK_SIZE Bytes to check for binary detection
|
|
2085
|
+
SR_MAX_BACKUPS Maximum number of backups to keep
|
|
2086
|
+
SR_VERBOSE Set to 'true' for verbose output
|
|
2087
|
+
|
|
2088
|
+
# New in 6.1.0:
|
|
2089
|
+
SR_IGNORE_CASE Set to 'true' for case-insensitive search
|
|
2090
|
+
SR_EXTENDED_REGEX Set to 'true' for extended regex
|
|
2091
|
+
SR_WORD_BOUNDARY Set to 'true' for word boundary matching
|
|
2092
|
+
SR_MULTILINE Set to 'true' for multi-line mode
|
|
2093
|
+
SR_LINE_NUMBERS Set to 'true' to show line numbers
|
|
2094
|
+
SR_DOT_ALL Set to 'true' for dot matches newline
|
|
2095
|
+
SR_GLOBAL_REPLACE Set to 'false' to disable global replace
|
|
2096
|
+
SR_FIND_FLAGS Additional flags for find command
|
|
2097
|
+
SR_SED_FLAGS Additional flags for sed command
|
|
2098
|
+
SR_GREP_FLAGS Additional flags for grep command
|
|
2099
|
+
|
|
2100
|
+
Compatibility Notes:
|
|
2101
|
+
- Some features (ignore case in sed) require GNU sed
|
|
2102
|
+
- Extended regex syntax varies between GNU and BSD sed
|
|
2103
|
+
- Word boundary matching may differ between grep and sed
|
|
2104
|
+
- Tool-specific flags are passed directly; validate compatibility
|
|
2105
|
+
|
|
2106
|
+
Performance Tips:
|
|
2107
|
+
- Use --no-global for faster processing when only first match per line needed
|
|
2108
|
+
- Use --binary-method=grep_only for fastest binary detection
|
|
2109
|
+
- Use --max-depth to limit recursive search
|
|
2110
|
+
- Use --max-size to skip large files
|
|
2111
|
+
EOF
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
show_version() {
|
|
2115
|
+
cat <<EOF
|
|
2116
|
+
${0##*/} - Search and Replace Tool
|
|
2117
|
+
Version 6.1.0 (Enterprise Enhanced Edition)
|
|
2118
|
+
Professional text replacement utility with safety enhancements
|
|
2119
|
+
|
|
2120
|
+
New in v6.1.0:
|
|
2121
|
+
- Enhanced configuration: Tool commands and flags as variables
|
|
2122
|
+
- Extended search options: Ignore case, word boundaries, extended regex
|
|
2123
|
+
- Tool parameter passing: Direct flag passing to find/sed/grep
|
|
2124
|
+
- Improved compatibility: Better GNU/BSD sed handling
|
|
2125
|
+
- Enhanced documentation: Complete tool flag reference
|
|
2126
|
+
- Performance: Optimized flag handling and execution
|
|
2127
|
+
|
|
2128
|
+
New in v6.0.0:
|
|
2129
|
+
- Fixed: Missing perform_replace function added
|
|
2130
|
+
- Fixed: IGNORE_CASE variable reference removed
|
|
2131
|
+
- Fixed: File discovery now properly returns multiple files
|
|
2132
|
+
- Fixed: Array initialization issue resolved
|
|
2133
|
+
- Fixed: Count occurrences now works with special characters like +
|
|
2134
|
+
- Enhanced: Better handling of file paths with spaces
|
|
2135
|
+
- Improved: Session metadata includes complete command line
|
|
2136
|
+
|
|
2137
|
+
Core Features:
|
|
2138
|
+
- Professional argument parsing with 40+ options
|
|
2139
|
+
- Configurable defaults in script header
|
|
2140
|
+
- Force backup option to override defaults
|
|
2141
|
+
- Proper special character handling with custom delimiters
|
|
2142
|
+
- Configurable backup options (folder-based or in-place)
|
|
2143
|
+
- Detailed logging and statistics with color coding
|
|
2144
|
+
- Dry-run mode for safe testing
|
|
2145
|
+
- File timestamps preserved when no changes made
|
|
2146
|
+
- Preserves file ownership and permissions (configurable)
|
|
2147
|
+
- Exclude patterns and directories with wildcard support
|
|
2148
|
+
- Maximum depth and file size limits
|
|
2149
|
+
- Separate search and output directories
|
|
2150
|
+
- Multiple replacement modes: inplace, copy, backup_only
|
|
2151
|
+
- Support for multiple encodings
|
|
2152
|
+
- GNU/BSD sed compatibility detection
|
|
2153
|
+
- Session tracking for reliable rollbacks
|
|
2154
|
+
- Multi-layer binary file detection
|
|
2155
|
+
- Tool-specific parameter passing
|
|
2156
|
+
- Extended regex and search options
|
|
2157
|
+
EOF
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
# ============================================================================
|
|
2161
|
+
# COMPREHENSIVE ARGUMENT PARSING WITH ENHANCED OPTIONS
|
|
2162
|
+
# ============================================================================
|
|
2163
|
+
|
|
2164
|
+
parse_arguments() {
|
|
2165
|
+
local args=()
|
|
2166
|
+
local parsing_options=true
|
|
2167
|
+
local rollback_target=""
|
|
2168
|
+
|
|
2169
|
+
# Store original arguments for session metadata
|
|
2170
|
+
SESSION_INITIAL_ARGS=("$@")
|
|
2171
|
+
|
|
2172
|
+
# Special handling for help and version before full parsing
|
|
2173
|
+
for arg in "$@"; do
|
|
2174
|
+
case "$arg" in
|
|
2175
|
+
-h | --help | help)
|
|
2176
|
+
show_help
|
|
2177
|
+
exit 0
|
|
2178
|
+
;;
|
|
2179
|
+
-V | --version | version)
|
|
2180
|
+
show_version
|
|
2181
|
+
exit 0
|
|
2182
|
+
;;
|
|
2183
|
+
--rollback-list)
|
|
2184
|
+
list_backups
|
|
2185
|
+
exit 0
|
|
2186
|
+
;;
|
|
2187
|
+
esac
|
|
2188
|
+
done
|
|
2189
|
+
|
|
2190
|
+
# Parse all arguments
|
|
2191
|
+
while [[ $# -gt 0 ]]; do
|
|
2192
|
+
case "$1" in
|
|
2193
|
+
# Options that take arguments
|
|
2194
|
+
-md | --max-depth)
|
|
2195
|
+
if [[ $# -gt 1 && "${2}" =~ ^[0-9]+$ ]]; then
|
|
2196
|
+
MAX_DEPTH="$2"
|
|
2197
|
+
log_debug "Maximum depth set to: $MAX_DEPTH"
|
|
2198
|
+
shift 2
|
|
2199
|
+
else
|
|
2200
|
+
log_error "Missing or invalid value for --max-depth"
|
|
2201
|
+
exit 1
|
|
2202
|
+
fi
|
|
2203
|
+
;;
|
|
2204
|
+
|
|
2205
|
+
--rollback)
|
|
2206
|
+
if [[ $# -gt 1 && "${2}" != -* && ! "$2" =~ ^-- ]]; then
|
|
2207
|
+
rollback_target="$2"
|
|
2208
|
+
shift 2
|
|
2209
|
+
else
|
|
2210
|
+
rollback_target="latest"
|
|
2211
|
+
shift
|
|
2212
|
+
fi
|
|
2213
|
+
;;
|
|
2214
|
+
|
|
2215
|
+
--rollback=*)
|
|
2216
|
+
rollback_target="${1#*=}"
|
|
2217
|
+
shift
|
|
2218
|
+
;;
|
|
2219
|
+
|
|
2220
|
+
--binary-method)
|
|
2221
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2222
|
+
BINARY_DETECTION_METHOD="$2"
|
|
2223
|
+
log_debug "Binary detection method: $BINARY_DETECTION_METHOD"
|
|
2224
|
+
shift 2
|
|
2225
|
+
else
|
|
2226
|
+
log_error "Missing value for --binary-method"
|
|
2227
|
+
exit 1
|
|
2228
|
+
fi
|
|
2229
|
+
;;
|
|
2230
|
+
|
|
2231
|
+
--binary-check-size)
|
|
2232
|
+
if [[ $# -gt 1 && "${2}" =~ ^[0-9]+$ ]]; then
|
|
2233
|
+
BINARY_CHECK_SIZE="$2"
|
|
2234
|
+
log_debug "Binary check size: $BINARY_CHECK_SIZE bytes"
|
|
2235
|
+
shift 2
|
|
2236
|
+
else
|
|
2237
|
+
log_error "Missing or invalid value for --binary-check-size"
|
|
2238
|
+
exit 1
|
|
2239
|
+
fi
|
|
2240
|
+
;;
|
|
2241
|
+
|
|
2242
|
+
--max-backups)
|
|
2243
|
+
if [[ $# -gt 1 && "${2}" =~ ^[0-9]+$ ]]; then
|
|
2244
|
+
MAX_BACKUPS="$2"
|
|
2245
|
+
log_debug "Maximum backups to keep: $MAX_BACKUPS"
|
|
2246
|
+
shift 2
|
|
2247
|
+
else
|
|
2248
|
+
log_error "Missing or invalid value for --max-backups"
|
|
2249
|
+
exit 1
|
|
2250
|
+
fi
|
|
2251
|
+
;;
|
|
2252
|
+
|
|
2253
|
+
# Tool-specific options (NEW in 6.1.0)
|
|
2254
|
+
--find-opts)
|
|
2255
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2256
|
+
FIND_FLAGS="$2"
|
|
2257
|
+
log_debug "Find flags set to: $FIND_FLAGS"
|
|
2258
|
+
shift 2
|
|
2259
|
+
else
|
|
2260
|
+
log_error "Missing value for --find-opts"
|
|
2261
|
+
exit 1
|
|
2262
|
+
fi
|
|
2263
|
+
;;
|
|
2264
|
+
|
|
2265
|
+
--sed-opts)
|
|
2266
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2267
|
+
SED_FLAGS="$2"
|
|
2268
|
+
log_debug "Sed flags set to: $SED_FLAGS"
|
|
2269
|
+
shift 2
|
|
2270
|
+
else
|
|
2271
|
+
log_error "Missing value for --sed-opts"
|
|
2272
|
+
exit 1
|
|
2273
|
+
fi
|
|
2274
|
+
;;
|
|
2275
|
+
|
|
2276
|
+
--grep-opts)
|
|
2277
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2278
|
+
GREP_FLAGS="$2"
|
|
2279
|
+
log_debug "Grep flags set to: $GREP_FLAGS"
|
|
2280
|
+
shift 2
|
|
2281
|
+
else
|
|
2282
|
+
log_error "Missing value for --grep-opts"
|
|
2283
|
+
exit 1
|
|
2284
|
+
fi
|
|
2285
|
+
;;
|
|
2286
|
+
|
|
2287
|
+
# Search/replace enhancement options (NEW in 6.1.0)
|
|
2288
|
+
-i | --ignore-case)
|
|
2289
|
+
IGNORE_CASE=true
|
|
2290
|
+
log_debug "Case-insensitive search enabled"
|
|
2291
|
+
shift
|
|
2292
|
+
;;
|
|
2293
|
+
|
|
2294
|
+
-E | --extended-regex)
|
|
2295
|
+
EXTENDED_REGEX=true
|
|
2296
|
+
log_debug "Extended regular expressions enabled"
|
|
2297
|
+
shift
|
|
2298
|
+
;;
|
|
2299
|
+
|
|
2300
|
+
-w | --word-boundary)
|
|
2301
|
+
WORD_BOUNDARY=true
|
|
2302
|
+
log_debug "Word boundary matching enabled"
|
|
2303
|
+
shift
|
|
2304
|
+
;;
|
|
2305
|
+
|
|
2306
|
+
-m | --multiline)
|
|
2307
|
+
MULTILINE_MATCH=true
|
|
2308
|
+
log_debug "Multi-line mode enabled"
|
|
2309
|
+
shift
|
|
2310
|
+
;;
|
|
2311
|
+
|
|
2312
|
+
-n | --line-numbers)
|
|
2313
|
+
LINE_NUMBERS=true
|
|
2314
|
+
log_debug "Line numbers enabled"
|
|
2315
|
+
shift
|
|
2316
|
+
;;
|
|
2317
|
+
|
|
2318
|
+
--dot-all)
|
|
2319
|
+
DOT_ALL=true
|
|
2320
|
+
log_debug "Dot matches newline enabled"
|
|
2321
|
+
shift
|
|
2322
|
+
;;
|
|
2323
|
+
|
|
2324
|
+
--no-global)
|
|
2325
|
+
GLOBAL_REPLACE=false
|
|
2326
|
+
log_debug "Global replace disabled (replace first occurrence only)"
|
|
2327
|
+
shift
|
|
2328
|
+
;;
|
|
2329
|
+
|
|
2330
|
+
# Standard options
|
|
2331
|
+
-d | --debug)
|
|
2332
|
+
DEBUG_MODE=true
|
|
2333
|
+
VERBOSE_MODE=true # Debug implies verbose
|
|
2334
|
+
log_debug "Debug mode enabled"
|
|
2335
|
+
shift
|
|
2336
|
+
;;
|
|
2337
|
+
|
|
2338
|
+
-v | --verbose)
|
|
2339
|
+
VERBOSE_MODE=true
|
|
2340
|
+
log_debug "Verbose mode enabled"
|
|
2341
|
+
shift
|
|
2342
|
+
;;
|
|
2343
|
+
|
|
2344
|
+
--binary)
|
|
2345
|
+
ALLOW_BINARY=true
|
|
2346
|
+
SKIP_BINARY_FILES=false # Override old behavior
|
|
2347
|
+
log_debug "Binary file processing enabled (explicit flag)"
|
|
2348
|
+
shift
|
|
2349
|
+
;;
|
|
2350
|
+
|
|
2351
|
+
-nr | --no-recursive)
|
|
2352
|
+
RECURSIVE_MODE=false
|
|
2353
|
+
log_debug "Non-recursive mode enabled"
|
|
2354
|
+
shift
|
|
2355
|
+
;;
|
|
2356
|
+
|
|
2357
|
+
-dry-run | --dry-run)
|
|
2358
|
+
DRY_RUN=true
|
|
2359
|
+
log_debug "Dry-run mode enabled"
|
|
2360
|
+
shift
|
|
2361
|
+
;;
|
|
2362
|
+
|
|
2363
|
+
-nb | --no-backup)
|
|
2364
|
+
CREATE_BACKUPS=false
|
|
2365
|
+
log_debug "Backup creation disabled"
|
|
2366
|
+
shift
|
|
2367
|
+
;;
|
|
2368
|
+
|
|
2369
|
+
-fb | --force-backup)
|
|
2370
|
+
FORCE_BACKUP=true
|
|
2371
|
+
log_debug "Force backup enabled"
|
|
2372
|
+
shift
|
|
2373
|
+
;;
|
|
2374
|
+
|
|
2375
|
+
-nbf | --no-backup-folder)
|
|
2376
|
+
BACKUP_IN_FOLDER=false
|
|
2377
|
+
log_debug "Backup in same folder enabled"
|
|
2378
|
+
shift
|
|
2379
|
+
;;
|
|
2380
|
+
|
|
2381
|
+
-no-preserve | --no-preserve-ownership)
|
|
2382
|
+
PRESERVE_OWNERSHIP=false
|
|
2383
|
+
log_debug "File ownership preservation disabled"
|
|
2384
|
+
shift
|
|
2385
|
+
;;
|
|
2386
|
+
|
|
2387
|
+
-delim | --delimiter)
|
|
2388
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2389
|
+
SED_DELIMITER="$2"
|
|
2390
|
+
log_debug "Custom sed delimiter: $SED_DELIMITER"
|
|
2391
|
+
shift 2
|
|
2392
|
+
else
|
|
2393
|
+
log_error "Missing delimiter character"
|
|
2394
|
+
exit 1
|
|
2395
|
+
fi
|
|
2396
|
+
;;
|
|
2397
|
+
|
|
2398
|
+
-e | --encoding)
|
|
2399
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2400
|
+
ENCODING="$2"
|
|
2401
|
+
log_debug "File encoding: $ENCODING"
|
|
2402
|
+
shift 2
|
|
2403
|
+
else
|
|
2404
|
+
log_error "Missing encoding specification"
|
|
2405
|
+
exit 1
|
|
2406
|
+
fi
|
|
2407
|
+
;;
|
|
2408
|
+
|
|
2409
|
+
-xh | --exclude-hidden)
|
|
2410
|
+
SKIP_HIDDEN_FILES=true
|
|
2411
|
+
log_debug "Excluding hidden files"
|
|
2412
|
+
shift
|
|
2413
|
+
;;
|
|
2414
|
+
|
|
2415
|
+
-xb | --exclude-binary)
|
|
2416
|
+
log_warning "-xb/--exclude-binary is deprecated. Binary files are now skipped by default."
|
|
2417
|
+
log_warning "Use --binary flag to explicitly allow binary file processing."
|
|
2418
|
+
SKIP_BINARY_FILES=true
|
|
2419
|
+
shift
|
|
2420
|
+
;;
|
|
2421
|
+
|
|
2422
|
+
--no-binary-skip)
|
|
2423
|
+
log_warning "--no-binary-skip is deprecated. Use --binary instead."
|
|
2424
|
+
ALLOW_BINARY=true
|
|
2425
|
+
SKIP_BINARY_FILES=false
|
|
2426
|
+
shift
|
|
2427
|
+
;;
|
|
2428
|
+
|
|
2429
|
+
-xs | --max-size)
|
|
2430
|
+
if [[ $# -gt 1 && "${2}" =~ ^[0-9]+$ ]]; then
|
|
2431
|
+
MAX_FILE_SIZE=$(($2 * 1024 * 1024))
|
|
2432
|
+
log_debug "Maximum file size: $2 MB"
|
|
2433
|
+
shift 2
|
|
2434
|
+
else
|
|
2435
|
+
log_error "Missing or invalid value for --max-size"
|
|
2436
|
+
exit 1
|
|
2437
|
+
fi
|
|
2438
|
+
;;
|
|
2439
|
+
|
|
2440
|
+
-xp | --exclude-patterns)
|
|
2441
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2442
|
+
EXCLUDE_PATTERNS="$2"
|
|
2443
|
+
log_debug "Exclude patterns: $EXCLUDE_PATTERNS"
|
|
2444
|
+
shift 2
|
|
2445
|
+
else
|
|
2446
|
+
log_error "Missing exclude patterns"
|
|
2447
|
+
exit 1
|
|
2448
|
+
fi
|
|
2449
|
+
;;
|
|
2450
|
+
|
|
2451
|
+
-xd | --exclude-dirs)
|
|
2452
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2453
|
+
EXCLUDE_DIRS="$2"
|
|
2454
|
+
log_debug "Exclude directories: $EXCLUDE_DIRS"
|
|
2455
|
+
shift 2
|
|
2456
|
+
else
|
|
2457
|
+
log_error "Missing exclude directories"
|
|
2458
|
+
exit 1
|
|
2459
|
+
fi
|
|
2460
|
+
;;
|
|
2461
|
+
|
|
2462
|
+
-sd | --search-dir)
|
|
2463
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2464
|
+
SEARCH_DIR="$2"
|
|
2465
|
+
log_debug "Search directory: $SEARCH_DIR"
|
|
2466
|
+
shift 2
|
|
2467
|
+
else
|
|
2468
|
+
log_error "Missing search directory"
|
|
2469
|
+
exit 1
|
|
2470
|
+
fi
|
|
2471
|
+
;;
|
|
2472
|
+
|
|
2473
|
+
-od | --output-dir)
|
|
2474
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2475
|
+
OUTPUT_DIR="$2"
|
|
2476
|
+
log_debug "Output directory: $OUTPUT_DIR"
|
|
2477
|
+
shift 2
|
|
2478
|
+
else
|
|
2479
|
+
log_error "Missing output directory"
|
|
2480
|
+
exit 1
|
|
2481
|
+
fi
|
|
2482
|
+
;;
|
|
2483
|
+
|
|
2484
|
+
-mode | --replace-mode)
|
|
2485
|
+
if [[ $# -gt 1 && -n "$2" ]]; then
|
|
2486
|
+
REPLACE_MODE="$2"
|
|
2487
|
+
log_debug "Replace mode: $REPLACE_MODE"
|
|
2488
|
+
shift 2
|
|
2489
|
+
else
|
|
2490
|
+
log_error "Missing replace mode"
|
|
2491
|
+
exit 1
|
|
2492
|
+
fi
|
|
2493
|
+
;;
|
|
2494
|
+
|
|
2495
|
+
--)
|
|
2496
|
+
shift
|
|
2497
|
+
# Everything after -- is treated as positional
|
|
2498
|
+
while [[ $# -gt 0 ]]; do
|
|
2499
|
+
args+=("$1")
|
|
2500
|
+
shift
|
|
2501
|
+
done
|
|
2502
|
+
break
|
|
2503
|
+
;;
|
|
2504
|
+
|
|
2505
|
+
-*)
|
|
2506
|
+
# Unknown option
|
|
2507
|
+
log_error "Unknown option: $1"
|
|
2508
|
+
log_error "Options must come before positional arguments"
|
|
2509
|
+
show_usage
|
|
2510
|
+
exit 1
|
|
2511
|
+
;;
|
|
2512
|
+
|
|
2513
|
+
*)
|
|
2514
|
+
# Positional argument - start collecting
|
|
2515
|
+
args+=("$1")
|
|
2516
|
+
shift
|
|
2517
|
+
# After first positional, stop parsing options
|
|
2518
|
+
parsing_options=false
|
|
2519
|
+
# Collect remaining as positional
|
|
2520
|
+
while [[ $# -gt 0 ]]; do
|
|
2521
|
+
args+=("$1")
|
|
2522
|
+
shift
|
|
2523
|
+
done
|
|
2524
|
+
break
|
|
2525
|
+
;;
|
|
2526
|
+
esac
|
|
2527
|
+
done
|
|
2528
|
+
|
|
2529
|
+
# Handle rollback before regular operation
|
|
2530
|
+
if [[ -n "$rollback_target" ]]; then
|
|
2531
|
+
perform_rollback "$rollback_target"
|
|
2532
|
+
exit $?
|
|
2533
|
+
fi
|
|
2534
|
+
|
|
2535
|
+
# ============================================================================
|
|
2536
|
+
# UNIVERSAL ARGUMENT PARSING - ENHANCED FOR ALL USE CASES
|
|
2537
|
+
# ============================================================================
|
|
2538
|
+
|
|
2539
|
+
# This section handles all possible argument patterns:
|
|
2540
|
+
# 1. Standard 3-arg: pattern search replace
|
|
2541
|
+
# 2. Standard 2-arg: search replace (default pattern "*.*")
|
|
2542
|
+
# 3. Expanded shell patterns: pattern was expanded by shell, last two are search/replace
|
|
2543
|
+
# 4. Edge cases with mixed quoting and shell expansion
|
|
2544
|
+
|
|
2545
|
+
# Store original argument count for diagnostics
|
|
2546
|
+
local arg_count=${#args[@]}
|
|
2547
|
+
log_debug "Raw positional arguments received: $arg_count"
|
|
2548
|
+
log_debug "Original command line: $0 ${SESSION_INITIAL_ARGS[*]}"
|
|
2549
|
+
log_debug "Current directory: $(pwd)"
|
|
2550
|
+
log_debug "Shell expansion test - what does '*.html' expand to here: $(echo *.html)"
|
|
2551
|
+
|
|
2552
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
2553
|
+
for ((i = 0; i < arg_count; i++)); do
|
|
2554
|
+
log_debug " args[$i] = '${args[$i]}'"
|
|
2555
|
+
done
|
|
2556
|
+
# Check what files exist in current directory
|
|
2557
|
+
log_debug "Files in current directory matching *.html:"
|
|
2558
|
+
for file in *.html; do
|
|
2559
|
+
[[ -f "$file" ]] && log_debug " - $file"
|
|
2560
|
+
done
|
|
2561
|
+
fi
|
|
2562
|
+
|
|
2563
|
+
# Minimal validation
|
|
2564
|
+
if [[ $arg_count -lt 2 ]]; then
|
|
2565
|
+
log_error "Insufficient arguments. You must provide at least search and replace strings."
|
|
2566
|
+
log_error "Examples:"
|
|
2567
|
+
log_error " 3 arguments: ${0##*/} \"*.html\" \"search\" \"replace\""
|
|
2568
|
+
log_error " 2 arguments: ${0##*/} \"search\" \"replace\" (uses default pattern: *.*)"
|
|
2569
|
+
exit 1
|
|
2570
|
+
fi
|
|
2571
|
+
|
|
2572
|
+
# Function to detect if a string looks like a glob pattern
|
|
2573
|
+
is_glob_pattern() {
|
|
2574
|
+
local str="$1"
|
|
2575
|
+
[[ "$str" == *"*"* || "$str" == *"?"* || "$str" == *"["* || "$str" == *"]"* ]]
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
# Function to detect if a string looks like a file path
|
|
2579
|
+
looks_like_filepath() {
|
|
2580
|
+
local str="$1"
|
|
2581
|
+
# Check for file extension pattern (something.something) without path separator
|
|
2582
|
+
[[ "$str" =~ ^[^/]*\.[a-zA-Z0-9]{1,10}$ ]] || [[ "$str" == *"/"* ]]
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
# Function to check if argument looks like a search string (not a file)
|
|
2586
|
+
looks_like_search_string() {
|
|
2587
|
+
local str="$1"
|
|
2588
|
+
# If it contains glob patterns, it's not a pure search string
|
|
2589
|
+
if is_glob_pattern "$str"; then
|
|
2590
|
+
return 1
|
|
2591
|
+
fi
|
|
2592
|
+
# If it looks like a file path, it's not a search string
|
|
2593
|
+
if looks_like_filepath "$str"; then
|
|
2594
|
+
return 1
|
|
2595
|
+
fi
|
|
2596
|
+
# Otherwise, it's likely a search string
|
|
2597
|
+
return 0
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
# DEBUG: Print detailed analysis
|
|
2601
|
+
log_debug "=== ARGUMENT ANALYSIS ==="
|
|
2602
|
+
log_debug "Total arguments: $arg_count"
|
|
2603
|
+
log_debug "Argument breakdown:"
|
|
2604
|
+
for ((i = 0; i < arg_count; i++)); do
|
|
2605
|
+
local arg="${args[$i]}"
|
|
2606
|
+
local type="unknown"
|
|
2607
|
+
if [[ $i -eq 0 ]]; then
|
|
2608
|
+
if is_glob_pattern "$arg"; then
|
|
2609
|
+
type="GLOB_PATTERN"
|
|
2610
|
+
elif looks_like_filepath "$arg"; then
|
|
2611
|
+
type="FILE_PATH"
|
|
2612
|
+
elif looks_like_search_string "$arg"; then
|
|
2613
|
+
type="SEARCH_STRING"
|
|
2614
|
+
fi
|
|
2615
|
+
elif [[ $i -eq $((arg_count - 2)) ]] || [[ $i -eq $((arg_count - 1)) ]]; then
|
|
2616
|
+
if looks_like_search_string "$arg"; then
|
|
2617
|
+
type="SEARCH/REPLACE"
|
|
2618
|
+
elif looks_like_filepath "$arg"; then
|
|
2619
|
+
type="FILE_PATH (possible search with dots)"
|
|
2620
|
+
fi
|
|
2621
|
+
else
|
|
2622
|
+
if looks_like_filepath "$arg"; then
|
|
2623
|
+
type="FILE_PATH (middle)"
|
|
2624
|
+
else
|
|
2625
|
+
type="UNKNOWN (middle)"
|
|
2626
|
+
fi
|
|
2627
|
+
fi
|
|
2628
|
+
log_debug " [$i] '$arg' - $type"
|
|
2629
|
+
done
|
|
2630
|
+
|
|
2631
|
+
# NEW APPROACH: Determine if shell expanded glob pattern
|
|
2632
|
+
# If we have more than 3 arguments and first argument is a simple filename (not glob)
|
|
2633
|
+
# but the last two look like search strings, then shell likely expanded a glob
|
|
2634
|
+
local shell_expanded=0
|
|
2635
|
+
local detected_pattern=""
|
|
2636
|
+
|
|
2637
|
+
# ============================================================================
|
|
2638
|
+
# ENHANCED ARGUMENT PARSING WITH SHELL EXPANSION DETECTION
|
|
2639
|
+
# ============================================================================
|
|
2640
|
+
|
|
2641
|
+
# Declare global variable for file list from shell expansion
|
|
2642
|
+
declare -g FILES_LIST=()
|
|
2643
|
+
|
|
2644
|
+
if [[ $arg_count -gt 3 ]]; then
|
|
2645
|
+
log_debug "=== SHELL EXPANSION ANALYSIS START ==="
|
|
2646
|
+
log_debug "Received $arg_count arguments (expected 2-3 for normal operation)"
|
|
2647
|
+
log_debug "First argument '${args[0]}' analysis:"
|
|
2648
|
+
log_debug " Is glob pattern: $(is_glob_pattern "${args[0]}" && echo "YES" || echo "NO")"
|
|
2649
|
+
log_debug " Looks like filepath: $(looks_like_filepath "${args[0]}" && echo "YES" || echo "NO")"
|
|
2650
|
+
log_debug " File exists: $([[ -f "${args[0]}" ]] && echo "YES" || echo "NO")"
|
|
2651
|
+
log_debug " Is directory: $([[ -d "${args[0]}" ]] && echo "YES" || echo "NO")"
|
|
2652
|
+
|
|
2653
|
+
log_debug "Last two arguments analysis:"
|
|
2654
|
+
log_debug " arg[$((arg_count - 2))]='${args[-2]}' - Looks like search: $(looks_like_search_string "${args[-2]}" && echo "YES" || echo "NO")"
|
|
2655
|
+
log_debug " arg[$((arg_count - 1))]='${args[-1]}' - Looks like search: $(looks_like_search_string "${args[-1]}" && echo "YES" || echo "NO")"
|
|
2656
|
+
|
|
2657
|
+
# Check if arguments (except last two) are files
|
|
2658
|
+
local all_are_files=1
|
|
2659
|
+
local non_file_args=() # Initialize array to avoid unbound variable error
|
|
2660
|
+
|
|
2661
|
+
log_debug "Checking if arguments 0..$((arg_count - 3)) are files:"
|
|
2662
|
+
for ((i = 0; i < arg_count - 2; i++)); do
|
|
2663
|
+
local arg="${args[$i]}"
|
|
2664
|
+
if [[ -f "$arg" ]]; then
|
|
2665
|
+
log_debug " [$i] '$arg' - EXISTS as file"
|
|
2666
|
+
FILES_LIST+=("$arg")
|
|
2667
|
+
elif [[ -e "$arg" ]]; then
|
|
2668
|
+
log_debug " [$i] '$arg' - EXISTS but not a regular file"
|
|
2669
|
+
all_are_files=0
|
|
2670
|
+
non_file_args+=("$arg")
|
|
2671
|
+
elif is_glob_pattern "$arg"; then
|
|
2672
|
+
log_debug " [$i] '$arg' - GLOB pattern (would be expanded by shell)"
|
|
2673
|
+
# Check if this pattern expands to existing files
|
|
2674
|
+
local expanded_files=()
|
|
2675
|
+
for expanded in $arg; do
|
|
2676
|
+
[[ -f "$expanded" ]] && expanded_files+=("$expanded")
|
|
2677
|
+
done
|
|
2678
|
+
if [[ ${#expanded_files[@]} -gt 0 ]]; then
|
|
2679
|
+
log_debug " Expands to ${#expanded_files[@]} file(s): ${expanded_files[*]}"
|
|
2680
|
+
FILES_LIST+=("${expanded_files[@]}")
|
|
2681
|
+
else
|
|
2682
|
+
log_debug " Does not expand to any existing files"
|
|
2683
|
+
all_are_files=0
|
|
2684
|
+
non_file_args+=("$arg")
|
|
2685
|
+
fi
|
|
2686
|
+
else
|
|
2687
|
+
log_debug " [$i] '$arg' - NOT a file or pattern"
|
|
2688
|
+
all_are_files=0
|
|
2689
|
+
non_file_args+=("$arg")
|
|
2690
|
+
fi
|
|
2691
|
+
done
|
|
2692
|
+
|
|
2693
|
+
log_debug "Shell expansion analysis results:"
|
|
2694
|
+
log_debug " All arguments (except last 2) are files: $([[ $all_are_files -eq 1 ]] && echo "YES" || echo "NO")"
|
|
2695
|
+
log_debug " FILES_LIST contains ${#FILES_LIST[@]} file(s)"
|
|
2696
|
+
log_debug " Non-file arguments: ${non_file_args[*]:-}" # Use :- to avoid error if array is empty
|
|
2697
|
+
|
|
2698
|
+
# DECISION LOGIC
|
|
2699
|
+
if [[ $all_are_files -eq 1 ]] || [[ ${#FILES_LIST[@]} -gt 0 ]]; then
|
|
2700
|
+
# Case 1: Shell expanded a pattern or explicit file list
|
|
2701
|
+
if [[ ${#FILES_LIST[@]} -gt 0 ]]; then
|
|
2702
|
+
FILE_PATTERN="" # Pattern not used when explicit file list is provided
|
|
2703
|
+
SEARCH_STRING="${args[-2]}"
|
|
2704
|
+
REPLACE_STRING="${args[-1]}"
|
|
2705
|
+
|
|
2706
|
+
log_info "SHELL EXPANSION DETECTED: Processing ${#FILES_LIST[@]} file(s) from command line"
|
|
2707
|
+
log_debug "Files to process:"
|
|
2708
|
+
for ((i = 0; i < ${#FILES_LIST[@]}; i++)); do
|
|
2709
|
+
log_debug " [$i] ${FILES_LIST[$i]}"
|
|
2710
|
+
done
|
|
2711
|
+
|
|
2712
|
+
# If all files have the same extension, guess the original pattern
|
|
2713
|
+
if [[ $all_are_files -eq 1 ]] && [[ ${#FILES_LIST[@]} -ge 2 ]]; then
|
|
2714
|
+
local first_ext="${FILES_LIST[0]##*.}"
|
|
2715
|
+
local same_ext=true
|
|
2716
|
+
for file in "${FILES_LIST[@]}"; do
|
|
2717
|
+
if [[ "${file##*.}" != "$first_ext" ]]; then
|
|
2718
|
+
same_ext=false
|
|
2719
|
+
break
|
|
2720
|
+
fi
|
|
2721
|
+
done
|
|
2722
|
+
|
|
2723
|
+
if [[ "$same_ext" == true ]]; then
|
|
2724
|
+
local guessed_pattern="*.${first_ext}"
|
|
2725
|
+
log_debug "All files have .$first_ext extension, guessing original pattern: $guessed_pattern"
|
|
2726
|
+
log_info "Hint: To avoid shell expansion, use quotes:"
|
|
2727
|
+
log_info " $0 \"$guessed_pattern\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2728
|
+
fi
|
|
2729
|
+
fi
|
|
2730
|
+
else
|
|
2731
|
+
# Case 2: There are non-file arguments among the first args
|
|
2732
|
+
log_warning "Mixed arguments detected. Assuming:"
|
|
2733
|
+
log_warning " Pattern: '${args[0]}'"
|
|
2734
|
+
log_warning " Search: '${args[-2]}'"
|
|
2735
|
+
log_warning " Replace: '${args[-1]}'"
|
|
2736
|
+
log_warning ""
|
|
2737
|
+
log_warning "If this is incorrect, please use one of these formats:"
|
|
2738
|
+
log_warning " For pattern matching: $0 \"PATTERN\" \"SEARCH\" \"REPLACE\""
|
|
2739
|
+
log_warning " For specific files: $0 -- FILE1 FILE2 ... \"SEARCH\" \"REPLACE\""
|
|
2740
|
+
|
|
2741
|
+
FILE_PATTERN="${args[0]}"
|
|
2742
|
+
SEARCH_STRING="${args[-2]}"
|
|
2743
|
+
REPLACE_STRING="${args[-1]}"
|
|
2744
|
+
fi
|
|
2745
|
+
else
|
|
2746
|
+
# Case 3: Could not determine - likely an error
|
|
2747
|
+
log_error "Cannot determine argument format. Multiple possibilities:"
|
|
2748
|
+
log_error " 1. Pattern '${args[0]}' with extra arguments"
|
|
2749
|
+
log_error " 2. Multiple files but some don't exist: ${non_file_args[*]:-}"
|
|
2750
|
+
log_error " 3. Invalid arguments"
|
|
2751
|
+
log_error ""
|
|
2752
|
+
log_error "Please use one of these formats:"
|
|
2753
|
+
log_error " Pattern mode: $0 \"*.html\" \"search\" \"replace\""
|
|
2754
|
+
log_error " File list mode: $0 -- file1.html file2.html \"search\" \"replace\""
|
|
2755
|
+
log_error ""
|
|
2756
|
+
log_error "Note: Use '--' to separate options from file list"
|
|
2757
|
+
exit 1
|
|
2758
|
+
fi
|
|
2759
|
+
|
|
2760
|
+
log_debug "=== SHELL EXPANSION ANALYSIS END ==="
|
|
2761
|
+
log_debug "Final decision:"
|
|
2762
|
+
log_debug " FILE_PATTERN: '$FILE_PATTERN'"
|
|
2763
|
+
log_debug " FILES_LIST: ${#FILES_LIST[@]} items"
|
|
2764
|
+
log_debug " SEARCH_STRING: '$SEARCH_STRING'"
|
|
2765
|
+
log_debug " REPLACE_STRING: '$REPLACE_STRING'"
|
|
2766
|
+
|
|
2767
|
+
# Case 1: Standard 2-argument call (search, replace)
|
|
2768
|
+
elif [[ $arg_count -eq 2 ]]; then
|
|
2769
|
+
FILE_PATTERN="*.*"
|
|
2770
|
+
SEARCH_STRING="${args[0]}"
|
|
2771
|
+
REPLACE_STRING="${args[1]}"
|
|
2772
|
+
|
|
2773
|
+
log_debug "Detected 2-argument syntax: using default pattern='$FILE_PATTERN'"
|
|
2774
|
+
|
|
2775
|
+
# Case 2: Standard 3-argument call (pattern, search, replace)
|
|
2776
|
+
elif [[ $arg_count -eq 3 ]]; then
|
|
2777
|
+
FILE_PATTERN="${args[0]}"
|
|
2778
|
+
SEARCH_STRING="${args[1]}"
|
|
2779
|
+
REPLACE_STRING="${args[2]}"
|
|
2780
|
+
|
|
2781
|
+
log_debug "Detected 3-argument syntax: pattern='$FILE_PATTERN'"
|
|
2782
|
+
|
|
2783
|
+
else
|
|
2784
|
+
# Less than 2 arguments - error
|
|
2785
|
+
log_error "Insufficient arguments. You must provide at least search and replace strings."
|
|
2786
|
+
log_error "Examples:"
|
|
2787
|
+
log_error " 3 arguments: ${0##*/} \"*.html\" \"search\" \"replace\""
|
|
2788
|
+
log_error " 2 arguments: ${0##*/} \"search\" \"replace\" (uses default pattern: *.*)"
|
|
2789
|
+
exit 1
|
|
2790
|
+
fi
|
|
2791
|
+
|
|
2792
|
+
# ============================================================================
|
|
2793
|
+
# ADDITIONAL DEBUGGING AND VALIDATION
|
|
2794
|
+
# ============================================================================
|
|
2795
|
+
|
|
2796
|
+
log_debug "=== FINAL ARGUMENT VALIDATION ==="
|
|
2797
|
+
|
|
2798
|
+
# Check that search string is not empty
|
|
2799
|
+
if [[ -z "$SEARCH_STRING" ]]; then
|
|
2800
|
+
log_error "Search string cannot be empty"
|
|
2801
|
+
exit 1
|
|
2802
|
+
fi
|
|
2803
|
+
|
|
2804
|
+
# If we have FILES_LIST, then FILE_PATTERN should be empty
|
|
2805
|
+
if [[ ${#FILES_LIST[@]} -gt 0 ]] && [[ -n "$FILE_PATTERN" ]]; then
|
|
2806
|
+
log_debug "WARNING: Both FILES_LIST (${#FILES_LIST[@]} files) and FILE_PATTERN ('$FILE_PATTERN') are set"
|
|
2807
|
+
log_debug "Using FILES_LIST, ignoring FILE_PATTERN"
|
|
2808
|
+
FILE_PATTERN=""
|
|
2809
|
+
fi
|
|
2810
|
+
|
|
2811
|
+
# If FILE_PATTERN contains wildcards, check if it's quoted
|
|
2812
|
+
if is_glob_pattern "$FILE_PATTERN" && [[ ${#FILES_LIST[@]} -eq 0 ]]; then
|
|
2813
|
+
log_debug "Pattern '$FILE_PATTERN' contains wildcards"
|
|
2814
|
+
|
|
2815
|
+
# Check if the pattern is expanded by the shell
|
|
2816
|
+
local expanded_count=0
|
|
2817
|
+
for file in $FILE_PATTERN; do
|
|
2818
|
+
[[ -f "$file" ]] && expanded_count=$((expanded_count + 1))
|
|
2819
|
+
done
|
|
2820
|
+
|
|
2821
|
+
if [[ $expanded_count -gt 0 ]]; then
|
|
2822
|
+
log_warning "WARNING: Pattern '$FILE_PATTERN' expands to $expanded_count file(s) via shell"
|
|
2823
|
+
log_warning "To process all matching files reliably, use quotes:"
|
|
2824
|
+
log_warning " $0 \"$FILE_PATTERN\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2825
|
+
fi
|
|
2826
|
+
fi
|
|
2827
|
+
|
|
2828
|
+
# Debug information about the final decision
|
|
2829
|
+
log_info "Final configuration:"
|
|
2830
|
+
if [[ ${#FILES_LIST[@]} -gt 0 ]]; then
|
|
2831
|
+
log_info " Processing mode: EXPLICIT FILE LIST"
|
|
2832
|
+
log_info " Files to process: ${#FILES_LIST[@]} file(s)"
|
|
2833
|
+
if [[ "$VERBOSE_MODE" == true ]]; then
|
|
2834
|
+
for ((i = 0; i < ${#FILES_LIST[@]} && i < 5; i++)); do
|
|
2835
|
+
log_info " - ${FILES_LIST[$i]}"
|
|
2836
|
+
done
|
|
2837
|
+
[[ ${#FILES_LIST[@]} -gt 5 ]] && log_info " ... and $((${#FILES_LIST[@]} - 5)) more"
|
|
2838
|
+
fi
|
|
2839
|
+
else
|
|
2840
|
+
log_info " Processing mode: PATTERN MATCHING"
|
|
2841
|
+
log_info " File pattern: $FILE_PATTERN"
|
|
2842
|
+
fi
|
|
2843
|
+
log_info " Search string: $SEARCH_STRING"
|
|
2844
|
+
log_info " Replace string: $REPLACE_STRING"
|
|
2845
|
+
|
|
2846
|
+
# DEBUG: Show what the pattern will match
|
|
2847
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
2848
|
+
log_debug "Pattern '$FILE_PATTERN' will match:"
|
|
2849
|
+
for file in $FILE_PATTERN; do
|
|
2850
|
+
[[ -f "$file" ]] && log_debug " - $file"
|
|
2851
|
+
done
|
|
2852
|
+
# Also try find command
|
|
2853
|
+
log_debug "Using find to locate files:"
|
|
2854
|
+
find . -maxdepth 1 -name "$FILE_PATTERN" 2>/dev/null | while read -r file; do
|
|
2855
|
+
log_debug " - $file"
|
|
2856
|
+
done
|
|
2857
|
+
fi
|
|
2858
|
+
|
|
2859
|
+
# Check if pattern contains wildcards
|
|
2860
|
+
if is_glob_pattern "$FILE_PATTERN" && [[ ${#FILES_LIST[@]} -eq 0 ]]; then
|
|
2861
|
+
# Test if pattern without wildcards exists
|
|
2862
|
+
local clean_pattern="${FILE_PATTERN//\*/}"
|
|
2863
|
+
clean_pattern="${clean_pattern//\?/}"
|
|
2864
|
+
clean_pattern="${clean_pattern//\[/}"
|
|
2865
|
+
clean_pattern="${clean_pattern//\]/}"
|
|
2866
|
+
|
|
2867
|
+
if [[ -e "$clean_pattern" ]] && [[ "$clean_pattern" != "$FILE_PATTERN" ]]; then
|
|
2868
|
+
log_warning "Pattern '$FILE_PATTERN' contains wildcards, but '$clean_pattern' exists."
|
|
2869
|
+
log_warning "If shell expands this pattern, only '$clean_pattern' will be processed."
|
|
2870
|
+
log_warning "To process multiple files, quote the pattern:"
|
|
2871
|
+
log_warning " $0 \"$FILE_PATTERN\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2872
|
+
fi
|
|
2873
|
+
|
|
2874
|
+
# Check if we're likely processing only one file due to shell expansion
|
|
2875
|
+
local match_count=0
|
|
2876
|
+
for file in $FILE_PATTERN; do
|
|
2877
|
+
[[ -f "$file" ]] && match_count=$((match_count + 1))
|
|
2878
|
+
done
|
|
2879
|
+
|
|
2880
|
+
if [[ $match_count -le 1 ]] && [[ "$FILE_PATTERN" == *"*"* ]]; then
|
|
2881
|
+
log_warning "Pattern '$FILE_PATTERN' matches only $match_count file(s)."
|
|
2882
|
+
log_warning "Expected more files. Did shell expand the pattern?"
|
|
2883
|
+
log_warning "Try with quotes: $0 \"$FILE_PATTERN\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2884
|
+
fi
|
|
2885
|
+
fi
|
|
2886
|
+
|
|
2887
|
+
# Special warning for phone number patterns that look like file paths
|
|
2888
|
+
if [[ "$SEARCH_STRING" =~ \.[0-9]+ ]] || [[ "$REPLACE_STRING" =~ \.[0-9]+ ]]; then
|
|
2889
|
+
log_debug "Note: Search/replace strings contain dots followed by numbers (phone number format)"
|
|
2890
|
+
fi
|
|
2891
|
+
|
|
2892
|
+
# Check if pattern contains spaces (needs quotes)
|
|
2893
|
+
if [[ "$FILE_PATTERN" == *" "* ]] && [[ $arg_count -gt 3 ]]; then
|
|
2894
|
+
log_warning "Pattern contains spaces. For reliable parsing, use quotes:"
|
|
2895
|
+
log_warning " $0 \"$FILE_PATTERN\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2896
|
+
fi
|
|
2897
|
+
|
|
2898
|
+
# Check for empty search string
|
|
2899
|
+
if [[ -z "$SEARCH_STRING" ]]; then
|
|
2900
|
+
log_error "Search string cannot be empty"
|
|
2901
|
+
exit 1
|
|
2902
|
+
fi
|
|
2903
|
+
|
|
2904
|
+
# Final recommendations
|
|
2905
|
+
log_debug "=== PARSING COMPLETE ==="
|
|
2906
|
+
log_debug "Pattern: '$FILE_PATTERN'"
|
|
2907
|
+
log_debug "Search: '$SEARCH_STRING'"
|
|
2908
|
+
log_debug "Replace: '$REPLACE_STRING'"
|
|
2909
|
+
log_debug "Total args: $arg_count"
|
|
2910
|
+
|
|
2911
|
+
# Show warning about quoting for glob patterns
|
|
2912
|
+
if is_glob_pattern "$FILE_PATTERN" && [[ ${#FILES_LIST[@]} -eq 0 ]]; then
|
|
2913
|
+
log_info "Note: For glob patterns like '$FILE_PATTERN', always use quotes to prevent shell expansion."
|
|
2914
|
+
log_info " Correct syntax: $0 \"$FILE_PATTERN\" \"$SEARCH_STRING\" \"$REPLACE_STRING\""
|
|
2915
|
+
fi
|
|
2916
|
+
|
|
2917
|
+
# Apply environment variable overrides (after command line parsing)
|
|
2918
|
+
[[ "${SR_DEBUG:-}" == "true" ]] && DEBUG_MODE=true
|
|
2919
|
+
[[ "${SR_DRY_RUN:-}" == "true" ]] && DRY_RUN=true
|
|
2920
|
+
[[ "${SR_NO_BACKUP:-}" == "true" ]] && CREATE_BACKUPS=false
|
|
2921
|
+
[[ "${SR_FORCE_BACKUP:-}" == "true" ]] && FORCE_BACKUP=true
|
|
2922
|
+
[[ -n "${SR_MAX_DEPTH:-}" ]] && MAX_DEPTH="${SR_MAX_DEPTH}"
|
|
2923
|
+
[[ -n "${SR_MAX_FILE_SIZE_MB:-}" ]] && MAX_FILE_SIZE=$((SR_MAX_FILE_SIZE_MB * 1024 * 1024))
|
|
2924
|
+
[[ -n "${SR_EXCLUDE_PATTERNS:-}" ]] && EXCLUDE_PATTERNS="${SR_EXCLUDE_PATTERNS}"
|
|
2925
|
+
[[ -n "${SR_EXCLUDE_DIRS:-}" ]] && EXCLUDE_DIRS="${SR_EXCLUDE_DIRS}"
|
|
2926
|
+
[[ -n "${SR_SEARCH_DIR:-}" ]] && SEARCH_DIR="${SR_SEARCH_DIR}"
|
|
2927
|
+
[[ -n "${SR_OUTPUT_DIR:-}" ]] && OUTPUT_DIR="${SR_OUTPUT_DIR}"
|
|
2928
|
+
[[ -n "${SR_REPLACE_MODE:-}" ]] && REPLACE_MODE="${SR_REPLACE_MODE}"
|
|
2929
|
+
[[ -n "${SR_ALLOW_BINARY:-}" ]] && ALLOW_BINARY="${SR_ALLOW_BINARY}"
|
|
2930
|
+
[[ -n "${SR_BINARY_METHOD:-}" ]] && BINARY_DETECTION_METHOD="${SR_BINARY_METHOD}"
|
|
2931
|
+
[[ -n "${SR_BINARY_CHECK_SIZE:-}" ]] && BINARY_CHECK_SIZE="${SR_BINARY_CHECK_SIZE}"
|
|
2932
|
+
[[ -n "${SR_MAX_BACKUPS:-}" ]] && MAX_BACKUPS="${SR_MAX_BACKUPS}"
|
|
2933
|
+
[[ -n "${SR_VERBOSE:-}" ]] && VERBOSE_MODE="${SR_VERBOSE}"
|
|
2934
|
+
|
|
2935
|
+
# New environment variables in 6.1.0
|
|
2936
|
+
[[ -n "${SR_IGNORE_CASE:-}" ]] && IGNORE_CASE="${SR_IGNORE_CASE}"
|
|
2937
|
+
[[ -n "${SR_EXTENDED_REGEX:-}" ]] && EXTENDED_REGEX="${SR_EXTENDED_REGEX}"
|
|
2938
|
+
[[ -n "${SR_WORD_BOUNDARY:-}" ]] && WORD_BOUNDARY="${SR_WORD_BOUNDARY}"
|
|
2939
|
+
[[ -n "${SR_MULTILINE:-}" ]] && MULTILINE_MATCH="${SR_MULTILINE}"
|
|
2940
|
+
[[ -n "${SR_LINE_NUMBERS:-}" ]] && LINE_NUMBERS="${SR_LINE_NUMBERS}"
|
|
2941
|
+
[[ -n "${SR_DOT_ALL:-}" ]] && DOT_ALL="${SR_DOT_ALL}"
|
|
2942
|
+
[[ -n "${SR_GLOBAL_REPLACE:-}" ]] && GLOBAL_REPLACE="${SR_GLOBAL_REPLACE}"
|
|
2943
|
+
[[ -n "${SR_FIND_FLAGS:-}" ]] && FIND_FLAGS="${SR_FIND_FLAGS}"
|
|
2944
|
+
[[ -n "${SR_SED_FLAGS:-}" ]] && SED_FLAGS="${SR_SED_FLAGS}"
|
|
2945
|
+
[[ -n "${SR_GREP_FLAGS:-}" ]] && GREP_FLAGS="${SR_GREP_FLAGS}"
|
|
2946
|
+
|
|
2947
|
+
# Validate replace mode
|
|
2948
|
+
if [[ "$REPLACE_MODE" != "inplace" && "$REPLACE_MODE" != "copy" && "$REPLACE_MODE" != "backup_only" ]]; then
|
|
2949
|
+
log_error "Invalid replace mode: $REPLACE_MODE. Must be: inplace, copy, or backup_only"
|
|
2950
|
+
exit 1
|
|
2951
|
+
fi
|
|
2952
|
+
|
|
2953
|
+
# Validate binary detection method
|
|
2954
|
+
if [[ "$BINARY_DETECTION_METHOD" != "multi_layer" &&
|
|
2955
|
+
"$BINARY_DETECTION_METHOD" != "file_only" &&
|
|
2956
|
+
"$BINARY_DETECTION_METHOD" != "grep_only" ]]; then
|
|
2957
|
+
log_error "Invalid binary detection method: $BINARY_DETECTION_METHOD"
|
|
2958
|
+
log_error "Must be: multi_layer, file_only, or grep_only"
|
|
2959
|
+
exit 1
|
|
2960
|
+
fi
|
|
2961
|
+
|
|
2962
|
+
# Force backup overrides CREATE_BACKUPS if set
|
|
2963
|
+
if [[ "$FORCE_BACKUP" == true ]]; then
|
|
2964
|
+
CREATE_BACKUPS=true
|
|
2965
|
+
log_debug "Force backup overrides: backups are enabled"
|
|
2966
|
+
fi
|
|
2967
|
+
|
|
2968
|
+
# Initialize session
|
|
2969
|
+
init_session
|
|
2970
|
+
|
|
2971
|
+
# Log configuration
|
|
2972
|
+
log_debug "=== Configuration ==="
|
|
2973
|
+
log_debug "Session ID: $SESSION_ID"
|
|
2974
|
+
log_debug "File pattern: $FILE_PATTERN"
|
|
2975
|
+
log_debug "Search string: $SEARCH_STRING"
|
|
2976
|
+
log_debug "Replace string: $REPLACE_STRING"
|
|
2977
|
+
log_debug "Recursive mode: $RECURSIVE_MODE"
|
|
2978
|
+
log_debug "Max depth: $MAX_DEPTH"
|
|
2979
|
+
log_debug "Dry run mode: $DRY_RUN"
|
|
2980
|
+
log_debug "Create backups: $CREATE_BACKUPS"
|
|
2981
|
+
log_debug "Force backup: $FORCE_BACKUP"
|
|
2982
|
+
log_debug "Allow binary: $ALLOW_BINARY"
|
|
2983
|
+
log_debug "Binary detection: $BINARY_DETECTION_METHOD"
|
|
2984
|
+
log_debug "Binary check size: $BINARY_CHECK_SIZE bytes"
|
|
2985
|
+
log_debug "Max backups: $MAX_BACKUPS"
|
|
2986
|
+
log_debug "Verbose mode: $VERBOSE_MODE"
|
|
2987
|
+
log_debug "Backup in folder: $BACKUP_IN_FOLDER"
|
|
2988
|
+
log_debug "Preserve ownership: $PRESERVE_OWNERSHIP"
|
|
2989
|
+
log_debug "Skip hidden files: $SKIP_HIDDEN_FILES"
|
|
2990
|
+
log_debug "Skip binary files: $SKIP_BINARY_FILES"
|
|
2991
|
+
log_debug "Max file size: $((MAX_FILE_SIZE / 1024 / 1024)) MB"
|
|
2992
|
+
log_debug "Exclude patterns: $EXCLUDE_PATTERNS"
|
|
2993
|
+
log_debug "Exclude dirs: $EXCLUDE_DIRS"
|
|
2994
|
+
log_debug "Search directory: $SEARCH_DIR"
|
|
2995
|
+
log_debug "Output directory: $OUTPUT_DIR"
|
|
2996
|
+
log_debug "Replace mode: $REPLACE_MODE"
|
|
2997
|
+
|
|
2998
|
+
# New configuration in 6.1.0
|
|
2999
|
+
log_debug "Ignore case: $IGNORE_CASE"
|
|
3000
|
+
log_debug "Extended regex: $EXTENDED_REGEX"
|
|
3001
|
+
log_debug "Word boundary: $WORD_BOUNDARY"
|
|
3002
|
+
log_debug "Multiline match: $MULTILINE_MATCH"
|
|
3003
|
+
log_debug "Line numbers: $LINE_NUMBERS"
|
|
3004
|
+
log_debug "Dot all: $DOT_ALL"
|
|
3005
|
+
log_debug "Global replace: $GLOBAL_REPLACE"
|
|
3006
|
+
log_debug "Find flags: $FIND_FLAGS"
|
|
3007
|
+
log_debug "Sed flags: $SED_FLAGS"
|
|
3008
|
+
log_debug "Grep flags: $GREP_FLAGS"
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
# ============================================================================
|
|
3012
|
+
# ENHANCED FILE PROCESSING FUNCTIONS WITH TOOL FLAGS SUPPORT
|
|
3013
|
+
# ============================================================================
|
|
3014
|
+
|
|
3015
|
+
should_exclude_file() {
|
|
3016
|
+
local file="$1"
|
|
3017
|
+
local filename
|
|
3018
|
+
|
|
3019
|
+
filename=$(basename "$file")
|
|
3020
|
+
|
|
3021
|
+
log_debug "Checking if should exclude file: $file (basename: $filename)"
|
|
3022
|
+
|
|
3023
|
+
# Skip hidden files if configured
|
|
3024
|
+
if [[ "$SKIP_HIDDEN_FILES" == true ]] && [[ "$filename" == .* ]]; then
|
|
3025
|
+
log_verbose "Excluding hidden file: $file"
|
|
3026
|
+
return 0
|
|
3027
|
+
fi
|
|
3028
|
+
|
|
3029
|
+
# Check against exclude patterns
|
|
3030
|
+
local pattern
|
|
3031
|
+
for pattern in $EXCLUDE_PATTERNS; do
|
|
3032
|
+
if [[ "$filename" == $pattern ]]; then
|
|
3033
|
+
log_debug "File $file matches exclude pattern: $pattern"
|
|
3034
|
+
log_verbose "Excluding file (pattern): $file"
|
|
3035
|
+
return 0
|
|
3036
|
+
fi
|
|
3037
|
+
done
|
|
3038
|
+
|
|
3039
|
+
# Check file size
|
|
3040
|
+
if [[ -f "$file" ]]; then
|
|
3041
|
+
local filesize
|
|
3042
|
+
filesize=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)
|
|
3043
|
+
if [[ "$filesize" -gt "$MAX_FILE_SIZE" ]]; then
|
|
3044
|
+
log_debug "File $file too large: $filesize bytes (max: $MAX_FILE_SIZE)"
|
|
3045
|
+
log_verbose "Skipping large file: $file ($((filesize / 1024 / 1024)) MB)"
|
|
3046
|
+
return 0
|
|
3047
|
+
fi
|
|
3048
|
+
else
|
|
3049
|
+
log_debug "File $file not found or not a regular file"
|
|
3050
|
+
return 0
|
|
3051
|
+
fi
|
|
3052
|
+
|
|
3053
|
+
# Check if binary file (ENHANCED with multi-layer detection)
|
|
3054
|
+
if [[ "$SKIP_BINARY_FILES" == true ]] && [[ -f "$file" ]]; then
|
|
3055
|
+
if is_binary_file "$file"; then
|
|
3056
|
+
if [[ "$ALLOW_BINARY" == true ]]; then
|
|
3057
|
+
log_verbose "Binary file allowed (--binary flag): $file"
|
|
3058
|
+
return 1 # Don't exclude, allow processing
|
|
3059
|
+
else
|
|
3060
|
+
log_debug "File $file is binary, excluding"
|
|
3061
|
+
log_verbose "Excluding binary file (use --binary to allow): $file"
|
|
3062
|
+
return 0 # Exclude binary file
|
|
3063
|
+
fi
|
|
3064
|
+
else
|
|
3065
|
+
log_debug "File $file is not binary"
|
|
3066
|
+
fi
|
|
3067
|
+
fi
|
|
3068
|
+
|
|
3069
|
+
log_debug "File $file passed all exclusion checks"
|
|
3070
|
+
return 1
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
should_exclude_dir() {
|
|
3074
|
+
local dir="$1"
|
|
3075
|
+
local dirname
|
|
3076
|
+
|
|
3077
|
+
dirname=$(basename "$dir")
|
|
3078
|
+
|
|
3079
|
+
# Skip hidden directories if configured
|
|
3080
|
+
if [[ "$SKIP_HIDDEN_FILES" == true ]] && [[ "$dirname" == .* ]]; then
|
|
3081
|
+
return 0
|
|
3082
|
+
fi
|
|
3083
|
+
|
|
3084
|
+
# Check against exclude directories
|
|
3085
|
+
local exclude_dir
|
|
3086
|
+
for exclude_dir in $EXCLUDE_DIRS; do
|
|
3087
|
+
if [[ "$dirname" == "$exclude_dir" ]]; then
|
|
3088
|
+
log_debug "Excluding directory: $dir"
|
|
3089
|
+
return 0
|
|
3090
|
+
fi
|
|
3091
|
+
done
|
|
3092
|
+
|
|
3093
|
+
return 1
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
# Function to find files - properly handles debug output with enhanced find flags
|
|
3097
|
+
find_files_simple() {
|
|
3098
|
+
local pattern="$1"
|
|
3099
|
+
local search_dir="$2"
|
|
3100
|
+
local recursive="$3"
|
|
3101
|
+
local files=()
|
|
3102
|
+
|
|
3103
|
+
# Save current directory and change to search directory
|
|
3104
|
+
log_debug "Finding files with pattern: $pattern in $search_dir"
|
|
3105
|
+
|
|
3106
|
+
local original_dir
|
|
3107
|
+
original_dir=$(pwd)
|
|
3108
|
+
cd "$search_dir" || {
|
|
3109
|
+
log_error "Cannot change to search directory: $search_dir"
|
|
3110
|
+
return 1
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
# Enable shell options for globbing
|
|
3114
|
+
shopt -s nullglob 2>/dev/null || true
|
|
3115
|
+
shopt -s dotglob 2>/dev/null || true
|
|
3116
|
+
|
|
3117
|
+
if [[ "$recursive" == true ]]; then
|
|
3118
|
+
# Enable globstar for recursive globbing
|
|
3119
|
+
if shopt -q globstar 2>/dev/null; then
|
|
3120
|
+
shopt -s globstar 2>/dev/null
|
|
3121
|
+
for file in **/$pattern; do
|
|
3122
|
+
[[ -f "$file" ]] && files+=("$file")
|
|
3123
|
+
done
|
|
3124
|
+
shopt -u globstar 2>/dev/null
|
|
3125
|
+
else
|
|
3126
|
+
# Fallback to find if globstar not available (with enhanced flags)
|
|
3127
|
+
while IFS= read -r -d '' file; do
|
|
3128
|
+
files+=("$file")
|
|
3129
|
+
done < <(find . -type f -name "$pattern" $FIND_FLAGS -print0 2>/dev/null)
|
|
3130
|
+
fi
|
|
3131
|
+
else
|
|
3132
|
+
# Non-recursive search
|
|
3133
|
+
for file in $pattern; do
|
|
3134
|
+
[[ -f "$file" ]] && files+=("$file")
|
|
3135
|
+
done
|
|
3136
|
+
fi
|
|
3137
|
+
|
|
3138
|
+
# Restore shell options
|
|
3139
|
+
shopt -u nullglob 2>/dev/null || true
|
|
3140
|
+
shopt -u dotglob 2>/dev/null || true
|
|
3141
|
+
|
|
3142
|
+
# Change back to original directory
|
|
3143
|
+
cd "$original_dir" || return 1
|
|
3144
|
+
|
|
3145
|
+
# Convert to absolute paths and print each on new line
|
|
3146
|
+
for file in "${files[@]}"; do
|
|
3147
|
+
local abs_file
|
|
3148
|
+
abs_file="$(cd "$search_dir" && pwd)/${file#./}"
|
|
3149
|
+
echo "$abs_file"
|
|
3150
|
+
done
|
|
3151
|
+
|
|
3152
|
+
log_debug "Found ${#files[@]} file(s) with pattern $pattern"
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
# ENHANCED: Function to create backup with preserved ownership
|
|
3156
|
+
create_backup() {
|
|
3157
|
+
local file="$1"
|
|
3158
|
+
local timestamp="$2"
|
|
3159
|
+
local owner="$3"
|
|
3160
|
+
local group="$4"
|
|
3161
|
+
local perms="$5"
|
|
3162
|
+
|
|
3163
|
+
[[ "$CREATE_BACKUPS" == false ]] && return 0
|
|
3164
|
+
|
|
3165
|
+
if [[ "$BACKUP_IN_FOLDER" == true ]]; then
|
|
3166
|
+
if [[ -z "$BACKUP_DIR" ]]; then
|
|
3167
|
+
BACKUP_DIR="${BACKUP_PREFIX}.${SESSION_ID}"
|
|
3168
|
+
|
|
3169
|
+
if ! mkdir -p "$BACKUP_DIR"; then
|
|
3170
|
+
log_error "Failed to create backup directory: $BACKUP_DIR"
|
|
3171
|
+
return 1
|
|
3172
|
+
fi
|
|
3173
|
+
|
|
3174
|
+
# Save initial session metadata when backup directory is created
|
|
3175
|
+
save_initial_session_metadata "$BACKUP_DIR"
|
|
3176
|
+
|
|
3177
|
+
if [[ "$PRESERVE_OWNERSHIP" == true ]] && [[ -n "$FIRST_FILE_OWNER" ]] && [[ -n "$FIRST_FILE_GROUP" ]]; then
|
|
3178
|
+
chown "${FIRST_FILE_OWNER}:${FIRST_FILE_GROUP}" "$BACKUP_DIR" 2>/dev/null ||
|
|
3179
|
+
log_warning "Could not set backup directory ownership (running as non-root?)"
|
|
3180
|
+
fi
|
|
3181
|
+
|
|
3182
|
+
log_info "Created backup directory: $BACKUP_DIR"
|
|
3183
|
+
fi
|
|
3184
|
+
|
|
3185
|
+
local relative_path="${file#$SEARCH_DIR/}"
|
|
3186
|
+
[[ "$relative_path" == "$file" ]] && relative_path=$(basename "$file")
|
|
3187
|
+
local backup_path="$BACKUP_DIR/$relative_path"
|
|
3188
|
+
local backup_dir="${backup_path%/*}"
|
|
3189
|
+
|
|
3190
|
+
mkdir -p "$backup_dir" 2>/dev/null || {
|
|
3191
|
+
log_warning "Cannot create backup directory: $backup_dir"
|
|
3192
|
+
return 1
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
if [[ "$PRESERVE_OWNERSHIP" == true ]] && [[ -n "$FIRST_FILE_OWNER" ]] && [[ -n "$FIRST_FILE_GROUP" ]]; then
|
|
3196
|
+
chown "${FIRST_FILE_OWNER}:${FIRST_FILE_GROUP}" "$backup_dir" 2>/dev/null || true
|
|
3197
|
+
fi
|
|
3198
|
+
|
|
3199
|
+
if cp --preserve=all "$file" "$backup_path" 2>/dev/null || cp "$file" "$backup_path" 2>/dev/null; then
|
|
3200
|
+
if [[ "$PRESERVE_OWNERSHIP" == true ]] && [[ -n "$owner" ]] && [[ -n "$group" ]]; then
|
|
3201
|
+
chown "${owner}:${group}" "$backup_path" 2>/dev/null || true
|
|
3202
|
+
fi
|
|
3203
|
+
|
|
3204
|
+
[[ -n "$perms" ]] && chmod "$perms" "$backup_path" 2>/dev/null || true
|
|
3205
|
+
|
|
3206
|
+
log_verbose "Created backup: $backup_path"
|
|
3207
|
+
|
|
3208
|
+
# Update file list after each successful backup
|
|
3209
|
+
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
3210
|
+
update_backup_filelist
|
|
3211
|
+
fi
|
|
3212
|
+
else
|
|
3213
|
+
log_warning "Failed to create backup for: $file"
|
|
3214
|
+
return 1
|
|
3215
|
+
fi
|
|
3216
|
+
else
|
|
3217
|
+
local backup_file="${file}.${BACKUP_PREFIX}_${SESSION_ID}"
|
|
3218
|
+
|
|
3219
|
+
if cp --preserve=all "$file" "$backup_file" 2>/dev/null || cp "$file" "$backup_file" 2>/dev/null; then
|
|
3220
|
+
if [[ "$PRESERVE_OWNERSHIP" == true ]] && [[ -n "$owner" ]] && [[ -n "$group" ]]; then
|
|
3221
|
+
chown "${owner}:${group}" "$backup_file" 2>/dev/null || true
|
|
3222
|
+
fi
|
|
3223
|
+
|
|
3224
|
+
[[ -n "$perms" ]] && chmod "$perms" "$backup_file" 2>/dev/null || true
|
|
3225
|
+
|
|
3226
|
+
log_verbose "Created backup: $backup_file"
|
|
3227
|
+
else
|
|
3228
|
+
log_warning "Failed to create backup for: $file"
|
|
3229
|
+
return 1
|
|
3230
|
+
fi
|
|
3231
|
+
fi
|
|
3232
|
+
|
|
3233
|
+
return 0
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
# Function to create output file with directory structure
|
|
3237
|
+
create_output_file() {
|
|
3238
|
+
local source_file="$1"
|
|
3239
|
+
local search_dir="$2"
|
|
3240
|
+
local output_dir="$3"
|
|
3241
|
+
local modified_content="$4"
|
|
3242
|
+
|
|
3243
|
+
# Get relative path from search directory
|
|
3244
|
+
local relative_path="${source_file#$search_dir/}"
|
|
3245
|
+
[[ "$relative_path" == "$source_file" ]] && relative_path=$(basename "$source_file")
|
|
3246
|
+
|
|
3247
|
+
local output_file="$output_dir/$relative_path"
|
|
3248
|
+
local output_file_dir="${output_file%/*}"
|
|
3249
|
+
|
|
3250
|
+
# Create directory structure
|
|
3251
|
+
mkdir -p "$output_file_dir" 2>/dev/null || {
|
|
3252
|
+
log_error "Cannot create output directory: $output_file_dir"
|
|
3253
|
+
return 1
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
# Write modified content to output file
|
|
3257
|
+
echo "$modified_content" >"$output_file" || {
|
|
3258
|
+
log_error "Failed to write to output file: $output_file"
|
|
3259
|
+
return 1
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
# Try to preserve permissions from source
|
|
3263
|
+
if [[ -f "$source_file" ]] && [[ "$PRESERVE_OWNERSHIP" == true ]]; then
|
|
3264
|
+
local source_perms
|
|
3265
|
+
source_perms=$(stat -c "%a" "$source_file" 2>/dev/null)
|
|
3266
|
+
[[ -n "$source_perms" ]] && chmod "$source_perms" "$output_file" 2>/dev/null || true
|
|
3267
|
+
fi
|
|
3268
|
+
|
|
3269
|
+
log_verbose "Created output file: $output_file"
|
|
3270
|
+
echo "$output_file"
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
# ============================================================================
|
|
3274
|
+
# ENHANCED FILE PROCESSING WITH DUAL MODE SUPPORT AND TOOL FLAGS
|
|
3275
|
+
# ============================================================================
|
|
3276
|
+
|
|
3277
|
+
process_files() {
|
|
3278
|
+
local timestamp files=()
|
|
3279
|
+
local search_escaped replace_escaped
|
|
3280
|
+
local start_time end_time processing_time
|
|
3281
|
+
|
|
3282
|
+
# Initialize escaped variables at the beginning
|
|
3283
|
+
search_escaped=$(escape_regex "$SEARCH_STRING")
|
|
3284
|
+
replace_escaped=$(escape_replacement "$REPLACE_STRING")
|
|
3285
|
+
|
|
3286
|
+
start_time=$(date +%s.%N)
|
|
3287
|
+
timestamp=$(date +"$TIMESTAMP_FORMAT")
|
|
3288
|
+
|
|
3289
|
+
log_header "=== FILE PROCESSING STARTED ==="
|
|
3290
|
+
log_info "Session ID: $SESSION_ID"
|
|
3291
|
+
log_info "Start time: $(date)"
|
|
3292
|
+
log_verbose "Current directory: $(pwd)"
|
|
3293
|
+
log_debug "Search directory: $SEARCH_DIR"
|
|
3294
|
+
log_debug "Recursive mode: $RECURSIVE_MODE"
|
|
3295
|
+
log_debug "Max depth: $MAX_DEPTH"
|
|
3296
|
+
log_debug "Find flags: $FIND_FLAGS"
|
|
3297
|
+
log_debug "Sed flags: $SED_FLAGS"
|
|
3298
|
+
log_debug "Grep flags: $GREP_FLAGS"
|
|
3299
|
+
|
|
3300
|
+
# MODE 1: EXPLICIT FILE LIST FROM SHELL EXPANSION OR USER INPUT
|
|
3301
|
+
if [[ ${#FILES_LIST[@]} -gt 0 ]]; then
|
|
3302
|
+
log_info "=== MODE: EXPLICIT FILE LIST ==="
|
|
3303
|
+
log_info "Processing ${#FILES_LIST[@]} file(s) from command line"
|
|
3304
|
+
|
|
3305
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
3306
|
+
log_debug "FILES_LIST detailed analysis:"
|
|
3307
|
+
for ((i = 0; i < ${#FILES_LIST[@]}; i++)); do
|
|
3308
|
+
local file="${FILES_LIST[$i]}"
|
|
3309
|
+
local file_status=""
|
|
3310
|
+
|
|
3311
|
+
if [[ ! -e "$file" ]]; then
|
|
3312
|
+
file_status="DOES NOT EXIST"
|
|
3313
|
+
elif [[ -d "$file" ]]; then
|
|
3314
|
+
file_status="DIRECTORY (will be skipped)"
|
|
3315
|
+
elif [[ ! -f "$file" ]]; then
|
|
3316
|
+
file_status="NOT REGULAR FILE"
|
|
3317
|
+
elif [[ ! -r "$file" ]]; then
|
|
3318
|
+
file_status="NOT READABLE"
|
|
3319
|
+
else
|
|
3320
|
+
file_status="OK - $(stat -c "%s" "$file" 2>/dev/null || echo "?") bytes"
|
|
3321
|
+
fi
|
|
3322
|
+
|
|
3323
|
+
log_debug " [$i] '$file' - $file_status"
|
|
3324
|
+
done
|
|
3325
|
+
fi
|
|
3326
|
+
|
|
3327
|
+
# Filter and validate files
|
|
3328
|
+
local valid_files=()
|
|
3329
|
+
local skipped_files=()
|
|
3330
|
+
local skip_reasons=()
|
|
3331
|
+
|
|
3332
|
+
for file in "${FILES_LIST[@]}"; do
|
|
3333
|
+
local skip_reason=""
|
|
3334
|
+
local normalized_file
|
|
3335
|
+
|
|
3336
|
+
# Convert to absolute path for consistent checking
|
|
3337
|
+
if [[ "$file" == /* ]]; then
|
|
3338
|
+
normalized_file="$file"
|
|
3339
|
+
else
|
|
3340
|
+
normalized_file="$(pwd)/${file#./}"
|
|
3341
|
+
fi
|
|
3342
|
+
|
|
3343
|
+
log_debug "Validating file: $file (normalized: $normalized_file)"
|
|
3344
|
+
|
|
3345
|
+
# Skip backup directories (check if file is inside a backup directory)
|
|
3346
|
+
local in_backup_dir=false
|
|
3347
|
+
local path_part="$normalized_file"
|
|
3348
|
+
while [[ "$path_part" != "/" ]] && [[ -n "$path_part" ]]; do
|
|
3349
|
+
local dir_name=$(basename "$path_part")
|
|
3350
|
+
if [[ "$dir_name" == "${BACKUP_PREFIX}."* ]]; then
|
|
3351
|
+
in_backup_dir=true
|
|
3352
|
+
skip_reason="inside backup directory '$dir_name'"
|
|
3353
|
+
log_debug " Skipping: File is inside backup directory: $dir_name"
|
|
3354
|
+
break
|
|
3355
|
+
fi
|
|
3356
|
+
path_part=$(dirname "$path_part")
|
|
3357
|
+
done
|
|
3358
|
+
|
|
3359
|
+
if [[ "$in_backup_dir" == true ]]; then
|
|
3360
|
+
skipped_files+=("$file")
|
|
3361
|
+
skip_reasons+=("$skip_reason")
|
|
3362
|
+
continue
|
|
3363
|
+
fi
|
|
3364
|
+
|
|
3365
|
+
# Basic file validation
|
|
3366
|
+
if [[ ! -e "$file" ]]; then
|
|
3367
|
+
skip_reason="file does not exist"
|
|
3368
|
+
elif [[ -d "$file" ]]; then
|
|
3369
|
+
skip_reason="is a directory"
|
|
3370
|
+
elif [[ ! -f "$file" ]]; then
|
|
3371
|
+
skip_reason="not a regular file"
|
|
3372
|
+
elif [[ ! -r "$file" ]]; then
|
|
3373
|
+
skip_reason="file not readable"
|
|
3374
|
+
elif should_exclude_file "$file"; then
|
|
3375
|
+
skip_reason="excluded by filters"
|
|
3376
|
+
fi
|
|
3377
|
+
|
|
3378
|
+
if [[ -n "$skip_reason" ]]; then
|
|
3379
|
+
skipped_files+=("$file")
|
|
3380
|
+
skip_reasons+=("$skip_reason")
|
|
3381
|
+
log_debug " Skipping: $skip_reason"
|
|
3382
|
+
else
|
|
3383
|
+
valid_files+=("$file")
|
|
3384
|
+
log_debug " Accepted: $file"
|
|
3385
|
+
fi
|
|
3386
|
+
done
|
|
3387
|
+
|
|
3388
|
+
# Proper array validation without unbound variable error
|
|
3389
|
+
if [[ ${#valid_files[@]} -gt 0 ]]; then
|
|
3390
|
+
files=("${valid_files[@]}")
|
|
3391
|
+
log_info "Will process ${#files[@]} valid file(s) from explicit list"
|
|
3392
|
+
else
|
|
3393
|
+
log_error "No valid files to process from the provided list"
|
|
3394
|
+
return 2
|
|
3395
|
+
fi
|
|
3396
|
+
|
|
3397
|
+
# Report skipped files
|
|
3398
|
+
if [[ ${#skipped_files[@]} -gt 0 ]]; then
|
|
3399
|
+
log_warning "Skipped ${#skipped_files[@]} invalid or excluded file(s):"
|
|
3400
|
+
for ((i = 0; i < ${#skipped_files[@]}; i++)); do
|
|
3401
|
+
log_warning " - ${skipped_files[$i]} (${skip_reasons[$i]})"
|
|
3402
|
+
done
|
|
3403
|
+
fi
|
|
3404
|
+
|
|
3405
|
+
# MODE 2: PATTERN-BASED FILE DISCOVERY WITH ENHANCED FIND FLAGS
|
|
3406
|
+
else
|
|
3407
|
+
log_info "=== MODE: PATTERN MATCHING ==="
|
|
3408
|
+
log_info "Searching for files with pattern: $FILE_PATTERN"
|
|
3409
|
+
log_info "Search directory: $SEARCH_DIR"
|
|
3410
|
+
log_info "Find flags: $FIND_FLAGS"
|
|
3411
|
+
|
|
3412
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
3413
|
+
log_debug "Pattern analysis:"
|
|
3414
|
+
log_debug " Raw pattern: '$FILE_PATTERN'"
|
|
3415
|
+
log_debug " Contains wildcards: $(is_glob_pattern "$FILE_PATTERN" && echo "YES" || echo "NO")"
|
|
3416
|
+
log_debug " Shell would expand to: $(for f in $FILE_PATTERN; do [[ -f "$f" ]] && echo -n "$f "; done)"
|
|
3417
|
+
fi
|
|
3418
|
+
|
|
3419
|
+
# Find files using the enhanced find function with flags
|
|
3420
|
+
log_debug "Starting file discovery with pattern: $FILE_PATTERN"
|
|
3421
|
+
|
|
3422
|
+
# Use temporary file for reliable handling of large file lists
|
|
3423
|
+
local temp_file_list
|
|
3424
|
+
temp_file_list=$(mktemp 2>/dev/null || echo "/tmp/sr_filelist_$$")
|
|
3425
|
+
|
|
3426
|
+
# Enhanced find with backup directory exclusion and user flags
|
|
3427
|
+
if [[ "$RECURSIVE_MODE" == true ]]; then
|
|
3428
|
+
log_debug "Using recursive find with max depth $MAX_DEPTH"
|
|
3429
|
+
local find_cmd="$FIND_TOOL \"$SEARCH_DIR\" -maxdepth $MAX_DEPTH"
|
|
3430
|
+
else
|
|
3431
|
+
log_debug "Using non-recursive find"
|
|
3432
|
+
local find_cmd="$FIND_TOOL \"$SEARCH_DIR\" -maxdepth 1"
|
|
3433
|
+
fi
|
|
3434
|
+
|
|
3435
|
+
# Build exclusion filters
|
|
3436
|
+
local exclude_filter=""
|
|
3437
|
+
if [[ -n "$EXCLUDE_DIRS" ]]; then
|
|
3438
|
+
for dir in $EXCLUDE_DIRS; do
|
|
3439
|
+
exclude_filter+=" -name \"$dir\" -prune -o"
|
|
3440
|
+
done
|
|
3441
|
+
fi
|
|
3442
|
+
|
|
3443
|
+
# Always exclude backup directories
|
|
3444
|
+
exclude_filter+=" -name \"${BACKUP_PREFIX}.*\" -prune -o"
|
|
3445
|
+
|
|
3446
|
+
# Execute find command with user flags
|
|
3447
|
+
local find_full_cmd="$find_cmd $exclude_filter -type f -name \"$FILE_PATTERN\" $FIND_FLAGS -print0 2>/dev/null"
|
|
3448
|
+
log_debug "Find command: $find_full_cmd"
|
|
3449
|
+
|
|
3450
|
+
# Execute and capture results
|
|
3451
|
+
local find_start=$(date +%s.%N)
|
|
3452
|
+
eval "$find_full_cmd" >"$temp_file_list.tmp"
|
|
3453
|
+
|
|
3454
|
+
# Read null-terminated files into array
|
|
3455
|
+
files=()
|
|
3456
|
+
if [[ -s "$temp_file_list.tmp" ]]; then
|
|
3457
|
+
while IFS= read -r -d '' file; do
|
|
3458
|
+
[[ -n "$file" ]] && files+=("$file")
|
|
3459
|
+
done <"$temp_file_list.tmp"
|
|
3460
|
+
fi
|
|
3461
|
+
|
|
3462
|
+
local find_end=$(date +%s.%N)
|
|
3463
|
+
local find_time=$(echo "$find_end - $find_start" | bc 2>/dev/null | awk '{printf "%.3f", $0}')
|
|
3464
|
+
|
|
3465
|
+
log_debug "File discovery completed in ${find_time}s"
|
|
3466
|
+
rm -f "$temp_file_list.tmp" 2>/dev/null
|
|
3467
|
+
|
|
3468
|
+
# Alternative method if find returns nothing
|
|
3469
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
3470
|
+
log_warning "No files found via find command, trying alternative methods..."
|
|
3471
|
+
|
|
3472
|
+
# Method 1: Simple shell globbing
|
|
3473
|
+
local glob_files=()
|
|
3474
|
+
if [[ "$RECURSIVE_MODE" == true ]] && shopt -q globstar 2>/dev/null; then
|
|
3475
|
+
log_debug "Trying globstar expansion"
|
|
3476
|
+
shopt -s globstar nullglob 2>/dev/null
|
|
3477
|
+
for file in "$SEARCH_DIR"/**/"$FILE_PATTERN"; do
|
|
3478
|
+
[[ -f "$file" ]] && glob_files+=("$file")
|
|
3479
|
+
done
|
|
3480
|
+
shopt -u globstar nullglob 2>/dev/null
|
|
3481
|
+
else
|
|
3482
|
+
log_debug "Trying simple glob expansion"
|
|
3483
|
+
shopt -s nullglob 2>/dev/null
|
|
3484
|
+
for file in "$SEARCH_DIR"/"$FILE_PATTERN"; do
|
|
3485
|
+
[[ -f "$file" ]] && glob_files+=("$file")
|
|
3486
|
+
done
|
|
3487
|
+
shopt -u nullglob 2>/dev/null
|
|
3488
|
+
fi
|
|
3489
|
+
|
|
3490
|
+
if [[ ${#glob_files[@]} -gt 0 ]]; then
|
|
3491
|
+
files=("${glob_files[@]}")
|
|
3492
|
+
log_debug "Found ${#files[@]} file(s) via shell globbing"
|
|
3493
|
+
|
|
3494
|
+
# Filter out backup directory files
|
|
3495
|
+
local filtered_files=()
|
|
3496
|
+
for file in "${files[@]}"; do
|
|
3497
|
+
local skip=false
|
|
3498
|
+
local path="$file"
|
|
3499
|
+
|
|
3500
|
+
# Check if file is inside backup directory
|
|
3501
|
+
while [[ "$path" != "/" ]] && [[ -n "$path" ]]; do
|
|
3502
|
+
local dir_name=$(basename "$path")
|
|
3503
|
+
if [[ "$dir_name" == "${BACKUP_PREFIX}."* ]]; then
|
|
3504
|
+
skip=true
|
|
3505
|
+
log_debug "Excluding file in backup directory: $file"
|
|
3506
|
+
break
|
|
3507
|
+
fi
|
|
3508
|
+
[[ "$path" == "." ]] && break
|
|
3509
|
+
path=$(dirname "$path")
|
|
3510
|
+
done
|
|
3511
|
+
|
|
3512
|
+
[[ "$skip" == false ]] && filtered_files+=("$file")
|
|
3513
|
+
done
|
|
3514
|
+
|
|
3515
|
+
files=("${filtered_files[@]}")
|
|
3516
|
+
fi
|
|
3517
|
+
fi
|
|
3518
|
+
|
|
3519
|
+
if [[ ${#files[@]} -eq 0 ]]; then
|
|
3520
|
+
log_error "No files found matching pattern '$FILE_PATTERN' in '$SEARCH_DIR'"
|
|
3521
|
+
|
|
3522
|
+
# Provide debugging information
|
|
3523
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
3524
|
+
log_debug "Directory listing of $SEARCH_DIR:"
|
|
3525
|
+
ls -la "$SEARCH_DIR" 2>/dev/null | head -20
|
|
3526
|
+
|
|
3527
|
+
log_debug "Files with similar patterns:"
|
|
3528
|
+
find "$SEARCH_DIR" -maxdepth 2 -type f -name "*" 2>/dev/null |
|
|
3529
|
+
grep -i "${FILE_PATTERN//\*/}" | head -10
|
|
3530
|
+
fi
|
|
3531
|
+
|
|
3532
|
+
return 2
|
|
3533
|
+
fi
|
|
3534
|
+
|
|
3535
|
+
log_info "Found ${#files[@]} file(s) matching pattern"
|
|
3536
|
+
fi
|
|
3537
|
+
|
|
3538
|
+
# ========================================================================
|
|
3539
|
+
# COMMON PROCESSING PHASE FOR BOTH MODES WITH ENHANCED FLAGS
|
|
3540
|
+
# ========================================================================
|
|
3541
|
+
|
|
3542
|
+
local file_count=${#files[@]}
|
|
3543
|
+
log_info "=== PROCESSING $file_count FILE(S) ==="
|
|
3544
|
+
|
|
3545
|
+
# Display file list with enhanced information
|
|
3546
|
+
if [[ "$VERBOSE_MODE" == true ]] && [[ $file_count -gt 0 ]]; then
|
|
3547
|
+
log_verbose "File list (first 10):"
|
|
3548
|
+
local display_limit=$((file_count > 10 ? 10 : file_count))
|
|
3549
|
+
for ((i = 0; i < display_limit; i++)); do
|
|
3550
|
+
local file="${files[$i]}"
|
|
3551
|
+
local file_size=""
|
|
3552
|
+
if [[ -f "$file" ]]; then
|
|
3553
|
+
file_size=$(stat -c "%s" "$file" 2>/dev/null || echo "?")
|
|
3554
|
+
if [[ "$file_size" =~ ^[0-9]+$ ]]; then
|
|
3555
|
+
if [[ $file_size -gt 1048576 ]]; then
|
|
3556
|
+
file_size="$((file_size / 1048576)) MB"
|
|
3557
|
+
elif [[ $file_size -gt 1024 ]]; then
|
|
3558
|
+
file_size="$((file_size / 1024)) KB"
|
|
3559
|
+
else
|
|
3560
|
+
file_size="${file_size} bytes"
|
|
3561
|
+
fi
|
|
3562
|
+
fi
|
|
3563
|
+
fi
|
|
3564
|
+
log_verbose " [$((i + 1))] ${files[$i]} ($file_size)"
|
|
3565
|
+
done
|
|
3566
|
+
[[ $file_count -gt 10 ]] && log_verbose " ... and $((file_count - 10)) more"
|
|
3567
|
+
fi
|
|
3568
|
+
|
|
3569
|
+
# Initialize counters
|
|
3570
|
+
local processed_count=0
|
|
3571
|
+
local modified_count=0
|
|
3572
|
+
local replacement_count=0
|
|
3573
|
+
local skipped_count=0
|
|
3574
|
+
local error_count=0
|
|
3575
|
+
|
|
3576
|
+
# Detailed statistics for enhanced reporting
|
|
3577
|
+
local stats_by_extension=()
|
|
3578
|
+
local stats_by_size=("small:0" "medium:0" "large:0")
|
|
3579
|
+
local stats_by_result=("success:0" "no_change:0" "error:0" "skipped:0")
|
|
3580
|
+
|
|
3581
|
+
# File processing performance tracking
|
|
3582
|
+
local total_file_size=0
|
|
3583
|
+
local largest_file=0
|
|
3584
|
+
local largest_file_name=""
|
|
3585
|
+
local smallest_file=0
|
|
3586
|
+
local smallest_file_name=""
|
|
3587
|
+
local first_file=true
|
|
3588
|
+
|
|
3589
|
+
# Create progress tracking variables
|
|
3590
|
+
local progress_interval=$((file_count > 100 ? file_count / 20 : 5))
|
|
3591
|
+
[[ $progress_interval -lt 1 ]] && progress_interval=1
|
|
3592
|
+
|
|
3593
|
+
log_debug "Progress reporting every $progress_interval files"
|
|
3594
|
+
log_debug "Search escaped: $search_escaped"
|
|
3595
|
+
log_debug "Replace escaped: $replace_escaped"
|
|
3596
|
+
log_debug "Search options: ignore_case=$IGNORE_CASE, extended_regex=$EXTENDED_REGEX, word_boundary=$WORD_BOUNDARY"
|
|
3597
|
+
|
|
3598
|
+
# Process each file with enhanced tracking
|
|
3599
|
+
for file_idx in "${!files[@]}"; do
|
|
3600
|
+
local file="${files[$file_idx]}"
|
|
3601
|
+
local file_display="${file#$SEARCH_DIR/}"
|
|
3602
|
+
[[ "$file_display" == "$file" ]] && file_display="$file"
|
|
3603
|
+
|
|
3604
|
+
# Enhanced progress reporting with time estimation
|
|
3605
|
+
if [[ $(((file_idx + 1) % progress_interval)) -eq 0 ]] || [[ $((file_idx + 1)) -eq $file_count ]]; then
|
|
3606
|
+
local progress_pct=$(((file_idx + 1) * 100 / file_count))
|
|
3607
|
+
|
|
3608
|
+
# Calculate estimated time remaining
|
|
3609
|
+
local current_time=$(date +%s.%N)
|
|
3610
|
+
local elapsed=$(echo "$current_time - $start_time" | bc 2>/dev/null || echo "0")
|
|
3611
|
+
local estimated_total=0
|
|
3612
|
+
if [[ $processed_count -gt 0 ]] && [[ $(echo "$elapsed > 0" | bc 2>/dev/null) -eq 1 ]]; then
|
|
3613
|
+
estimated_total=$(echo "scale=2; $elapsed * $file_count / $processed_count" | bc 2>/dev/null || echo "0")
|
|
3614
|
+
local remaining=$(echo "$estimated_total - $elapsed" | bc 2>/dev/null || echo "0")
|
|
3615
|
+
if [[ $(echo "$remaining > 0" | bc 2>/dev/null) -eq 1 ]]; then
|
|
3616
|
+
log_info "Progress: $((file_idx + 1))/$file_count files ($progress_pct%) - Modified: $modified_count, Replacements: $replacement_count - Est. remaining: ${remaining}s"
|
|
3617
|
+
else
|
|
3618
|
+
log_info "Progress: $((file_idx + 1))/$file_count files ($progress_pct%) - Modified: $modified_count, Replacements: $replacement_count"
|
|
3619
|
+
fi
|
|
3620
|
+
else
|
|
3621
|
+
log_info "Progress: $((file_idx + 1))/$file_count files ($progress_pct%) - Modified: $modified_count, Replacements: $replacement_count"
|
|
3622
|
+
fi
|
|
3623
|
+
fi
|
|
3624
|
+
|
|
3625
|
+
log_debug "Processing file [$((file_idx + 1))/$file_count]: $file_display"
|
|
3626
|
+
|
|
3627
|
+
# Detailed file analysis for debugging and statistics
|
|
3628
|
+
if [[ "$DEBUG_MODE" == true ]] || [[ "$VERBOSE_MODE" == true ]]; then
|
|
3629
|
+
local file_info=""
|
|
3630
|
+
if [[ -f "$file" ]]; then
|
|
3631
|
+
local file_size=$(stat -c "%s" "$file" 2>/dev/null || echo "?")
|
|
3632
|
+
local file_perm=$(stat -c "%a" "$file" 2>/dev/null || echo "?")
|
|
3633
|
+
local file_owner=$(stat -c "%U:%G" "$file" 2>/dev/null || echo "?:?")
|
|
3634
|
+
local file_mime=$(file -b --mime-type "$file" 2>/dev/null || echo "unknown")
|
|
3635
|
+
|
|
3636
|
+
# Track file size statistics
|
|
3637
|
+
if [[ "$file_size" =~ ^[0-9]+$ ]]; then
|
|
3638
|
+
total_file_size=$((total_file_size + file_size))
|
|
3639
|
+
|
|
3640
|
+
if [[ $first_file == true ]]; then
|
|
3641
|
+
largest_file=$file_size
|
|
3642
|
+
largest_file_name="$file_display"
|
|
3643
|
+
smallest_file=$file_size
|
|
3644
|
+
smallest_file_name="$file_display"
|
|
3645
|
+
first_file=false
|
|
3646
|
+
else
|
|
3647
|
+
if [[ $file_size -gt $largest_file ]]; then
|
|
3648
|
+
largest_file=$file_size
|
|
3649
|
+
largest_file_name="$file_display"
|
|
3650
|
+
fi
|
|
3651
|
+
if [[ $file_size -lt $smallest_file ]]; then
|
|
3652
|
+
smallest_file=$file_size
|
|
3653
|
+
smallest_file_name="$file_display"
|
|
3654
|
+
fi
|
|
3655
|
+
fi
|
|
3656
|
+
|
|
3657
|
+
# Update size categories
|
|
3658
|
+
if [[ $file_size -lt 10240 ]]; then
|
|
3659
|
+
stats_by_size[0]="small:$((${stats_by_size[0]#*:} + 1))"
|
|
3660
|
+
elif [[ $file_size -lt 1048576 ]]; then
|
|
3661
|
+
stats_by_size[1]="medium:$((${stats_by_size[1]#*:} + 1))"
|
|
3662
|
+
else
|
|
3663
|
+
stats_by_size[2]="large:$((${stats_by_size[2]#*:} + 1))"
|
|
3664
|
+
fi
|
|
3665
|
+
fi
|
|
3666
|
+
|
|
3667
|
+
file_info="size=${file_size}, perm=${file_perm}, owner=${file_owner}, mime=${file_mime}"
|
|
3668
|
+
else
|
|
3669
|
+
file_info="NOT FOUND"
|
|
3670
|
+
fi
|
|
3671
|
+
log_debug " File info: $file_info"
|
|
3672
|
+
fi
|
|
3673
|
+
|
|
3674
|
+
# Additional safety checks for backup directories
|
|
3675
|
+
local in_backup_dir=false
|
|
3676
|
+
local check_path="$file"
|
|
3677
|
+
while [[ "$check_path" != "/" ]] && [[ -n "$check_path" ]]; do
|
|
3678
|
+
local dir_name=$(basename "$check_path")
|
|
3679
|
+
if [[ "$dir_name" == "${BACKUP_PREFIX}."* ]]; then
|
|
3680
|
+
in_backup_dir=true
|
|
3681
|
+
log_warning "SAFETY CHECK: Skipping file in backup directory: $file"
|
|
3682
|
+
skipped_count=$((skipped_count + 1))
|
|
3683
|
+
stats_by_result[3]="skipped:$((${stats_by_result[3]#*:} + 1))"
|
|
3684
|
+
break
|
|
3685
|
+
fi
|
|
3686
|
+
[[ "$check_path" == "." ]] && break
|
|
3687
|
+
check_path=$(dirname "$check_path")
|
|
3688
|
+
done
|
|
3689
|
+
|
|
3690
|
+
[[ "$in_backup_dir" == true ]] && continue
|
|
3691
|
+
|
|
3692
|
+
# Check file permissions and accessibility before processing
|
|
3693
|
+
if [[ -f "$file" ]] && [[ ! -r "$file" ]]; then
|
|
3694
|
+
log_warning "Cannot read file (permission denied): $file"
|
|
3695
|
+
skipped_count=$((skipped_count + 1))
|
|
3696
|
+
stats_by_result[3]="skipped:$((${stats_by_result[3]#*:} + 1))"
|
|
3697
|
+
continue
|
|
3698
|
+
fi
|
|
3699
|
+
|
|
3700
|
+
if [[ -f "$file" ]] && [[ ! -w "$file" ]] && [[ "$REPLACE_MODE" == "inplace" ]]; then
|
|
3701
|
+
log_warning "Cannot write to file (permission denied): $file"
|
|
3702
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
3703
|
+
skipped_count=$((skipped_count + 1))
|
|
3704
|
+
stats_by_result[3]="skipped:$((${stats_by_result[3]#*:} + 1))"
|
|
3705
|
+
continue
|
|
3706
|
+
fi
|
|
3707
|
+
fi
|
|
3708
|
+
|
|
3709
|
+
# Perform the replacement with error handling and enhanced flags
|
|
3710
|
+
local result=0
|
|
3711
|
+
perform_replace "$file" "$search_escaped" "$replace_escaped" "$timestamp" || result=$?
|
|
3712
|
+
|
|
3713
|
+
processed_count=$((processed_count + 1))
|
|
3714
|
+
|
|
3715
|
+
# Detailed result processing with enhanced categorization
|
|
3716
|
+
case $result in
|
|
3717
|
+
0)
|
|
3718
|
+
# File was modified successfully
|
|
3719
|
+
modified_count=$((modified_count + 1))
|
|
3720
|
+
replacement_count=$((replacement_count + TOTAL_REPLACEMENTS - replacement_count))
|
|
3721
|
+
stats_by_result[0]="success:$((${stats_by_result[0]#*:} + 1))"
|
|
3722
|
+
|
|
3723
|
+
# Track file extension statistics
|
|
3724
|
+
local ext="${file##*.}"
|
|
3725
|
+
[[ "$ext" == "$file" ]] && ext="no_extension"
|
|
3726
|
+
local found_ext=false
|
|
3727
|
+
for i in "${!stats_by_extension[@]}"; do
|
|
3728
|
+
if [[ "${stats_by_extension[$i]%%:*}" == "$ext" ]]; then
|
|
3729
|
+
local count="${stats_by_extension[$i]#*:}"
|
|
3730
|
+
stats_by_extension[$i]="${ext}:$((count + 1))"
|
|
3731
|
+
found_ext=true
|
|
3732
|
+
break
|
|
3733
|
+
fi
|
|
3734
|
+
done
|
|
3735
|
+
[[ "$found_ext" == false ]] && stats_by_extension+=("${ext}:1")
|
|
3736
|
+
|
|
3737
|
+
# Update session tracking
|
|
3738
|
+
if [[ -n "${SESSION_MODIFIED_FILES+set}" ]]; then
|
|
3739
|
+
local already_tracked=false
|
|
3740
|
+
for tracked_file in "${SESSION_MODIFIED_FILES[@]}"; do
|
|
3741
|
+
if [[ "$tracked_file" == "$file" ]]; then
|
|
3742
|
+
already_tracked=true
|
|
3743
|
+
break
|
|
3744
|
+
fi
|
|
3745
|
+
done
|
|
3746
|
+
|
|
3747
|
+
if [[ "$already_tracked" == false ]]; then
|
|
3748
|
+
SESSION_MODIFIED_FILES+=("$file")
|
|
3749
|
+
log_debug "Tracked modified file: $file (total: ${#SESSION_MODIFIED_FILES[@]})"
|
|
3750
|
+
fi
|
|
3751
|
+
fi
|
|
3752
|
+
;;
|
|
3753
|
+
1)
|
|
3754
|
+
# General error
|
|
3755
|
+
error_count=$((error_count + 1))
|
|
3756
|
+
stats_by_result[2]="error:$((${stats_by_result[2]#*:} + 1))"
|
|
3757
|
+
log_debug "General error processing file: $file"
|
|
3758
|
+
;;
|
|
3759
|
+
2)
|
|
3760
|
+
# File not found
|
|
3761
|
+
error_count=$((error_count + 1))
|
|
3762
|
+
stats_by_result[2]="error:$((${stats_by_result[2]#*:} + 1))"
|
|
3763
|
+
log_debug "File not found: $file"
|
|
3764
|
+
;;
|
|
3765
|
+
3)
|
|
3766
|
+
# Permission error
|
|
3767
|
+
error_count=$((error_count + 1))
|
|
3768
|
+
stats_by_result[2]="error:$((${stats_by_result[2]#*:} + 1))"
|
|
3769
|
+
log_debug "Permission error: $file"
|
|
3770
|
+
;;
|
|
3771
|
+
4)
|
|
3772
|
+
# Backup creation failed
|
|
3773
|
+
error_count=$((error_count + 1))
|
|
3774
|
+
stats_by_result[2]="error:$((${stats_by_result[2]#*:} + 1))"
|
|
3775
|
+
log_debug "Backup creation failed: $file"
|
|
3776
|
+
;;
|
|
3777
|
+
5)
|
|
3778
|
+
# Binary file skipped
|
|
3779
|
+
skipped_count=$((skipped_count + 1))
|
|
3780
|
+
stats_by_result[3]="skipped:$((${stats_by_result[3]#*:} + 1))"
|
|
3781
|
+
log_debug "Binary file skipped: $file"
|
|
3782
|
+
;;
|
|
3783
|
+
6)
|
|
3784
|
+
# No changes made (search string not found)
|
|
3785
|
+
stats_by_result[1]="no_change:$((${stats_by_result[1]#*:} + 1))"
|
|
3786
|
+
log_debug "No changes made: $file (search string not found)"
|
|
3787
|
+
;;
|
|
3788
|
+
*)
|
|
3789
|
+
# Unknown result code
|
|
3790
|
+
error_count=$((error_count + 1))
|
|
3791
|
+
stats_by_result[2]="error:$((${stats_by_result[2]#*:} + 1))"
|
|
3792
|
+
log_warning "Unknown result code $result for file: $file"
|
|
3793
|
+
;;
|
|
3794
|
+
esac
|
|
3795
|
+
|
|
3796
|
+
# Update backup file list in real-time
|
|
3797
|
+
if [[ "$CREATE_BACKUPS" == true ]] && [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
3798
|
+
update_backup_filelist
|
|
3799
|
+
fi
|
|
3800
|
+
done
|
|
3801
|
+
|
|
3802
|
+
# ========================================================================
|
|
3803
|
+
# PROCESSING COMPLETE - ENHANCED FINAL STATISTICS
|
|
3804
|
+
# ========================================================================
|
|
3805
|
+
|
|
3806
|
+
end_time=$(date +%s.%N)
|
|
3807
|
+
processing_time=$(echo "$end_time - $start_time" | bc 2>/dev/null | awk '{printf "%.3f", $0}')
|
|
3808
|
+
|
|
3809
|
+
log_header "=== PROCESSING COMPLETE ==="
|
|
3810
|
+
log_info "Total processing time: ${processing_time}s"
|
|
3811
|
+
log_info "Files processed: $processed_count"
|
|
3812
|
+
log_info "Files modified: $modified_count"
|
|
3813
|
+
log_info "Total replacements: $replacement_count"
|
|
3814
|
+
log_info "Files skipped: $skipped_count"
|
|
3815
|
+
log_info "Errors encountered: $error_count"
|
|
3816
|
+
|
|
3817
|
+
# Enhanced statistics reporting
|
|
3818
|
+
if [[ "$DEBUG_MODE" == true ]] || [[ "$VERBOSE_MODE" == true ]]; then
|
|
3819
|
+
log_verbose "=== DETAILED STATISTICS ==="
|
|
3820
|
+
log_verbose "By result:"
|
|
3821
|
+
for stat in "${stats_by_result[@]}"; do
|
|
3822
|
+
local type="${stat%%:*}"
|
|
3823
|
+
local count="${stat#*:}"
|
|
3824
|
+
local pct=0
|
|
3825
|
+
if [[ $processed_count -gt 0 ]]; then
|
|
3826
|
+
pct=$((count * 100 / processed_count))
|
|
3827
|
+
fi
|
|
3828
|
+
log_verbose " ${type}: $count file(s) (${pct}%)"
|
|
3829
|
+
done
|
|
3830
|
+
|
|
3831
|
+
log_verbose "By size category:"
|
|
3832
|
+
for stat in "${stats_by_size[@]}"; do
|
|
3833
|
+
local type="${stat%%:*}"
|
|
3834
|
+
local count="${stat#*:}"
|
|
3835
|
+
log_verbose " ${type}: $count file(s)"
|
|
3836
|
+
done
|
|
3837
|
+
|
|
3838
|
+
# File size statistics
|
|
3839
|
+
if [[ $processed_count -gt 0 ]]; then
|
|
3840
|
+
local avg_size=0
|
|
3841
|
+
if [[ $total_file_size -gt 0 ]]; then
|
|
3842
|
+
avg_size=$((total_file_size / processed_count))
|
|
3843
|
+
fi
|
|
3844
|
+
|
|
3845
|
+
log_verbose "File size statistics:"
|
|
3846
|
+
log_verbose " Total size: $((total_file_size / 1024)) KB"
|
|
3847
|
+
log_verbose " Average size: $((avg_size / 1024)) KB"
|
|
3848
|
+
if [[ -n "$largest_file_name" ]]; then
|
|
3849
|
+
log_verbose " Largest file: $largest_file_name ($((largest_file / 1024)) KB)"
|
|
3850
|
+
fi
|
|
3851
|
+
if [[ -n "$smallest_file_name" ]]; then
|
|
3852
|
+
log_verbose " Smallest file: $smallest_file_name ($((smallest_file / 1024)) KB)"
|
|
3853
|
+
fi
|
|
3854
|
+
fi
|
|
3855
|
+
|
|
3856
|
+
# Extension statistics
|
|
3857
|
+
if [[ ${#stats_by_extension[@]} -gt 0 ]]; then
|
|
3858
|
+
log_verbose "By extension (top 10):"
|
|
3859
|
+
# Sort by count (descending)
|
|
3860
|
+
local sorted_exts=()
|
|
3861
|
+
for ext in "${stats_by_extension[@]}"; do
|
|
3862
|
+
sorted_exts+=("$ext")
|
|
3863
|
+
done
|
|
3864
|
+
|
|
3865
|
+
# Simple bubble sort for small arrays
|
|
3866
|
+
local n=${#sorted_exts[@]}
|
|
3867
|
+
for ((i = 0; i < n-1; i++)); do
|
|
3868
|
+
for ((j = 0; j < n-i-1; j++)); do
|
|
3869
|
+
local count1="${sorted_exts[$j]#*:}"
|
|
3870
|
+
local count2="${sorted_exts[$((j+1))]#*:}"
|
|
3871
|
+
if [[ $count1 -lt $count2 ]]; then
|
|
3872
|
+
# Swap
|
|
3873
|
+
local temp="${sorted_exts[$j]}"
|
|
3874
|
+
sorted_exts[$j]="${sorted_exts[$((j+1))]}"
|
|
3875
|
+
sorted_exts[$((j+1))]="$temp"
|
|
3876
|
+
fi
|
|
3877
|
+
done
|
|
3878
|
+
done
|
|
3879
|
+
|
|
3880
|
+
local display_count=$(( ${#sorted_exts[@]} > 10 ? 10 : ${#sorted_exts[@]} ))
|
|
3881
|
+
for ((i = 0; i < display_count; i++)); do
|
|
3882
|
+
local ext="${sorted_exts[$i]%%:*}"
|
|
3883
|
+
local count="${sorted_exts[$i]#*:}"
|
|
3884
|
+
local pct=0
|
|
3885
|
+
if [[ $processed_count -gt 0 ]]; then
|
|
3886
|
+
pct=$((count * 100 / processed_count))
|
|
3887
|
+
fi
|
|
3888
|
+
log_verbose " .$ext: $count file(s) (${pct}%)"
|
|
3889
|
+
done
|
|
3890
|
+
fi
|
|
3891
|
+
|
|
3892
|
+
# Performance statistics
|
|
3893
|
+
if [[ $(echo "$processing_time > 0" | bc 2>/dev/null) -eq 1 ]]; then
|
|
3894
|
+
local rate=$(echo "scale=2; $processed_count / $processing_time" | bc 2>/dev/null)
|
|
3895
|
+
local mb_per_sec=0
|
|
3896
|
+
if [[ $total_file_size -gt 0 ]]; then
|
|
3897
|
+
mb_per_sec=$(echo "scale=2; $total_file_size / 1048576 / $processing_time" | bc 2>/dev/null)
|
|
3898
|
+
fi
|
|
3899
|
+
|
|
3900
|
+
log_verbose "Performance statistics:"
|
|
3901
|
+
log_verbose " Processing rate: ${rate} files/second"
|
|
3902
|
+
if [[ $(echo "$mb_per_sec > 0" | bc 2>/dev/null) -eq 1 ]]; then
|
|
3903
|
+
log_verbose " Data throughput: ${mb_per_sec} MB/second"
|
|
3904
|
+
fi
|
|
3905
|
+
log_verbose " Time per file: $(echo "scale=3; $processing_time / $processed_count" | bc 2>/dev/null)s"
|
|
3906
|
+
fi
|
|
3907
|
+
|
|
3908
|
+
# Session tracking information
|
|
3909
|
+
if [[ -n "$SESSION_MODIFIED_FILES+set" ]] && [[ ${#SESSION_MODIFIED_FILES[@]} -gt 0 ]]; then
|
|
3910
|
+
log_verbose "Session tracking:"
|
|
3911
|
+
log_verbose " Files in session: ${#SESSION_MODIFIED_FILES[@]}"
|
|
3912
|
+
if [[ "$VERBOSE_MODE" == true ]] && [[ ${#SESSION_MODIFIED_FILES[@]} -le 20 ]]; then
|
|
3913
|
+
log_verbose " Modified files:"
|
|
3914
|
+
for ((i = 0; i < ${#SESSION_MODIFIED_FILES[@]}; i++)); do
|
|
3915
|
+
local tracked_file="${SESSION_MODIFIED_FILES[$i]}"
|
|
3916
|
+
local display_file="${tracked_file#$SEARCH_DIR/}"
|
|
3917
|
+
[[ "$display_file" == "$tracked_file" ]] && display_file="$tracked_file"
|
|
3918
|
+
log_verbose " - $display_file"
|
|
3919
|
+
done
|
|
3920
|
+
fi
|
|
3921
|
+
fi
|
|
3922
|
+
fi
|
|
3923
|
+
|
|
3924
|
+
# Store final counts in global variables
|
|
3925
|
+
PROCESSED_FILES=$processed_count
|
|
3926
|
+
MODIFIED_FILES=$modified_count
|
|
3927
|
+
TOTAL_REPLACEMENTS=$replacement_count
|
|
3928
|
+
|
|
3929
|
+
# Final validation with enhanced error reporting
|
|
3930
|
+
if [[ $processed_count -eq 0 ]]; then
|
|
3931
|
+
log_error "No files were processed"
|
|
3932
|
+
|
|
3933
|
+
# Provide troubleshooting information
|
|
3934
|
+
if [[ ${#FILES_LIST[@]} -gt 0 ]]; then
|
|
3935
|
+
log_error "Troubleshooting for explicit file list mode:"
|
|
3936
|
+
log_error " - Check if files exist: ${FILES_LIST[*]:0:5}"
|
|
3937
|
+
log_error " - Check file permissions with: ls -la ${FILES_LIST[0]}"
|
|
3938
|
+
elif [[ -n "$FILE_PATTERN" ]]; then
|
|
3939
|
+
log_error "Troubleshooting for pattern matching mode:"
|
|
3940
|
+
log_error " - Pattern: $FILE_PATTERN"
|
|
3941
|
+
log_error " - Search directory: $SEARCH_DIR"
|
|
3942
|
+
log_error " - Test pattern manually: find \"$SEARCH_DIR\" -name \"$FILE_PATTERN\" -type f | head -5"
|
|
3943
|
+
fi
|
|
3944
|
+
|
|
3945
|
+
return 2
|
|
3946
|
+
fi
|
|
3947
|
+
|
|
3948
|
+
if [[ $modified_count -eq 0 ]] && [[ "$DRY_RUN" != true ]]; then
|
|
3949
|
+
log_warning "Search pattern not found in any processed files"
|
|
3950
|
+
log_warning " Search string: '$SEARCH_STRING'"
|
|
3951
|
+
log_warning " Replace string: '$REPLACE_STRING'"
|
|
3952
|
+
log_warning " Search options: ignore_case=$IGNORE_CASE, extended_regex=$EXTENDED_REGEX, word_boundary=$WORD_BOUNDARY"
|
|
3953
|
+
|
|
3954
|
+
return 3
|
|
3955
|
+
fi
|
|
3956
|
+
|
|
3957
|
+
# Success summary with session information
|
|
3958
|
+
if [[ "$CREATE_BACKUPS" == true ]] && [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
3959
|
+
log_info "Backup created: $BACKUP_DIR"
|
|
3960
|
+
log_info " Rollback command: $0 --rollback=$BACKUP_DIR"
|
|
3961
|
+
|
|
3962
|
+
# Count files in backup
|
|
3963
|
+
local backup_file_count=$(find "$BACKUP_DIR" -type f -not -name ".sr_*" 2>/dev/null | wc -l)
|
|
3964
|
+
if [[ $backup_file_count -gt 0 ]]; then
|
|
3965
|
+
log_info " Backup contains $backup_file_count file(s)"
|
|
3966
|
+
fi
|
|
3967
|
+
fi
|
|
3968
|
+
|
|
3969
|
+
return 0
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
# ============================================================================
|
|
3973
|
+
# COMPREHENSIVE SUMMARY WITH ENHANCED OPTIONS
|
|
3974
|
+
# ============================================================================
|
|
3975
|
+
|
|
3976
|
+
show_summary() {
|
|
3977
|
+
echo ""
|
|
3978
|
+
log_header "=== SEARCH AND REPLACE SUMMARY ==="
|
|
3979
|
+
|
|
3980
|
+
# Get tracked files count from file
|
|
3981
|
+
local tracked_files_count=0
|
|
3982
|
+
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
3983
|
+
local modified_list="$BACKUP_DIR/.sr_modified_files"
|
|
3984
|
+
if [[ -f "$modified_list" ]]; then
|
|
3985
|
+
tracked_files_count=$(wc -l <"$modified_list" 2>/dev/null || echo "0")
|
|
3986
|
+
fi
|
|
3987
|
+
fi
|
|
3988
|
+
|
|
3989
|
+
echo "Session ID: $SESSION_ID"
|
|
3990
|
+
echo "Files scanned: $PROCESSED_FILES"
|
|
3991
|
+
echo "Files modified: $MODIFIED_FILES ($tracked_files_count tracked in backup)"
|
|
3992
|
+
echo "Total replacements: $TOTAL_REPLACEMENTS"
|
|
3993
|
+
echo "Search pattern: '$SEARCH_STRING'"
|
|
3994
|
+
echo "Replace with: '$REPLACE_STRING'"
|
|
3995
|
+
echo "File pattern: $FILE_PATTERN"
|
|
3996
|
+
echo "Search directory: $SEARCH_DIR"
|
|
3997
|
+
echo "Mode: $([[ "$RECURSIVE_MODE" == true ]] && echo "Recursive (depth: $MAX_DEPTH)" || echo "Non-recursive")"
|
|
3998
|
+
echo "Binary detection: $BINARY_DETECTION_METHOD"
|
|
3999
|
+
echo "Allow binary: $([[ "$ALLOW_BINARY" == true ]] && echo "Yes (--binary used)" || echo "No (skipped if detected)")"
|
|
4000
|
+
echo "Search options:"
|
|
4001
|
+
echo " Ignore case: $([[ "$IGNORE_CASE" == true ]] && echo "Yes" || echo "No")"
|
|
4002
|
+
echo " Extended regex: $([[ "$EXTENDED_REGEX" == true ]] && echo "Yes" || echo "No")"
|
|
4003
|
+
echo " Word boundary: $([[ "$WORD_BOUNDARY" == true ]] && echo "Yes" || echo "No")"
|
|
4004
|
+
echo " Multiline: $([[ "$MULTILINE_MATCH" == true ]] && echo "Yes" || echo "No")"
|
|
4005
|
+
echo " Line numbers: $([[ "$LINE_NUMBERS" == true ]] && echo "Yes" || echo "No")"
|
|
4006
|
+
echo " Global replace: $([[ "$GLOBAL_REPLACE" == true ]] && echo "Yes" || echo "No")"
|
|
4007
|
+
echo "Tool flags:"
|
|
4008
|
+
echo " Find flags: $FIND_FLAGS"
|
|
4009
|
+
echo " Sed flags: $SED_FLAGS"
|
|
4010
|
+
echo " Grep flags: $GREP_FLAGS"
|
|
4011
|
+
echo "Backups: $([[ "$CREATE_BACKUPS" == true ]] && echo "Enabled" || echo "Disabled")"
|
|
4012
|
+
echo "Force backup: $([[ "$FORCE_BACKUP" == true ]] && echo "Yes" || echo "No")"
|
|
4013
|
+
echo "Preserve ownership: $([[ "$PRESERVE_OWNERSHIP" == true ]] && echo "Yes" || echo "No")"
|
|
4014
|
+
echo "Replace mode: $REPLACE_MODE"
|
|
4015
|
+
echo "Verbose mode: $([[ "$VERBOSE_MODE" == true ]] && echo "Yes" || echo "No")"
|
|
4016
|
+
|
|
4017
|
+
if [[ -n "$OUTPUT_DIR" ]]; then
|
|
4018
|
+
echo "Output directory: $OUTPUT_DIR"
|
|
4019
|
+
fi
|
|
4020
|
+
|
|
4021
|
+
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
4022
|
+
echo "Backup directory: $BACKUP_DIR"
|
|
4023
|
+
echo "Backup metadata: $([[ -f "$BACKUP_DIR/.sr_session_metadata" ]] && echo "Yes" || echo "No")"
|
|
4024
|
+
local filelist_count=0
|
|
4025
|
+
if [[ -f "$BACKUP_DIR/.sr_modified_files" ]]; then
|
|
4026
|
+
filelist_count=$(wc -l <"$BACKUP_DIR/.sr_modified_files" 2>/dev/null || echo "0")
|
|
4027
|
+
fi
|
|
4028
|
+
echo "Files tracked: $filelist_count"
|
|
4029
|
+
[[ -n "$FIRST_FILE_OWNER" ]] &&
|
|
4030
|
+
echo "Backup owner: $FIRST_FILE_OWNER:$FIRST_FILE_GROUP"
|
|
4031
|
+
fi
|
|
4032
|
+
|
|
4033
|
+
echo "Excluded patterns: $EXCLUDE_PATTERNS"
|
|
4034
|
+
echo "Excluded dirs: $EXCLUDE_DIRS"
|
|
4035
|
+
|
|
4036
|
+
# Show modified files if verbose
|
|
4037
|
+
if [[ "$VERBOSE_MODE" == true ]] && [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
4038
|
+
local modified_list="$BACKUP_DIR/.sr_modified_files"
|
|
4039
|
+
if [[ -f "$modified_list" ]] && [[ $tracked_files_count -gt 0 ]]; then
|
|
4040
|
+
echo ""
|
|
4041
|
+
log_info "Modified files in this session:"
|
|
4042
|
+
local display_count=0
|
|
4043
|
+
while IFS= read -r line; do
|
|
4044
|
+
[[ $display_count -ge 10 ]] && break
|
|
4045
|
+
echo " - $line"
|
|
4046
|
+
RESTORED_COUNT=$((RESTORED_COUNT + 1))
|
|
4047
|
+
done <"$modified_list"
|
|
4048
|
+
[[ $tracked_files_count -gt 10 ]] &&
|
|
4049
|
+
echo " ... and $((tracked_files_count - 10)) more"
|
|
4050
|
+
fi
|
|
4051
|
+
fi
|
|
4052
|
+
|
|
4053
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
4054
|
+
echo ""
|
|
4055
|
+
log_warning "NOTE: Dry-run mode was active."
|
|
4056
|
+
echo " No files were actually modified."
|
|
4057
|
+
fi
|
|
4058
|
+
|
|
4059
|
+
if [[ "$DEBUG_MODE" == true ]]; then
|
|
4060
|
+
echo ""
|
|
4061
|
+
log_warning "NOTE: Debug mode was active."
|
|
4062
|
+
fi
|
|
4063
|
+
|
|
4064
|
+
if [[ "$FORCE_BACKUP" == true ]] && [[ "$CREATE_BACKUPS" == true ]]; then
|
|
4065
|
+
echo ""
|
|
4066
|
+
log_warning "NOTE: Backups were forced with --force-backup option."
|
|
4067
|
+
fi
|
|
4068
|
+
|
|
4069
|
+
if [[ "$REPLACE_MODE" == "copy" ]] && [[ -n "$OUTPUT_DIR" ]]; then
|
|
4070
|
+
echo ""
|
|
4071
|
+
log_warning "NOTE: Copy mode active. Original files unchanged."
|
|
4072
|
+
echo " Modified files saved to: $OUTPUT_DIR"
|
|
4073
|
+
fi
|
|
4074
|
+
|
|
4075
|
+
if [[ "$REPLACE_MODE" == "backup_only" ]]; then
|
|
4076
|
+
echo ""
|
|
4077
|
+
log_warning "NOTE: Backup-only mode active. Original files unchanged."
|
|
4078
|
+
fi
|
|
4079
|
+
|
|
4080
|
+
# Rollback information
|
|
4081
|
+
if [[ "$CREATE_BACKUPS" == true ]] && [[ -n "$BACKUP_DIR" ]]; then
|
|
4082
|
+
echo ""
|
|
4083
|
+
log_info "Rollback commands:"
|
|
4084
|
+
echo " Restore this session: $0 --rollback=$BACKUP_DIR"
|
|
4085
|
+
echo " Restore latest: $0 --rollback"
|
|
4086
|
+
echo " List all backups: $0 --rollback-list"
|
|
4087
|
+
fi
|
|
4088
|
+
|
|
4089
|
+
if [[ "$MODIFIED_FILES" -eq 0 ]] && [[ "$DRY_RUN" != true ]]; then
|
|
4090
|
+
log_warning "No replacements were made. Search pattern not found in any files."
|
|
4091
|
+
exit 3
|
|
4092
|
+
fi
|
|
4093
|
+
|
|
4094
|
+
[[ "$PROCESSED_FILES" -gt 0 ]] && log_success "Operation completed successfully"
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
# ============================================================================
|
|
4098
|
+
# MAIN EXECUTION FUNCTION
|
|
4099
|
+
# ============================================================================
|
|
4100
|
+
|
|
4101
|
+
main() {
|
|
4102
|
+
# Force initialize arrays for safety
|
|
4103
|
+
declare -g SESSION_MODIFIED_FILES=()
|
|
4104
|
+
declare -g SESSION_INITIAL_ARGS=()
|
|
4105
|
+
|
|
4106
|
+
log_debug "Main function started"
|
|
4107
|
+
log_debug "Arguments: $@"
|
|
4108
|
+
log_debug "SESSION_MODIFIED_FILES initialized: ${#SESSION_MODIFIED_FILES[@]} items"
|
|
4109
|
+
|
|
4110
|
+
local start_time end_time duration
|
|
4111
|
+
|
|
4112
|
+
start_time=$(date +%s.%N)
|
|
4113
|
+
|
|
4114
|
+
validate_environment
|
|
4115
|
+
parse_arguments "$@"
|
|
4116
|
+
|
|
4117
|
+
log_debug "After parse_arguments: SESSION_MODIFIED_FILES has ${#SESSION_MODIFIED_FILES[@]} items"
|
|
4118
|
+
|
|
4119
|
+
# Mode warnings
|
|
4120
|
+
[[ "$DRY_RUN" == true ]] && log_warning "DRY RUN MODE - No files will be modified"
|
|
4121
|
+
[[ "$DEBUG_MODE" == true ]] && log_warning "DEBUG MODE - Detailed logging enabled"
|
|
4122
|
+
[[ "$VERBOSE_MODE" == true ]] && log_warning "VERBOSE MODE - Detailed output enabled"
|
|
4123
|
+
[[ "$CREATE_BACKUPS" == false ]] && log_warning "BACKUP CREATION DISABLED - No backup files will be created"
|
|
4124
|
+
[[ "$FORCE_BACKUP" == true ]] && log_warning "FORCE BACKUP ENABLED - Overriding backup settings"
|
|
4125
|
+
[[ "$REPLACE_MODE" != "inplace" ]] && log_warning "REPLACE MODE: $REPLACE_MODE"
|
|
4126
|
+
[[ "$ALLOW_BINARY" == true ]] && log_warning "BINARY PROCESSING ALLOWED - Binary files will be modified"
|
|
4127
|
+
[[ "$IGNORE_CASE" == true ]] && log_warning "IGNORE CASE ENABLED - Case-insensitive search"
|
|
4128
|
+
[[ "$EXTENDED_REGEX" == true ]] && log_warning "EXTENDED REGEX ENABLED - Using extended regular expressions"
|
|
4129
|
+
[[ "$WORD_BOUNDARY" == true ]] && log_warning "WORD BOUNDARY ENABLED - Matching whole words only"
|
|
4130
|
+
|
|
4131
|
+
# Binary detection method info
|
|
4132
|
+
log_info "Binary detection method: $BINARY_DETECTION_METHOD"
|
|
4133
|
+
if [[ "$BINARY_DETECTION_METHOD" == "file_only" ]] && ! command -v file >/dev/null 2>&1; then
|
|
4134
|
+
log_warning "file utility not found. Binary detection may fail."
|
|
4135
|
+
fi
|
|
4136
|
+
|
|
4137
|
+
process_files
|
|
4138
|
+
|
|
4139
|
+
log_debug "After process_files: SESSION_MODIFIED_FILES has ${#SESSION_MODIFIED_FILES[@]} items"
|
|
4140
|
+
|
|
4141
|
+
# Final metadata save after processing all files
|
|
4142
|
+
if [[ "$CREATE_BACKUPS" == true ]] && [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
|
|
4143
|
+
finalize_session_metadata
|
|
4144
|
+
fi
|
|
4145
|
+
|
|
4146
|
+
end_time=$(date +%s.%N)
|
|
4147
|
+
duration=$(echo "$end_time - $start_time" | bc 2>/dev/null || echo "0" | awk '{printf "%.2f", $0}')
|
|
4148
|
+
|
|
4149
|
+
show_summary
|
|
4150
|
+
log_verbose "Execution time: ${duration} seconds"
|
|
4151
|
+
|
|
4152
|
+
# Cleanup old backups
|
|
4153
|
+
if [[ "$CREATE_BACKUPS" == true ]] && [[ "$MAX_BACKUPS" -gt 0 ]] && [[ "$DRY_RUN" != true ]]; then
|
|
4154
|
+
cleanup_old_backups
|
|
4155
|
+
fi
|
|
4156
|
+
|
|
4157
|
+
exit 0
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
# ============================================================================
|
|
4161
|
+
# ERROR HANDLING AND ENTRY POINT
|
|
4162
|
+
# ============================================================================
|
|
4163
|
+
|
|
4164
|
+
trap 'log_error "Script interrupted by user"; exit 1' INT TERM
|
|
4165
|
+
trap 'log_error "Error occurred at line $LINENO"; exit 4' ERR
|
|
4166
|
+
|
|
4167
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
4168
|
+
[[ $# -eq 0 ]] && {
|
|
4169
|
+
show_help
|
|
4170
|
+
exit 0
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
# Quick help/version checks without full parsing
|
|
4174
|
+
for arg in "$@"; do
|
|
4175
|
+
if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then
|
|
4176
|
+
show_help
|
|
4177
|
+
exit 0
|
|
4178
|
+
fi
|
|
4179
|
+
if [[ "$arg" == "-V" || "$arg" == "--version" ]]; then
|
|
4180
|
+
show_version
|
|
4181
|
+
exit 0
|
|
4182
|
+
fi
|
|
4183
|
+
done
|
|
4184
|
+
|
|
4185
|
+
main "$@"
|
|
4186
|
+
fi
|