50 lines
1.3 KiB
Bash
Executable File
50 lines
1.3 KiB
Bash
Executable File
#!/opt/homebrew/bin/bash
|
||
#
|
||
# x.sh — sanitize filenames: lowercase, replace non-alnum/dot with underscores,
|
||
# collapse runs of underscores, trim, and never overwrite a *different* existing file.
|
||
#
|
||
shopt -s globstar
|
||
SAVEIFS=$IFS
|
||
IFS=$'\n' # only split on newlines
|
||
|
||
sanitize() {
|
||
local name="$1"
|
||
# 1) lowercase
|
||
name="${name,,}"
|
||
# 2) anything not a–z, 0–9, or dot → underscore
|
||
name="${name//[^a-z0-9.]/_}"
|
||
# 3) collapse multiple underscores → one underscore
|
||
name="$(echo "$name" | sed -E 's/_+/_/g')"
|
||
# 4) remove underscore adjacent to dots
|
||
name="$(echo "$name" | sed -E 's/_\.|\._/./g')"
|
||
# 5) trim leading/trailing underscores
|
||
name="$(echo "$name" | sed -E 's/^_+|_+$//g')"
|
||
printf '%s' "$name"
|
||
}
|
||
|
||
export -f sanitize
|
||
|
||
# depth-first traversal so directories are renamed after their contents
|
||
find . -depth -print0 | while IFS= read -r -d '' src; do
|
||
dir=$(dirname "$src")
|
||
base=$(basename "$src")
|
||
dst=$(sanitize "$base")
|
||
|
||
if [[ "$base" != "$dst" ]]; then
|
||
target="$dir/$dst"
|
||
if [[ -e "$target" ]]; then
|
||
# If the only difference is case (macOS/APFS), allow it:
|
||
if [[ "${src,,}" = "${target,,}" ]]; then
|
||
mv -v "$src" "$target"
|
||
else
|
||
echo "Skipping: '$src' → '$target' (target exists)"
|
||
fi
|
||
else
|
||
mv -v "$src" "$target"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
IFS=$SAVEIFS
|
||
|