Batch rename files replacing non-alphanumeric characters with underscore

I would like to recursively rename several files located in all subdirectories of a parent folder. I would like to remove spaces and other non-alphanumeric characters and replace them with an underscore.

I have looked at several command such as tr, for f in *, and rename but I'm still having struggles trying to accomplish this recursively.

How can I go about doing such a rename recursively?


When you're talking about "recursively" then find comes into play.

cd /path/to/parent_folder
find . -depth -exec do_stuff_here +

I'm using -depth so that files in a subdirectory get renamed before the subdirectory itself,
and -exec program {} + so that the program receives many filenames at once, reducing startup overhead.

Since there's no builtin rename utility that ships with the mac, I'll wrote do_stuff_here as a bash script

bash -c '
  for file; do
    tail=${file##*/}                         # part after the last slash
    alnum_only=${tail//[^[:alnum:]]/_}       # replace characters
    mv -v "$file" "${file%/*}/$alnum_only"
  done
' sh file ...

That trailing "sh" is required for bash -c '...' one-liners -- the first argument is taken as $0 and the rest of the arguments are $1 etc

Putting it all together:

find . -depth -exec bash -c '
  for file; do
    tail=${file##*/}
    alnum_only=${tail//[^[:alnum:]]/_}
    mv -v "$file" "${file%/*}/$alnum_only"
  done
' sh {} +

Other notes:

  • this solution does not take care of file extensions: file.txt will turn into file_txt.

    • if this is a problem, change ${tail//[^[:alnum:]]/_} to ${tail//[^[:alnum:].]/_} -- adding a dot to the characters to keep.
  • Have you thought about what should happen if two different files_with_special_chars both map to the same destination file?

    ./subdir/file_1
    ./subdir/file-1
    

    With my solution, only the contents of the last file will remain.