#! /bin/sh # Recursively rename unportable filenames: # anything with characters other than ASCII letters (a-z, A-Z), # numbers, periods, dashes or underscores in them, # names beginning with dashes, and names longer than 14 characters. # With option -v displays each move, # with -t only displays what would do but does nothing, # with -r "scriptfile" creates an undo script for changing names back. # Non-option arguments should be directory names (if first begins # with - it should be separated from option with -- as usual). # Assumes POSIX.2 compatible shell (ksh88 should be OK). # Tapani Tarvainen, May 2001 # If 1st argument is -X we're within a recursive call if [ "X$1" != "X-X" ] || [ $# -le 2 ] ;then # Normal (1st) call export MOVE=mv export SHOW=: n=0 set -C 2>/dev/null || set -o noclobber until CLEANUP=/tmp/.cleanup$n; >$CLEANUP ;do : $((n+=1)) done 2>/dev/null export CLEANUP scriptfile= while getopts :tvr: opt ;do case $opt in t) MOVE=fakemv; SHOW=show ;; v) SHOW=show ;; r) scriptfile="$OPTARG"; rm -f "$scriptfile" ;; ?) printf "Usage %s: [-t] [-r scriptfile] dir\n" "${0##*/}" exit 1 ;; esac done shift $((OPTIND - 1)) # the only way to retain newlines in filenames is to use -exec # (with Gnu find could use -print0) find "$*" -depth \( -name '*[!-a-zA-Z0-9_.]*' -o -name '-*' \ -o -name '???????????????*' \) -exec "$0" -X "$scriptfile" {} \; 4>>$CLEANUP sh $CLEANUP rm -f $CLEANUP else # We've been called recursively to handle one filepath # (which may contain spaces, newlines &c) - # note this should only be called for names that need to be changed! # Second argument should be reverse-action script file name # (or blank if such is not needed), and environment variable MOVE # should be either "mv" or "fakemv" (default to latter, just in case), # and SHOW should be either "SHOW" or blank (or :). scriptfile=$2 shift 2 badpath="$*" show() { printf 'mv %s %s %s\n' "$1" "$2" "$3" } # Fake mv command for -t option: schedule target for removal fakemv() { printf "rm '" >&4 printf "%s\n" "$3" | sed -e "s/'/'\\\\''/g" -e "\$s/\$/'/" >&4 } badname=${badpath##*/} dir=${badpath%/*} # Build acceptable substitute for filename. # We need to add newline after 'tr' has done it's work # because sed can't handle non-terminated lines (tr can). newname=$(printf "%s" "$badname" | ( tr -c 'a-zA-Z0-9_.-' '[_*]'; echo ) | sed -e 's/^-/_/' -e 's/\(.\{14\}\).*/\1/') # if the result is same as original something's wrong, quit: if [ "$newname" = "$badname" ] ;then # this really should never happen but... printf "${0##*/}: internal error with '%s'!\n" "$badpath" >&2 exit 2 fi set -C 2>/dev/null || set -o noclobber # noclobber for ksh88 # note we can leave this on # see if the name is already used, and append a number if so newcandidate=$newname count=0 # create empty file with the new name to avoid race condition until >"$dir/$newname" ;do # test directory writability here so this won't loop forever even # if it changes after we started... if [ ! -w "$dir" ] ;then printf "${0##*/}: no write permission to directory '$dir'!\n" >&2 exit 3 fi : $((count+=1)) newname=$newcandidate$count # if the result is longer than 14 characters, shorten it while [ ${#newname} -gt 14 ] ;do newcandidate=${newcandidate%?} newname=$newcandidate$count done done 2>/dev/null # all set: rename it ${SHOW:-:} -f "$badpath" "$dir/$newname" if ${MOVE:-fakemv} -f "$badpath" "$dir/$newname" ; then if [ "X$scriptfile" != "X" ] ;then # save reverse command in *the beginning* of scriptfile # noclobber was left on above count=0 until 2>/dev/null >"$scriptfile.tmp.$count" ;do : $((count+=1)) ;done TMPFILE="$scriptfile.tmp.$count" # We enclose the filenames in the reversal script in single quotes. # Single quotes in the names need extra prosessing, # also in source because $dir can be anything at this point. # Command substitution loses extra trailing newlines, # which is a problem for target but not for source because # $newname is known to be well-behaved. source=$(printf "%s\n" "$dir/$newname" | sed "s/'/'\\\\''/g") printf "mv '%s' '" "$source" >>"$TMPFILE" printf "%s\n" "$badpath" | sed -e "s/'/'\\\\''/g" -e "\$s/\$/'/" >>"$TMPFILE" [ -f "$scriptfile" ] && cat "$scriptfile" >> "$TMPFILE" mv "$TMPFILE" "$scriptfile" fi else # Move failed -- # this should only happen if the file disappeared from under us # or some permissions got in our way. printf "${0##*/}: rename failed for '%s'!\n" "$badpath" >&2 exit 4 fi fi #################################The End#################################