Skip to content

Commit 08a681b

Browse files
authored
Add options for excluding directories and hidden files
Enhanced the replace_everywhere.sh script with additional options for excluding directories and including hidden files. Updated usage instructions and improved string escaping functions.
1 parent 921eb19 commit 08a681b

File tree

1 file changed

+109
-33
lines changed

1 file changed

+109
-33
lines changed

src/replace_everywhere.sh

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,122 @@
11
#!/usr/bin/env bash
2-
2+
#
33
# Script Name: replace_everywhere.sh
4-
# Description: Replace string a with string b in all files in the current directory and all subdirectories.
5-
# Usage: replace_everywhere.sh [string_a] [string_b]
6-
# [string_a] - Old string.
7-
# [string_b] - New string.
8-
# Example: replace_everywhere.sh "cat" "dog"
4+
# Description: Replace string a with string b in all files in the current directory and subdirectories.
5+
# Skips hidden directories by default. Optionally exclude specific subdirectories.
6+
#
7+
# Usage:
8+
# replace_everywhere.sh [options] <string_a> <string_b>
9+
#
10+
# Options:
11+
# -x, --exclude DIR Exclude a subdirectory (repeatable). Examples: -x node_modules -x build
12+
# --include-hidden Include hidden directories (like .git, .venv).
13+
# -h, --help Show this help and exit.
14+
#
15+
# Examples:
16+
# replace_everywhere.sh "cat" "dog"
17+
# replace_everywhere.sh -x dist -x build 'string with space' 'new string'
18+
# replace_everywhere.sh --include-hidden 'foo**' 'bar\baz'
919
#
10-
# For strings with spaces or special characters, use single quotes:
11-
# replace_everywhere.sh 'string with space' 'new string'
12-
# replace_everywhere.sh 'string\*\*' 'new\string'
20+
# Notes:
21+
# - For literal matching, special chars are handled safely. Replacement also escapes '&' properly.
22+
# - Works on GNU sed and BSD/macOS sed.
23+
24+
set -euo pipefail
1325

1426
print_usage() {
15-
echo "Usage: replace_everywhere.sh [string_a] [string_b]"
16-
echo " [string_a] - Old string."
17-
echo " [string_b] - New string."
27+
sed -n '2,40p' "$0"
28+
}
29+
30+
# Escape a string for use as a *literal* sed pattern (s/…/…/)
31+
escape_sed_pattern() {
32+
# Escapes: \ / [ ] . ^ $ * and other regex metas so the pattern is treated literally
33+
printf '%s' "$1" | sed -e 's/[.[\*^$\/]/\\&/g' -e 's/]/\\]/g'
1834
}
1935

20-
escape_string() {
21-
printf '%s' "$1" | sed 's:[][\/.^$*]:\\&:g'
36+
# Escape replacement for sed (so '&' doesn't expand to the whole match)
37+
escape_sed_replacement() {
38+
local s=$1
39+
s=${s//\\/\\\\} # escape backslashes first
40+
s=${s//&/\\&} # then ampersands
41+
printf '%s' "$s"
2242
}
2343

2444
main() {
25-
if [ $# -ne 2 ]; then
26-
print_usage
27-
return 1
28-
fi
29-
30-
local search replace
31-
search=$(escape_string "$1")
32-
replace=$(escape_string "$2")
33-
34-
# Confirm action
35-
read -r -p "Are you sure you want to replace all occurrences of '$1' with '$2'? [y/N] " confirmation
36-
if [[ ! $confirmation =~ ^[Yy]$ ]]; then
37-
echo "Operation cancelled."
38-
return 1
39-
fi
40-
41-
# Perform the replacement
42-
find . -type f -exec sed -i "s/$search/$replace/g" {} \;
45+
local include_hidden=false
46+
local -a excludes=()
47+
48+
# Parse args (support short and long)
49+
while [[ $# -gt 0 ]]; do
50+
case "$1" in
51+
-x|--exclude)
52+
[[ $# -lt 2 ]] && { echo "Missing argument for $1" >&2; exit 2; }
53+
excludes+=("$2"); shift 2;;
54+
--include-hidden)
55+
include_hidden=true; shift;;
56+
-h|--help)
57+
print_usage; exit 0;;
58+
--) shift; break;;
59+
-*)
60+
echo "Unknown option: $1" >&2; print_usage; exit 2;;
61+
*)
62+
break;;
63+
esac
64+
done
65+
66+
if [[ $# -ne 2 ]]; then
67+
print_usage
68+
exit 1
69+
fi
70+
71+
local search_raw="$1"
72+
local replace_raw="$2"
73+
74+
local search replace
75+
search=$(escape_sed_pattern "$search_raw")
76+
replace=$(escape_sed_replacement "$replace_raw")
77+
78+
# Confirm action
79+
read -r -p "Replace ALL occurrences of '$search_raw' with '$replace_raw' in this tree? [y/N] " confirmation
80+
if [[ ! $confirmation =~ ^[Yy]$ ]]; then
81+
echo "Operation cancelled."
82+
exit 1
83+
fi
84+
85+
# Detect GNU vs BSD sed for in-place flag
86+
local -a SED_INPLACE
87+
if sed --version >/dev/null 2>&1; then
88+
SED_INPLACE=(-i)
89+
else
90+
# macOS/BSD sed needs a backup suffix (empty is allowed)
91+
SED_INPLACE=(-i '')
92+
fi
93+
94+
# Build the find prune predicates
95+
# Start with hidden directories (if not including them)
96+
local -a PRUNE_BLOCK=()
97+
if [[ "$include_hidden" == false ]]; then
98+
PRUNE_BLOCK+=( -type d -name '.*' -prune -o )
99+
fi
100+
101+
# Add user-specified excludes (treat as directory names/paths from repo root)
102+
if [[ ${#excludes[@]} -gt 0 ]]; then
103+
for d in "${excludes[@]}"; do
104+
# Normalize leading './'
105+
[[ "$d" != ./* ]] && d="./$d"
106+
PRUNE_BLOCK+=( -path "$d" -prune -o )
107+
done
108+
fi
109+
110+
# Execute replacement
111+
# Logic: find . \( PRUNES \) -o -type f -exec sed -i ... '{}' \;
112+
# Using a here-string to keep array expansions intact.
113+
if [[ ${#PRUNE_BLOCK[@]} -gt 0 ]]; then
114+
find . \( "${PRUNE_BLOCK[@]}" -false \) -o -type f -exec sed "${SED_INPLACE[@]}" "s/${search}/${replace}/g" {} +
115+
} else
116+
find . -type f -exec sed "${SED_INPLACE[@]}" "s/${search}/${replace}/g" {} +
117+
fi
118+
119+
echo "Done."
43120
}
44121

45122
main "$@"
46-

0 commit comments

Comments
 (0)