This question already has an answer here:

I have multiple files in directories and subdirectories that have special characters not supported by Microsoft file shares and SharePoint. The types of characters run the gamut from tildes to ampersands to who knows what. I have used commands such as:

find . -type f -print | while read file
  file_clean=$( echo ${file} | tr " \~()&'" "_____" )
  mv $file $file_clean

and see that some have suggested using globstar. But I am still not able to find a relatively simple command to remove all special characters, leaving only A-Z, a-z, 0-9 recursively (recursively appears to be the hardest part).

marked as duplicate by Anthon, user79743, cuonglm, cas, jimmij Mar 24 '16 at 5:56

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

  • How about allowing a period for an extension? – Jeff Schaller Mar 23 '16 at 17:10
  • recursion part will need to be different than the actual content manipulation part. If you are hoping to find something to do the both, you need to write and compile the custom code. With whatever is provided by shell, you will use awk/sed for the charter replacement and some form of find command to find all the files you need to process. How you use the pipes to make it the easiest way for you, is taotally up to you – MelBurslan Mar 23 '16 at 17:12
  • consider something along the lines of: file_clean=$(echo "file" | tr -dc '[:alnum:].') – Jeff Schaller Mar 23 '16 at 17:24
  • Thank you, all, for the feedback. I rather feared as much - meaning, I would need a script. – SMDouk Mar 23 '16 at 19:07
  • Yes, Jeff, allowing for a period for the extension.I did find that it was a pretty common problem without a pretty easy solution. Oh well, back to the ol' drawing board. – SMDouk Mar 23 '16 at 19:09

To get all filenames you should rely on null delemitted strings:

find /your/path/ -print0 | while read -r -d $'\0' filename; do
    dn="`dirname "$filename"`"
    bn="`basename "$filename"`"
    bn_clean="`echo "$bn" | tr " \~()&'" "_______"`"
    if [ "$filename" != "$file_clean" ] ; then
        mv -fv "$filename" "$file_clean"
  • I tried this and received back: -bash: -print0: command not found I am new to Unix so am not sure what I did wrong. – SMDouk Mar 24 '16 at 17:51

Careful with this:

perl -pi -e 's/\W//g' $(find . -type f)

You can make backups with something like:

perl -pi.bak -e 's/\W//g' $(find . -type f)
  • 1
    Can't open perl script "s/\W//g": No such file or directory – SMDouk Mar 24 '16 at 17:53
  • Added -e so it runs. – r_2 Mar 25 '16 at 13:30

The easy way is with zsh. Zsh is part of the base OS X installation but needs to be installed through the package manager on most Linux distributions and installed from ports on *BSD. Zsh provides the zmv function which makes many file renaming tasks easy. First run this (or put it in your .zshrc, for interactive use):

autoload zmv

Then you can use either

zmv '(**/)(*)' '$1${2//[^A-Za-z0-9]/_}'

or the equivalent

zmv '**/*' '$f:h${${f:t}//[^A-Za-z0-9]/_}'

The first zmv command renames all files matching **/* (i.e. all files in the current directory and in subdirectories recursively), into files in the same directory ($1) and with the base name transformed to replace every character matching [^A-Za-z0-9] by a _. The parentheses in (**/)(*) cause the directory part of the path (everything up to the last /) to be assigned to $1 and the base name of the file to $2. The second command does the same, but uses $f to refer to the whole original name and the modifiers :h and :t to extract the directory and base parts of the name.

Your script breaks on all kinds of ways because it runs various special characters through their shell handling instead of treating them literally. To understand why, read Why does my shell script choke on whitespace or other special characters?

  • Thank you for taking the time to respond, I will check into this solution. I currently don't have zsh installed or have experience using it. – SMDouk Mar 24 '16 at 17:46
  • This is great, but for some reason, this only goes one folder deep? Is it possible to make this fully recursive? – Etienne Low-Décarie Dec 8 '17 at 8:46
  • @EtienneLow-Décarie The exact code I posted is recursive. Note that it's zmv '(**/)(*)' …, not zmv '(**)/(*)', the second one would only go one level deep due to limitations on the usage of ** inside parentheses. I've added another way to write this command that doesn't run into the parenthesis difficulty. – Gilles Dec 8 '17 at 14:19

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