#!/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 ))