Introduction to Shell Scripting LG #57

Rate this post

« When the only hammer you have is C++, the whole world looks like a thumb. »
— Keith Hodges

The Myth of Design; the Mystery of Color

At this point in the series, we’re getting pretty close to what I consider the upper limit of basic shell scripting; there are still a few areas I’d like to cover, but most of the issues involved are getting rather, umm, involved. A good example is the `tput’ command that I’ll be covering this month: in order to really understand what’s going on, as opposed to just using it, you’d need to learn all about « termcap/terminfo » controversy (A.K.A. one of the main arguments in the « Why UNIX Sucks » debate) – a deep, involved, ugly issue (for a fairly decent and simple explanation, see Hans de Goede’s fixkeys.tgz, which contains a neat little « HOWTO ». For a more in-depth study, the Keyboard-and-Console-HOWTO is an awesome reference on the subject). I’ll try to make sense despite the confusion, but be warned…

Affunctionately Yours

The concept of functions is not a difficult one, but is certainly very useful: they are simply blocks of code that you can execute under a single label. Unlike a script, they do not spawn a new subshell but execute within the current one. They can be used within a script, or stand-alone.

Let’s see how a function works in a shell script: (text version)

 
#!/bin/bash
#
# "venice_beach" - translates English to beach-bunny

function kewl ()        # Makes everything, like, totally rad, dude!
{
     [ -z $1 ] &&& {
        echo "That was bogus, dude."
        return
     }

     echo "Like, I'm thinkin', dude, gimme a minute..."
     sleep 3
     echo $@', dude!'
     # While the function runs, positional parameters ($1, etc.)
     # refer to those given the function - not the shell script.
}

clear

kewl $(echo "$@"|tr -d "[:punct:]")    # Strip off all punctuation

This, umm, incredibly important script should print the « I’m thinkin’… » line followed by a thoroughly mangled list of parameters:

Odin:~$ venice_beach Right on
Like, I'm thinkin', dude, gimme a minute...
Right on, dude!

Odin:~$ venice_beach Rad.
Like, I'm thinkin', dude, gimme a minute...
Rad, dude!

Odin:~$ venice_beach Dude!
Like, I'm thinkin', dude, gimme a minute...
Dude, dude!

Functions may also be loaded into the environment, and invoked just like shell scripts; we’ll talk about sourcing functions later on. For those of you who use Midnight Commander, check out the « mc () » function described in their man page – it’s a very useful one, and is loaded from « .bashrc ».

Important item: functions are created as « function pour_the_beer () { … } » or « pour_the_beer () { … } » (the keyword is optional); they are invoked as « pour_the_beer » (no parentheses). Also, be very careful (as in, _do not_ unless you really mean it) about using an « exit » statement in a function: since you’re running the code in the current shell, this will cause you to exit your current (i.e. the « login ») shell! Exiting a shell script this way can produce some very ugly results, like a `hung’ shell that has to be killed from another VT (yep, I’ve experimented). The statement that will terminate a function without killing the shell is « return ».

Single, Free, and Easy

Everything we’ve discussed in this series so far has a common underlying assumption: that the script you’re writing is going to be saved and re-used. For most scripts, that’s what you’d want – but what if you have a situation where you need the structure of a script, but you’re only going to use it once (i.e., don’t need or want to create a file)? The answer is – Just Do It:

Odin:~$ (
> echo
> [ $blood_caffeine_concentration -lt 5ppm ] &&& {
> echo $LOW_CAFFEINE_ERROR
> while [ $coffee_cup != "full" ]
> do
> brew ttyS2 # Enable coffeepot via /dev/ttyS2
> echo "Drip..."
> sleep 1m
> done
> echo
>
> while [ $coffee_cup != "empty" ]
> do
> sip_slowly # Coffee ingestion binary, from coffee_1.6.12-4.tgz
> done
> }
>
> echo "Aaaahhh!"
> echo
> )
Coffee Not Found: Operator Halted!
Drip...
Drip...
Drip...

Aaaahhh!

Odin:~$

Typing a `(‘ character tells « bash » that you’d like to spawn a subshell and execute, within that subshell, the code that follows – and this is what a shell script does. The ending character, `)’, obviously tells the subshell to ‘close and execute’. For an equivalent of a function (i.e., code executed within the current shell), the delimiters are `{‘ and `}’.

Of course, something like a simple loop or a single ‘if’ statement doesn’t even require that:

Odin:~$ for fname in *.c
> do
> echo $fname
> cc $fname -o $(basename $fname .c)
> done

« bash » is smart enough to recognize a multi-part command of this type – a handy sort of thing when you have more than a line’s worth of syntax to type (not an uncommon situation in a ‘for’ or a ‘while’ statement). By the way, a cute thing happens when you hit the up-arrow to repeat the last command: « bash » will reproduce everything as a single line – with the appropriate semi-colons added. Clever, those GNU people…

No « hash-bang » (« #!/bin/bash ») is necessary for a one-time script, as it would be at the start of a script file. You know that you’re executing this as a « bash » subshell (at least I _hope_ you’re running « bash » while writing and testing a « bash » script…), whereas with a script file you can never be sure: the user’s choice of shell is a variable, so the « hash-bang » is necessary to make sure that the script uses the correct interpreter.

The Best Laid Plans of Mice and Men

In order to write good shell scripts, you have to learn good programming. Simply knowing the ins and outs of the commands that « bash » will accept is far from all there is – the first step of problem resolution is problem definition, and defining exactly what needs to be done can be far more challenging than writing the actual script.

One of the first scripts I ever wrote, « bkgr » (a random background selector for X), had a problem – I’d call it a « race condition », but that means something different in Unix terminology – that took a long time and a large number of rewrites to resolve. « bkgr » is executed as part of my « .xinitrc »:

...
# start some nice programs
bkgr &
rxvt-xterm -geometry 78x28+0+26 -tn xterm -fn 10x20 -iconic &
coolicon &
icewm

OK, by the book – I background all the processes except the last one, « icewm » (this way, the window manager keeps X « up », and exiting it kills the server). Here was the problem: « bkgr » runs, and « paints » my background image on the root window; fine, so far. Then, « icewm » runs – and paints a greenish-gray background over it (as far as I’ve been able to discover, there’s no way to disable that other than hacking the code).

What to do? I can’t put « bkgr » after « icewm » – the WM has to be last. How about a delay after « bkgr », say 3 seconds… oh, that won’t work: it would simply delay the « icewm » start by 3 seconds. OK, how about this (in « bkgr »):

...
while [ -z "$(ps ax|grep icewm)" ] # Check via 'ps' if "icewm" is up
do
    sleep 1                        # If not, wait, then loop
done
...

That should work, since it’ll delay the actual « root window painting » until after « icewm » is up!

Lire aussi...  Linux Gazette MailBag LG #42

It didn’t work, for three major reasons.

Reason #1: try the above « ps ax|grep » line, from your command line, for any process that you have running; e.g., type

ps ax|grep init

Try it several times. What you will get, randomly, is either one or two lines: just « init », or « init » and the « grep init » as well, where « ps » manages to catch the line that you’re currently executing!

Reason #2: « icewm » starts, takes a second or so to load, and then paints the root window. Worse yet, that initial delay varies – when you start X for the first time after booting, it takes significantly longer than subsequent restarts. « So, » you’d say, « make the delay in the loop a bit longer! » That doesn’t work either – I’ve got two machines, an old laptop and a desktop, and the laptop is horribly slow by comparison; you can’t « size » a delay to one machine and have it work on both… and in my not-so-humble opinion, a script should be universal – you shouldn’t have to « adjust » it for a given machine. At the very least, that kind of tuning should be minimized, and preferably eliminated completely.

One of the things that also caused trouble at this point is that some of my pics are pretty large – e.g., my photos from the Kennedy Space Center – and take several seconds to load. The overall effect was to allow large pics to work with « bkgr », whereas the smaller ones got overpainted – and trying to stretch the delay resulted in a significant built-in slowdown in the X startup process, an untenable situation.

Reason #3: « bkgr » was supposed to be a random background selector as well as a startup background selector – meaning that if I didn’t like the original background, I’d just run it again to get another one. A built-in delay any longer than a second or so, given that a pic takes time to paint anyway, was not acceptable.

What a mess. What was needed was a conditional delay that would keep running as long as « icewm » wasn’t up, then a fixed delay that would cover the interval between the « icewm » startup and the « root window painting ». The first thing I tried was creating a reliable `detector’ for « icewm »:

...
delay=0
X="$(ps ax)"

while [ $(echo $X|grep -c icewm) -lt 1 ]
do
   [ $delay -eq 0 ] && (delay=1; sleep 3)
   [ $delay -eq 1 ] && sleep 1
   X="$(ps ax)"
done
...

‘$X’ gets set to the value of « $(ps ax) », a long string listing all the running processes which we check for the presence of « icewm » as the loop condition. The thing that makes all the difference here is that « ps ax » and « grep » are not running at the same time: one runs inside (and just before) the loop, the other is done as part of the loop test (a nifty little hack, and well worth remembering). This registers a count of only one « icewm » if it is running, and none if it is not. Unfortunately, due to the finicky timing – specifically the difference in the delays between an initial X startup and repeated ones – this wasn’t quite good enough. Lots of experimentation later, here’s a version that works:

...
delay=0
until [ ! $(xv -root -quit /usr/share/Eterm/tiny.gif) ]
do
    delay=1
    sleep 1
done
[ delay -eq 1 ] && sleep 3
...

What I’m doing here is loading a 1×1-pixel image and checking to see if « xv » has managed to do so successfully; if it has not, I continue looping. Once it has – and this only means that X has reached a point where it will accept those directives from a program – I stick in a 3 second delay (but only if we’ve done the loop; if « icewm » is already up, no delay is necessary or wanted). This seems to work very well no matter what the « startup count » is. Running it this way, I have not had a single image « overpainted », or a delay of longer than a second or so. I was a bit concerned about the effect of all those « xv »s running one after another, but timing the X startup with and without « bkgr » put that to rest: I found no measurable difference (as a guess, when « xv » exits with an error code it probably doesn’t take much in a way of resources.)

Note that the resulting script is only slightly longer than the original – what took all this time was not writing some huge, complex magical fix but understanding the problem and defining the solution… even though it was a strange one.

There are a number of programming errors to watch out for: « race conditions » (a security concern, not just a time conflict), the `banana problem’, the `fencepost/Obi-Wan error’… (Yes, they do have interesting names; a story behind each one.) Reading up on a bit of programming theory would benefit anyone who’s learning to write shell scripts; if nothing else, you won’t be repeating someone else’s mistakes. My favorite reference is an ancient « C » manual, long out of print, but there are many fine reference texts available on the net; take a peek. « Canned » solutions for standard programming errors do exist, tend to be language-independent, and are very good things to have in your mental toolbox.

Coloring Fun with Dick and Jane

One of the things that I used to do, way back when in the days of BBSs and the ASCII art that went with them, is create flashy opening screens that moved and bleeped and blinked and did all sorts of things – without any graphics programming or anything more complicated than those ASCII codes and ANSI escape sequences (they could get complicated enough, thank you very much), since all of this ran on pure text terminals. Linux, thanks to the absolutely stunning results of work done by Jan Hubicka and his friends (if you have not seen the « bb » demo of « aalib », you’re missing out on a serious acid trip. As far as I know, the authorities have not yet caught on, and it’s still legal), has far outstripped everything even the fanciest ASCII artist could come up with back then (« Quake » and fractal generators on text-only terminals, as two examples).

What does this have to do with us, since we’re not doing any « aalib »-based programming? Well, there are times when you want to create a nice-looking menu, say one you’ll be using every day – and if you’re working with text, you’ll need some specialized tools:

1) Cursor manipulation. The ability to position it is a must; being able to turn it on and off, and saving and restoring the position are nice to have.

2) Text attribute control. Bold, underline, blinking, reverse – these are all useful in menu creation.

3) Color. Let’s face it: plain old B&W gets boring after a bit, and even something as simple as a text menu can benefit from a touch of spiffing up.

So, let’s start with a simple menu: (text version)

#!/bin/bash
#
# "ho-hum" - a text-mode menu

clear

while [ 1 ]         # Loop `forever'
do
# We're going to do some `display formatting' to lay out the text;
# a `here-document', using "cat", will do the job for us.

cat