Monday, February 17, 2014

Including (beer) made easy

Include Beer
Recently, I had a question from an expect-lite user about include files. Include files in their simplest form can be thought of an automated way to paste code into your script. And the original intent of an include file was just that. This concept is not a new in computer languages, c has header files, bash can source files, python imports files, etc. But as expect-lite has evolved, I'd like to say improved, and so have the uses of include files.

There are two types of include files,
  • standard or regular
  • fail script
The standard type of include file starts with a tilde followed by a file name.
~my_beer.inc

Using paths and automatic search for include files

There is an automatic search made by expect-lite to look for a simple file name (with no path defined) in the same directory as the expect-lite script, making it easy to use include files without having to worry about their location.

However, if you want to keep your include files in a different location, say a common directory, that is also supported, either by specifying a relative path:
 ~../common/my_beer.inc
Or by an absolute path:
 ~/Users/craig/common/my_beer.inc
This last one may look a little confusing, since bash uses the tilde to indicate the user's home directory. This is not a bash tilde, but an expect-lite tilde, representing "include this file from this location." I have a habit of putting the extent ".inc" on my include files, which just makes them easier to find and manage. But expect-lite doesn't care about extents, you can use anything that works for you.

A Simple Example

So great, now we know how to include something, what does it actually do. In the following example, the include file, my_beer.inc will be:
#my_beer.inc
$beer=beer
; $i bottles of $beer on the wall

And the main script (example 1):
#!/usr/bin/env expect-lite
# Count the beers backwards
$i=99
[ $i > 0
  ~my_beer.inc
  -$i
]

An include script can have any expect-lite lines, and shares global variable space with the main script. In the above example, $i is a counter variable in the while loop, but it also available in the include script which will print out the counter (using the semicolon printable comment feature).

Given this simple example, it would have been just as easy to paste the include file lines into the main script and skipped the include file. And that is a natural way to look at include files. But it would miss some of the power of include files.

Include files like functions

For example, if I modified the main script just a bit, you will see that just as the power of constants can be used on the main script when placing constants on the CLI, it can also be applied to include files.
Editing the main script (example 2):
#!/usr/bin/env expect-lite
# Count the beers backwards
$my_favourite=stout
$i=99
[ $i > 0
  ~my_beer.inc beer=$my_favourite
  -$i
]

By adding a parameter to the include file beer=$my_favourite it is possible change the behaviour of the include file which will now print:
99 bottles of stout on the wall

But wait, you say, include files share variable space of the main script, all I had to do was set $beer to stout in the main program, and this absolutely correct. But there are times when you may not want to change the value of $beer in the main script, passing values to the include script allows you to use include files more like functions than just copy and pasting lines into your script. Since expect-lite does not support real functions, this is a method to use when desiring function-like behaviour.

Fail Script

The other type of include file is that of a fail script. The expect-lite script has no knowledge of if a step failed or passed, this is managed by expect-lite itself. A failed step will either immediately stop the script (default behaviour), or continue to the end  of the script (when *NOFAIL is used).

The fail script is old feature which was designed for over night regression runs, where if a script failed, some clean up may be require before running the next test script. But it has more modern uses as you will see.

The fail script is declared, but no action is taken until script failure occurs (usually something expected was not returned). It is declared anywhere, usually early, in the script with the following:
*~my_fail.inc

There can only be one fail script active at a time, but the name can be changed as often as needed by declaring another fail script which will be in effect until the name is changed, or the end of the script:
*~my_broken_bottle.inc

In the following example, we determine what day it is, and it it isn't the weekend, call a fail script which sets a flag and returns to the main script (example 3):
#!/usr/bin/env expect-lite
# Check if it is the weekend, drink beer
*NOFAIL
*~no_beer.inc
$beer_days=Saturday|Sunday
>date +%A
<$beer_days
# use if statement to print correct beer drinking action
? $weekend==no? ;no beer today :: ;hoist a beer, it is $beer_days 

The include fail script, which will only be run if it is not a weekend looks like:
#no_beer.inc
# set flag if this script runs
$weekend=no

With these two scripts, there is no need to know if the result of the date command fails, since the fail script will set a flag (or variable) $weekend to signal to the if statement in the main script  to send the correct comment (the :: means else in the if statement).

Note the variable $beer_days=Saturday|Sunday includes the regex OR (a vertical line, also called a pipe) which means Saturday OR Sunday. Since the expect line <$beer_days is a regex evaluation, only the one word Saturday or Sunday need be found, and other days of the week will trigger the fail script. More on regex can be found at Dymystify Regex with 7 simple terms (including OR).

Of course the real power of include scripts come from reuse. For example, create a single telnet login include script, and every time a telnet is required, just call the include script. If later, you want to know what other users are on the system at the time of the login, just add a >w command to the telnet login script, and now everywhere that include script is used, it will automagically list the users and what they are doing.

Including Beer

Include scripts are there to make your life easier by simple copy/paste, function calls, fail scripts, and code reuse. Automating beer drinking, include me in.

EDIT: as of version 4.7.0, include file names can not start with equals '=' or larger-than '<' as these are used for fuzzy expect