Tuesday, March 2, 2010

Better Safe Than Sorry: Writing Code that Writes Safer Code

I write code that writes code. A lot. On the command line. It's safer.

Hal Pomeranz and co-conspirators have another fine post up about command-line programming. In it, they write a clever loop to rename a list of numbered attachments.

Here's Hal's code:

$ cat id-to-filename.txt | while read id file; do mv attachment.$id "$file"; done
(His input file is a two-column list, like this:
$ cat id-to-filename.txt
...
43567 sekrit plans.doc
44211 pizza-costs.xls
...
And, actually, Hal takes the list from stdin, with a less-than sign. Blogger whines and eats my posts when I use those -- it thinks I'm opening an unclosed HTML tag. What a pain.)

The quotes are there because without them the code tries to do this:

mv attachment.43567 sekrit plans.doc
which gets the mysterious message back
mv: target `plans.doc' is not a directory
$
Uh-oh.

When this happens, I usually don't know what the message means. Figuring it out eats time. Plus, with my luck, some files have been moved but others haven't. Recovering from that eats even more time.

Here's what I type instead:
  • First step: I write code that says what I'd like to do.
$ cat id-to-filename.txt | while read id file; do echo "mv attachment.$id $file"; done

...
mv attachment.43567 sekrit plans.doc
mv attachment.44211 pizza-costs.xls
...

Often, when I do this, I scan the output, notice something's going to go wrong, and fix it.

"Oh. Oops. I need quotes. I'm an idiot."

Note that no files were moved; my code's only echoing commands.
  • Next step: I recall my command-line, with an up-arrow, and add fixes. I keep doing that until the commands I see are the ones I actually want.
$ cat id-to-filename.txt | while read id file; do echo "mv attachment.$id '$file' "; done

...
mv attachment.43567 'sekrit plans.doc'
mv attachment.44211 'pizza-costs.xls'
...
Look okay? Yep.
  • Last step: I recall the previous command, one final time, and pipe it to a subshell, which executes the commands my code writes.
$ cat id-to-filename.txt | while read id file; do echo "mv attachment.$id '$file' "; done | bash

$
When I'm nervous about what I'm doing, I even try out the first line by itself, like this:
$ cat id-to-filename.txt | while read id file; do echo "mv attachment.$id '$file' "; done | head -1 | bash

$
I check the result, and if I've done the right thing I go ahead and run the rest.
$ cat id-to-filename.txt | while read id file; do echo "mv attachment.$id '$file' "; done | sed 1d | bash

$
"Never write code on the command line when you can write code that writes code on the command line," I always say.

No comments: