Created
January 22, 2024 07:02
-
-
Save Roy-Orbison/be10716f4522c44ae1bf5d6e302915f0 to your computer and use it in GitHub Desktop.
A bash script to make filenames created on Mac/Linux compatible with Windows for shared Dropbox folders (or similar file syncing services).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
#shopt -s compat32 | |
# illegal on windows | |
re_ctrl='[:cntrl:]' | |
re_ctrl="^([^$re_ctrl]*)[$re_ctrl]+(.*)" | |
re_punct='<>:"\\/|?*' | |
re_punct="^([^$re_punct]*)[$re_punct]+(.*)" | |
re_ts_td='[. ]+$' | |
# here be dragons! | |
re_win='^(CO(N|M[1-9])|PRN|AUX|NUL|LPT[1-9])(\.[^.]+)?$' | |
# not illegal but quite problematic | |
re_ls='^ +(.*)' | |
re_cs=' +(.*)' | |
re_tsf=' ((\.[^.[:space:]]+)+)$' | |
punct_replacement='-' | |
startpoint=() | |
list=0 # only used to check for conflicts | |
rename=0 | |
show_new=0 | |
helpme=0 | |
badopts=0 | |
while getopts ':hnlp:r' opt; do | |
case $opt in | |
h) | |
helpme=1 | |
;; | |
n) | |
show_new=1 | |
;; | |
l) | |
list=1 | |
;; | |
p) | |
punct_replacement="$OPTARG" | |
if [[ "$punct_replacement" =~ $re_ctrl || "$punct_replacement" =~ $re_punct ]]; then | |
badopts=1 | |
>&2 echo -n 'Invalid punctuation replacement character: '; >&2 printf '%q\n' "$punct_replacement" | |
fi | |
;; | |
r) | |
rename=1 | |
;; | |
:) | |
badopts=1 | |
case $OPTARG in | |
p) | |
>&2 echo "You did not specify any punctuation replacement after the '-$OPTARG' option." | |
;; | |
*) | |
>&2 echo "Argument missing from option '-$OPTARG'." | |
;; | |
esac | |
;; | |
\?) | |
badopts=1 | |
>&2 echo "Unknown option '-$OPTARG'." | |
;; | |
esac | |
done | |
if (( OPTIND > 1 )); then | |
shift $(( OPTIND - 1 )) | |
fi | |
case $(( list + rename )) in | |
0) | |
helpme=1 | |
;; | |
1) | |
if [[ $# -eq 0 ]]; then | |
badopts=1 | |
>&2 echo You must specify at least one directory or file as a starting point. | |
>&2 echo Use a dot for the current directory. | |
else | |
startpoint=("$@") | |
for path in "${startpoint[@]}"; do | |
if [[ ! -e "$path" ]]; then | |
badopts=1 | |
>&2 printf '%q ' "$path"; >&2 echo is inaccessible or does not exist. | |
fi | |
done | |
fi | |
;; | |
2) | |
badopts=1 | |
>&2 echo Can either output a list of files, or rename them, not both. | |
;; | |
esac | |
if (( badopts + helpme )); then | |
if (( badopts )); then | |
helpout=2 | |
else | |
helpout=1 | |
fi | |
>&$helpout cat <<-EOT | |
Usage: | |
$0 [ -h ] | |
$0 -l [ -p PUNCT ] [ -n ] START_POINT [ START_POINT ... ] | |
$0 -r [ -p PUNCT ] START_POINT [ START_POINT ... ] | |
Options: | |
-h This help text (the default). | |
-l List problematic files. | |
-n Show suggested new names as well. | |
-r Rename problematic files. | |
-p Override dash character used for replacing illegal punctuation. | |
EOT | |
exit $badopts | |
fi | |
if (( rename )); then | |
>&2 echo Will prompt for renames. Ensure you only rename items BELOW the main Dropbox directory, | |
>&2 echo and do not change your account name directory. Press Ctrl + C to cancel. | |
else | |
>&2 echo Will list problematic filenames. | |
>&2 echo 'To perform renames, add the "-r" option before the starting point(s).' | |
>&2 echo | |
fi | |
suggest () { | |
suggested="$1" | |
local original="$suggested" es=0 | |
# order of replacements is significant | |
while [[ "$suggested" =~ $re_ctrl ]]; do | |
suggested="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" | |
done | |
while [[ "$suggested" =~ $re_punct ]]; do | |
suggested="${BASH_REMATCH[1]}$punct_replacement${BASH_REMATCH[2]}" | |
done | |
if [[ "$suggested" =~ $re_ls ]]; then | |
suggested="${BASH_REMATCH[1]}" | |
fi | |
if [[ "$suggested" =~ $re_ts_td ]]; then | |
before_end=$(( ${#suggested} - ${#BASH_REMATCH[0]} )) | |
suggested="${suggested:0:before_end}" | |
fi | |
while [[ "$suggested" =~ $re_cs ]]; do | |
before_end=$(( ${#suggested} - ${#BASH_REMATCH[0]} )) | |
suggested="${suggested:0:before_end} ${BASH_REMATCH[1]}" | |
done | |
while [[ "$suggested" =~ $re_tsf ]]; do | |
before_end=$(( ${#suggested} - ${#BASH_REMATCH[0]} )) | |
suggested="${suggested:0:before_end}${BASH_REMATCH[1]}" | |
done | |
# "${reserved@Q}" and "${reserved^^}" not avail. | |
reserved="$(printf '%sX\n' "$suggested" | tr '[a-z]' '[A-Z]')" | |
reserved="${reserved%X}" | |
if [[ "$reserved" =~ $re_win ]]; then | |
suggested="_$suggested" | |
es=33 | |
elif [[ -z "$suggested" ]]; then | |
es=22 | |
elif [[ "$suggested" != "$original" ]]; then | |
es=11 | |
fi | |
set +e # don't halt on custom errors | |
return $es | |
} | |
re_skip='[?][[:space:]]*$' | |
skipped=0 | |
while IFS= read -r -d $'\0' path <&3; do | |
if [[ ! -e "$path" && ! -L "$path" ]]; then | |
>&2 echo "'$path' is inaccessible or no longer exists." | |
exit 1 | |
fi | |
parent="$(dirname "$path"; err=$?; echo X; exit $err)" | |
parent="${parent%?X}" | |
current="$(basename "$path"; err=$?; echo X; exit $err)" | |
current="${current%?X}" | |
if [[ "$current" == . || "$current" == .. ]]; then | |
continue | |
fi | |
suggest "$current" | |
suggest_error=$? | |
set -e | |
if ! (( suggest_error )); then | |
# already compliant | |
continue | |
fi | |
if ! (( rename )); then | |
if (( show_new )); then | |
echo "$path"$'\t'"$parent/$suggested" | |
else | |
echo "$path" | |
fi | |
continue | |
fi | |
first_suggested="$suggested" | |
while : ; do | |
>&2 echo | |
if [[ "$parent" == . ]]; then | |
in= | |
else | |
in=" in '$parent'" | |
fi | |
if (( suggest_error == 33 )); then | |
>&2 echo "'$reserved' is a reserved filename on Windows, so can never be used." | |
fi | |
>&2 echo "Confirm or adjust sanitised name for '$current'$in." | |
>&2 echo "To skip past this rename, type a ? at the end." | |
read -r -e -i "$suggested" -p 'New name: ' | |
confirmed="$REPLY" | |
if [[ "$confirmed" =~ $re_skip ]]; then | |
(( skipped++ )) || true | |
>&2 echo Skipping. | |
break | |
fi | |
suggest "$confirmed" | |
suggest_error=$? | |
set -e | |
case $suggest_error in | |
0) | |
path_new="$parent/$suggested" | |
if [[ -e "$path_new" ]]; then | |
>&2 echo Something with that name already exists. | |
else | |
mv "$path" "$path_new" | |
break | |
fi | |
;; | |
22) | |
if [[ "$confirmed" != "$suggested" ]]; then | |
>&2 echo -n 'That name becomes zero length after re-sanitising it. ' | |
fi | |
>&2 echo Names cannot be zero length. | |
suggested="$first_suggested" | |
;; | |
*) | |
>&2 echo That adjusted name still does not comply. | |
;; | |
esac | |
done | |
done 3< <(find -H "${startpoint[@]}" -depth -print0) | |
symlinks_broke=0 | |
if (( rename )); then | |
>&2 echo | |
while IFS= read -r -d $'\0' symlink <&3; do | |
(( symlinks_broke++ )) || true | |
>&2 echo "The target of the symlink '$symlink' no longer exists." | |
done 3< <(find -H "${startpoint[@]}" -depth -xtype l -print0) | |
if (( symlinks_broke )); then | |
>&2 echo | |
fi | |
>&2 echo Done. | |
fi | |
exit $(( ( skipped + symlinks_broke ) > 0 )) |
Author
Roy-Orbison
commented
Jan 22, 2024
- Download
- Make the script executable:
- Run script to provide help:
- Typical usage is something like:
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment