Skip to content

Instantly share code, notes, and snippets.

@Roy-Orbison
Created January 22, 2024 07:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Roy-Orbison/be10716f4522c44ae1bf5d6e302915f0 to your computer and use it in GitHub Desktop.
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).
#!/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 ))
@Roy-Orbison
Copy link
Author

  1. Download
  2. Make the script executable:
    chmod +x dropbox-filenames-fix
    
  3. Run script to provide help:
    ./dropbox-filenames-fix
    
  4. Typical usage is something like:
    ./dropbox-filenames-fix -r ~/Dropbox\ \(Your\ Organisation\)/
    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment