Wednesday, February 27, 2008

Simplifying File Renaming Using Bash Without Sed

Hello again,

This is a little tip I picked up that I really like. It's not new; just something I never gave any real thought to. It has to do with renaming files (A common enough task) en masse and how to do that in as few keystrokes as possible. But that really puts a limitation on this trick that doesn't exist. It can be used for many other purposes.

I was in the bad habit (Well, it's not a bad habit, really, since it'll work on almost any distro of Linux or Unix) of typing extremely long command lines to change file names. For instance, if I had a directory full of script files that looked like this:

host # ls
script1 script2 script3

and I wanted to copy them all off to files with a .bak extension (In case I screwed up my edits on the originals), I would almost always type something like the following (Note that this command line is a perfectly acceptable way to rename your files if you can't do it the way I'm going to explain here):

host # ls -1 script*|while read name;do newname=`echo $name|sed 's/$/\.bak/'`;cp $name $newname;done

which would work perfectly well, and leave me with:

host #
script1 script1.bak script2 script2.bak script3 script3.bak

In Bash (on Linux or Unix) you can get around this with variable expansion. Like I said, especially in this instance, this may not seem like much, but it does save a lot of typing (and can be used to help you save time in a lot of different ways if you use your imagination :)

So, for purposes of demonstration, we'll put everything back the way it was:

host # rm *.bak
host # ls
script1 script2 script3

Now we're going to look at Bash's expansion operators and see how much easier they can make this whole process. The expansion operators are, basically, curly brackets - which can be nested - containing values. The two most important things to note about them are that:

1. The values must be separated by commas (e.g. {1,2,3})
2. The values can't contain any spaces between themselves and the commas - on either side - unless the values are quoted, like so (there can also be no space between the quotation marks and the comma separators):

Correct = {"1 "," 2 "," 3"}
Who Knows? = {1 , 2 , "3"}

So, now we can do the exact same thing we did above (add the .bak extension to our 3 script files), but do it a lot more cleanly and quickly - not to mention the fact that your input shouldn't bleed over a simple 80 column tty display any more ;)

host # ls -1 script*|while read name;do cp $name{,.bak};done
host # ls
script1 script1.bak script2 script2.bak script3 script3.bak

Here's an even shorter line, but it might cause you problems, given "for x"'s default behavior of reading each variable on a line as the value of x. This wouldn't work very well if you were trying to operate on files with spaces in their names (You can read more on that in our previous post on working with Windows generated file names):

host # for x in `ls -1 script*`;do cp $name{,.bak};done

Not only do Bash's built in variable expansion operators shorten my line, once you get used to using them they make it more easily readable. If you noted that my variable expansion, inside the curly brackets, was missing a value before the first comma, that was intentional. Although you can't have spaces between the values in your expansion set, you can have an empty value (no spaces). This way the first "append" that the variable expansion does is just like doing no append at all.

For example:

host # ls script{1,2}
script1 script2

And now, with a blank first variable (You can put your blank variable anywhere - more than once - but it makes the most sense to use it first in the "cp" command above since I want to copy from the original file name to the new name):

host # ls script{,2}
ls: script: No such file or directory

Like I said, this little trick (well, technically not a trick, since it's built in to Bash ;) has saved me a lot of time and I hope you find it useful as well!


, Mike