Renaming lots of files

Posted June 2007

I was astounded to find out a couple of days ago that one of my colleagues was not aware of shell parameter expansion. He'd been confronted with the following problem:

Given a directory full of files, write a shell one-liner to rename all the ones ending in .doc such that they now end in .txt.

I almost choked on my drink as I heard suggestions involving Perl. There's no need to complicate things! Here's how to solve the task using parameter expansion (both Bash and Zsh certainly have these):

for f in *.doc ; do mv $f ${f%.doc}.txt ; done

Now I know that my previously mentioned colleague won't need the following explanation (he's already dived into the Bash man page by now, I suspect), but I'm feeling wordy for a change. Here's the above line with white space added for readability:

for f in *.doc ; do
    mv $f ${f%.doc}.txt
done

On the first line we use a shell glob (the *) to select all the files in the current directory that ends in .doc. For each iteration of the for loop, the variable f contains the name of one of those files. The third line just says that we're done with the looping, so all the really interesting bits are happening in the second line.

That line says: rename (on Unix a rename is just a move to a new name. Unix geeks are lazy typists, hence "move" was shortened to "mv") the file named $f (in shell programming we put a $ in front of the variable name when it can be confused with a literal string) to the result of the following expression: ${f%.doc}.txt. Of that ${f%.doc} is the parameter expansion part. From the Zsh man page:

${name%pattern}
${name%%pattern}

If the pattern matches the end of the value of name, then substitute the value of name with the matched portion deleted; otherwise, just substitute the value of name. In the first form, the smallest matching pattern is preferred; in the second form, the largest matching pattern is preferred.

What that means is that if the string in the variable f contains .doc at the end of it (which we know that it does) strip it off. This will be the case for all the files, since our for loop only iterates over files with this ending. The dot over the i is that we simply append .txt.

There are loads of other parameter expansion tricks you can do, including the following, which should feel familiar to Perl people: ${f:gs/foo/bar/}—substitute with the content of the variable f, but with the string "foo" replaced by "bar" everywhere it occurs in f.

Enough from me. Dive into the Bash/Zsh man page already!