How to process files with spaces using bash

The other day I wanted to rename a whole bunch of images with spaces and brackets in them. I had files of the form filename1 (1).JPG, filename1 (2).JPG, ..., filename2 (1).JPG, ...
In case you are wondering, these file came from a windows box where copies of files get saved in the format filename().xyz. I know that from a OS point of view spaces in file names are no problem, but hell are spaces a pita if you have to process the files in a script. So what I wanted to do was to remove the space with a '_', replace the brackets and while on it lowercase the filenames (I really just don't like JPG, I want jpg). For these kind of problems I would normally try a for loop. Here is my first attempt:

for i in `ls -1 *.JPG`; do j=`echo $i | tr -d "[:blank:]" | tr "[:upper:]" "[:lower:]" | tr "(" "_" | tr -d ")"`; mv "$i" "$j"; done

The idea is to process each file name in a loop, transform the filename with a a set of tr calls and then do a final rename of the files using mv. tr takes its input from standard in and writes to standard out, hence the trick of first echoing the current value of the iteration, passing it through several tr calls and then assigning it to a temporary variable. Note the use of the character classes for the transformation via tr.

This one liner works fine for file names without spaces, but as soon as there are spaces in the file names you are getting into trouble. The default field separator of a for loop is the space character which means file names with spaces are causing two iterations of the loop. One solution to the problem is to set the $IFS variable prior to executing the for loop. The value of this variable determines the field separator. However, in this case you also have to remember to reset the variable afterward.

Another approach is to  use a completely different construct which you don't see so often. Let's use find together with a while loop.

find . -name "*.JPG" -print0 | while read -d $'\0' file; do j=`echo $file | tr -d "[:blank:]" | tr "[:upper:]" "[:lower:]" | tr "(" "_" | tr -d ")"`; mv "$file" $j; done

We use find together with the -print0 option to print all file names into one line separated by a ASCII NUL character. In the loop we use read together with the -d option which determines the delimiter for read. The rest is the same as in the for loop solution.

Hope this helps some people, but maybe its just me who gets frustrated with spaces in file names ;-)


Old comments

Hi Hardy,

another great way to proccess filenames having spaces in name is the Unix-command xargs. When using it with options -l -i each filename can be processed in a very simple way.

Greating from Germany!

Thomas (a new toastmaster)
2010-03-14Hardy Ferentschik
ahh, nice one. I've never used any arguments to xargs. I use it a lot, but I never checked for any options. I guess its a good practice to read the manpages for the tools you are using.
P.S. Have fun with Toastmasters :)