Skip to content

Instantly share code, notes, and snippets.

@Roy-Orbison
Created January 22, 2024 07:02
Show Gist options
  • Select an option

  • Save Roy-Orbison/be10716f4522c44ae1bf5d6e302915f0 to your computer and use it in GitHub Desktop.

Select an option

Save Roy-Orbison/be10716f4522c44ae1bf5d6e302915f0 to your computer and use it in GitHub Desktop.

Revisions

  1. Roy-Orbison created this gist Jan 22, 2024.
    254 changes: 254 additions & 0 deletions dropbox-filenames-fix
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,254 @@
    #!/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 ))