Tuesday, April 28, 2009

One of the simplest possible C programs is:

int main() {
return 0;
}

Just as functions in programming languages can have return values, programs in Unix also have return values, as the above example illustrates. We're not usually aware of the values returned by the commands we run, but they can be quite useful.

Put the above example into a file called success.c. Then compile it with gcc -o success success.c. Congratulations! You're a C programmer. You can run ./success to try it out, although you'll find that it does very little.

Now make a binary called failure, which returns 1 instead of 0. Run it, and you'll see that it behaves just like success did. At least, as far as you can tell.

Wait, what? Isn't 1 normally associated with truth and success, while 0 is associated with failure? Well, yes. But here's the thing: the designers of Unix figured there's only one kind of success, but many ways to fail. So they inverted the normal convention just in this case, defining 0 to be success, and nonzero values to indicate failure. Don't let that confuse you too much; mostly, it'll work the way you expect.

There are a couple of ways to make use of exit codes from the shell. The simplest are && and ||. I used those for years before I bothered to learn about if statements and for loops in shell scripting. They work like this:

./success && echo foo
./success || echo foo
./failure && echo foo
./failure || echo foo

They're called || and && because in C, there are "logical OR" and "logical AND" operators that let you say things like:

if (gun_unloaded && safety_engaged) { printf("Safe to put away.\n"); }
// this would be read "if gun_unloaded AND safety_engaged..."

or

if (gun_loaded || safety_off) { printf("Gun isn't safe yet.\n"); }
// this would be read "if gun_loaded OR safety_off..."

That's the normal way to use them, but it really doesn't explain what happens in the shell. You need one more factoid: somebody realized that the compiler can save a few instructions if it treats && and || as "short circuit" operators. That is, if gun_unloaded is false, there's no point checking whether safety_engaged is true, since we only proceed if both are true. Likewise, if gun_loaded is true, we know that we'll proceed regardless of whether safety_off is true.

So, still working in C, we can turn those variables into function calls and see for ourselves that the compiler correctly "short circuits" both operators:

#include

bool is_gun_unloaded() { return false; }

bool is_safety_engaged() { return true; } // this never gets called, since is_gun_unloaded returns false

main() {
if ( is_gun_unloaded() && is_safety_engaged() ) { printf("Safe to put away.\n"); }
}

And *that's* how it relates to the shell. When you say `./failure && echo foo`, the shell says "hm, failure failed, so don't bother to echo foo. And when you say `./success && echo foo`, it says "hm, success succeeded, so we need to find out if echo foo will succeed before we know whether the whole statement "./success && echo foo" is true or not. Now, we don't really care whether the whole statement evaluates to true or not. We just want to ensure that "echo foo" only gets run if success succeeded. We call that a "side effect" of evaluating the statement.

So, the way to think about it is that && lets you do the thing on the right only if the left hand side command succeeds, while || lets you do the thing on the right only if the left hand side fails. So:

ls / && echo yes, the root directory still exists
ls /sdfasdfasdflk || echo hm, having trouble finding /sdfasdfasdflk

You can quiet ls down so you don't actually have to listen to what it says, but only what it returns:

ls / >/dev/null 2>/dev/null && echo yup, still there
ls /bleh >/dev/null 2>/dev/null || echo nope, no /bleh either

But wait, there's more. You can also use the special varilable $? in bash to give you the actual numerical return value of the last command run:

ls /
echo $? # remember, 0 means success
ls /bleh
echo $?

Likewise, when you run a shell script, you can return the integer of your choice (modulo 256) using exit:

$ bash # start a subshell
$ exit 3 # leave the subshell
$ echo $? # the second copy of bash just terminated, so tell us its return value

This can be a useful way for your scripts to tell the scripts that called them whether they failed, and what kind of failures they had.

Challenge:

- use ping -w 1 -c 1 google.com to see if google is up. But don't show the output of ping, just print out a nice message saying so.
- write a script that grabs 1MB of data from /dev/urandom at a time and tells you if the word "foo" appears in it. Stop once you find one and indicate how many tries it took.

Make things smaller

To make the images for a GUI recently, I used gimp to do a screen capture of a Google maps picture of the earth (under File... Acquire). But the images I made at first made the GUI too big, so I needed to make them all 1/2 their linear dimension.

1. Use mkdir -p small/big to create small and its subdirectory big in one swell foop.

2. Copy some big images into small/big.

3. cd into small/big. You can iterate over the files like this:

for f in *.jpg ; do echo File $f: ; ls -l $f ; echo and thats all for file $f. ; done

You can also format it more nicely, and bash will let you know when you're inside the loop by giving a > prompt instead of the normal one:

for f in *.jpg
do
echo File $f
ls -l $f
etc...
done

4. You can use convert, part of the ImageMagick package, to convert an image from one type to another and to scale it (and to do many, many other things like cropping and overlaying text):

convert foo.jpg -scale '90%' smaller-foo.png

Challenge: use a for loop to convert each of the images in small/big into an image with the same name in small/.

Extra credit: Use seq to create a for loop that prints out the even numbers from 4 to 20, using sleep to pause 1/4 second between each number. Hint: you'll need to use backticks: ` around the call to seq. Explain in detail what the backticks do for double extra credit.