Friday, January 06, 2006

Using the Return Value

Functions, like programs, succeed or fail. That's what $? is about. (At the shell level, $? tells whether the last command succeeded or failed. Here, as far as the shell is concerned, the function is just like a command.)

This means that if you have a function to check out and build code
get-N-make() {
svn co $*
(
cd $_
make
)
} &> /dev/null
You can call it like this:

get-N-make -Dyesterday mycode ||
die "something in the checkout or build failed"
(Notice that you can redirect the output of a function as part of its definition.

Thursday, January 05, 2006

The Return of the Value

Okay, so the only way to return a value from a function is with a side effect. What the heck do you do to return values?

One option is to have the function create real output and capture it.
#!/bin/bash

date_diff() {
t2=$(date -d "$1" +%s)
t1=$(date -d "$2" +%s)
echo $((t1-t2))
}

secs=$(date_diff "Dec 25" "now")
echo "$secs more shopping seconds until Christmas"
This works, but costs the execution of a subshell each time you call the function. A more efficient approach is to capture output in a global, and use a naming convention to keep from accidentally stepping on a some other variable with the same name.

I've seen a handful, but I like this:
date_diff() {
t2=$(date -d "$1" +%s)
t1=$(date -d "$2" +%s)
_date_diff=$((t1-t2))
}

date_diff
echo "$_date_diff more shopping seconds until Christmas"
Try using the name of the function, preceded by an underscore, to hold the return value of the latest call.

Wednesday, January 04, 2006

Functions Don't Return Values

Functions really don't return values. Really. That's hard to get used to. The shell does have a return() call, but it doesn't do what you'd guess.
#!/bin/bash

jo_mamma() {
return 69
}

fezmo=jo_mamma
echo fezmo is $fezmo

# No? Well, what if we reason by analogy and use $() to call the function?

fezmo=$(jo_mamma)
echo fezmo is $fezmo

# Still no. But look at this:

jo_mamma
echo after call, status is $?
return() returns from a function call and sets the predefined variable, $? -- the "exit status."

Even this isn't as useful as you'd hope. Substitute a string for the "69" in the example to see what happens.

Tuesday, January 03, 2006

Functions Can Set Globals

The title says it all. Shell variables are global by default.
#!/bin/bash

fezmo=6
jo_mamma() {
fezmo=9
}

echo before call fezmo is $fezmo
jo_mamma
echo after call fezmo is $fezmo
That means that you have to keep track of the names of every variable you set in a function. Bummer. Shell scripts aren't typically very long, so this usually isn't a big deal. Still, using local inside functions is a good habit to get into.

#!/bin/bash

fezmo=6
jo_mamma() {
local fezmo=9
}

echo before call fezmo is $fezmo
jo_mamma
echo after call fezmo is $fezmo
(Don't just read these examples and nod your head. Use snarf-N-barf to try them.)

Monday, January 02, 2006

Design by Analogy

Here's a function that's in almost every shell script I write:
die() {
echo $* >&2 ; exit -1;
}
The syntax is straightforward and obvious. $* is the list of
arguments passed to die() by the caller. The call

die "usage: foobar mumble frabitz"
spits a usage message to stderr and then ends the program.

Why call it "die()"? Because that's what the corresponding Perl
function is called.

Designing things to mimic one another is a good habit. To link
bar to foo, do you say ln foo bar or
ln bar foo? It's easier to remember once you know that cp,
mv, and ln all use the same argument order. Similarly, once you
know rm -i, you're more likely to guess that cp -i,
mv -i, and ln -i all work, too.

The drawback is that you need to remember that the copy isn't exactly the same
as the original. For example, Perl's die() spits out additional
information unless the argument string ends with a carriage return. In
practice, I don't find this a disadvantage. If you do, you could name yours
s.die() or something.

For example, $* in a function is all the args to a function; in a
shell script, it's all the args to the script. $1, $2, etc.
are the individual args to a script. Guess what $1 is inside a
function.

But don't just guess. Try it. And try writing a warn() function to
mimic Perl's. If you don't know Perl, try writing a shell analogue to some
other useful thing you know from another language.

Sunday, January 01, 2006

Functions

Well, I'm back from the holidays. Hope yours were fun. This week: shell functions.

Macros, subroutines, procedures, functions ..., a programming language needs some way a way to encapsulate repeated actions.

The shell calls 'em functions. An academic might say a function should return a value, while a subroutine works through its side-effects. Unfortunately, shell functions mostly work through their side effects. Oh well.

They're still very useful little things, and you can use them a lot, to good effect.

Friday, December 16, 2005

Grow a Command, Then Execute It

Let's make a copy of all the files under /etc/ that contain references to httpd, the Apache http daemon.

First, a directory to hold the copies:
mkdir /tmp/Apachefiles
Next, we'll construct the commands, on the fly, by growing a pipeline. Follow along with me by executing these in a terminal window. For each step, just recall the previous line and edit it.

find /etc/ # list all the files under /etc.
find /etc/ | xargs grep -l httpd # look for all those files that contain the string httpd
{ find /etc/ | xargs grep -l httpd; } 2>/dev/null # get rid of annoying warnings
# now transform the list into a series of commands
{ find /etc/ | xargs grep -l httpd; } 2>/dev/null |
perl -lane 'print "cp $_ /tmp/Apachefiles/"'
Okay, these look good. (I've left out all the mistakes I made while developing the pipeline, because I know you would never make any. I do, so command history is my friend.)

Now, how about executing it? We could redirect the output into a file, mark the file as executable, and then execute it as a script.

Or we could just add one step to the pipeline, like this:
{ find /etc/ | xargs grep -l httpd; } 2>/dev/null |
perl -lane 'print "cp $_ /tmp/Apachefiles/"' | bash
I frequently grow a set of commands like this, on the fly, then when I have them right, pipe them into a subshell, which will execute them, one by one.