Okay, so I want to batch rename files (and files within subdirectories) in a directory to lowercase, but I also want to undo that at some point. Should I store their real names somewhere so I can go back to the original structure once I am done?

Can this be done in bash?

up vote 3 down vote accepted

yes, quite straightforward

for f in *; do
    mv "$f" "${f,,}"
    printf "mv %q %q\n" "${f,,}" "$f" >> restore.sh
done

run bash restore.sh to restore the former file names.


Recursive: trickier than first appears: I ended up processing the files and directories separately, only renaming the base part of the path.

declare -A restore=([f]=restore_file.sh [d]=restore_dir.sh)
for type in f d; do
    find . -type $type -print0 | while IFS= read -rd '' path; do
        base=$(basename "$path")
        lower="$(dirname "$path")/${base,,}"
        mv "$path" "$lower"
        printf "mv %q %q\n" "$lower" "$path" >> "${restore[$type]}"
    done
done

To restore:

bash restore_dir.sh
bash restore_file.sh
  • Thanks, this is perfect. But is there a way to make it recursive? That is to apply it to subdirectories as well? – CarrKnight Oct 12 '14 at 1:32
  • Are spaces in filenames going to mess things up in the reversal script? – Oli Oct 12 '14 at 2:00
  • Spaces will not be a problem: bash's printf %q will properly escape them. – glenn jackman Oct 12 '14 at 16:05
  • 1
    the recursive version fails at restoring. This is because it stores all subdirectories as lowercase, but once the foldername is restored the mv commands fail because they can't find the lower-case foldername. – CarrKnight Oct 13 '14 at 8:56
  • This is awesome. If only I could up-vote more than once. – jimmiebtlr Aug 4 '15 at 4:39

The rename is simple enough with a little transform with rename. The following is inert (remove the -n flag to make it do things) and it will tell you what it' doing:

rename 'tr/A-Z/a-z/' -vn * 

However, if you want to build that into a reversible script, you have to do a bit more:

rename 'tr/A-Z/a-z/' -vn * | sed 's/ renamed as /#/' | awk -F'#' '{print "mv '\''" $2 "'\'' '\''" $1 "'\''"}' > reverse.sh

If you need to change the list of files going into this (eg recursive), you can either pipe names into rename:

find | rename ...

Or use some Bash options for globbing recursively:

shopt -s globstar
rename ... ** *
  • That's great but I think the problem is that it renames the folder before renaming its content because i keep getting "no such files or directory" for all files in the subdirectories. The subdirectories themselves change names, but the content doesn't – CarrKnight Oct 12 '14 at 2:08
  • One potential risk with your awk script: filenames may contain single quotes. – glenn jackman Oct 12 '14 at 16:06
  • @CarrKnight if using find, use the -depth option. – muru Oct 12 '14 at 16:47
#!/bin/bash

IFS=$'\n'
WORKING_PATH=$(pwd $1)

RESTORE_FILENAME="$WORKING_PATH/restore_case.sh"

echo "#!/bin/bash" > "$RESTORE_FILENAME"
chmod a+x "$RESTORE_FILENAME"


for FILEPATH in $(find . -type f)
do
    FILENAME="${FILEPATH##*/}"
    LOWER_FILE="${FILEPATH::-${#FILENAME}}${FILENAME,,}"

    if [ "$LOWER_FILE" != "$FILEPATH" ]; then
        echo moving "$FILEPATH" to "$LOWER_FILE"
        echo mv \""$LOWER_FILE"\" \""$FILEPATH"\" >> "$RESTORE_FILENAME"_files
        mv "$FILEPATH" "$LOWER_FILE"
    fi
done

touch "$RESTORE_FILENAME"_dirs

for DIRPATH in $(find . -type d -printf "%d%p\n"|sort -nr)
do
    DIRPATH=${DIRPATH:1}
    DIRNAME="${DIRPATH##*/}"
    LOWER_DIRPATH="${DIRPATH::-${#DIRNAME}}${DIRNAME,,}"
    if [ "$LOWER_DIRPATH" != "$DIRPATH" ]; then
        echo moving "$DIRPATH" to "$LOWER_DIRPATH"
        echo -e "mv \""$LOWER_DIRPATH"\" \""$DIRPATH"\"\n$(cat "$RESTORE_FILENAME"_dirs)" > "$RESTORE_FILENAME"_dirs
        mv "$DIRPATH" "$LOWER_DIRPATH"
    fi
done

cat "$RESTORE_FILENAME"_dirs "$RESTORE_FILENAME"_files >> "$RESTORE_FILENAME"
rm "$RESTORE_FILENAME"_dirs "$RESTORE_FILENAME"_files

echo rm '$0' >> "$RESTORE_FILENAME"

longer, a bit dirty, but more precise, also does not do unnecessary mv

  • 1
    You should use printf "%q" like the accepted answer. echo "mv \"...." breaks easily. – muru Jun 27 '17 at 1:36
  • Parsing the output of IFS=$'\n'; find is vulnerable to filenames that contain newlines. It might be better to use find . -type f -print0 | while read -d '' FILEPATH ... or shopt -s globstar; for FILEPATH in **/* .... – wjandrea Jun 27 '17 at 1:49
  • yes, implementation is a bit dirty, but logic is much more precise (which is important for me) also this level of "vulnerability" is suffice for me., i have no time to dig deep into bash scripting now, just done required task for me, and think someone may find it useful (accepted answer does not fit in my case) – sss123next Jun 27 '17 at 18:26

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.