Even if I master more advanced scripting languages such as Perl or Python, when your task is mainly to run external programs and check return code, those languages make the code a bit harder to read.

I prefer:

#!/bin/ksh
./my-program1 -x || ./myprogram2 -p 1 "$@"

to:

#!/usr/bin/perl
system './my-program1', '-x' || system './my-program2', '-p', 1, @ARGV;

Of course that was a trivial sample and real programs are always more complex. But with a bit of abstraction you can keep your program readable and still powerful with strict error checking.

That was my aim when writing this simple framework that make easier the logging of a sequence of tasks. This is just an experimentation but you may still find it useful.

The aims of the framework are:

  • to transparently handle exhaustive error reporting of every failing command : error codes are tracked
  • to bubble errors up to the top level to report any error code to the calling program
  • to give improved readability of the program and of its output (the log)
  • to be flexible : let the frameork user handle errors as he wants ; the framework should be just some help, not a prison, so let the user interact with the framework

The API uses a few words to border your tasks and monitors tasks result code:

  • BEGIN title: just before a task starts
  • END return_code: just after a task stops
  • STEP title command: launch a task, with BEGIN and END.
  • ABORT return_code: report a fatal error and exit the program, bubling the error code up to the caller

Here is a sample shell program using this framework:

#!/bin/ksh

. ./steps.lib.ksh

function err
{
        echo "Test $1 : échec $2"
        return $2
}

function ok
{
        echo "Test $1 : succès"
        return 0
}


BEGIN "Prog"

  BEGIN Tests

    STEP "T1" ok  T1
    STEP "T2" err T2 1
    STEP "T3" ok  T3

  END

  STEP T4   err   T4 2

END

echo Never reached.>&2

And the output:

$ ./steps-proto.ksh 
Enable log
BEGIN Prog
BEGIN Tests
BEGIN T1
Test T1 : succès
END T1: 0
BEGIN T2
Test T2 : échec 1
END T2: 1
BEGIN T3
Test T3 : succès
END T3: 0
END Tests: 1
BEGIN T4
Test T4 : échec 2
END T4: 2
END Prog: 3
EXIT 3
Disable log
$ echo $? 
3

And here the framework itself. The implementation uses a stack implemented using ksh arrays to keep track of the depth level of the BEGIN/END blocks.

# vim:set ft=sh:

# steps.lib.ksh
# Copyright Olivier Mengué 2007

# Args:
#  $1  Title
function BEGIN
{
        typeset title="$1"
        typeset -i depth=${#step_stack[*]}
        if [[ depth -eq 0 ]] ; then
                echo Enable log
        fi

        # Push the title on the stack
        step_stack[depth]="$title"

        # Status accumulator for sub-steps initialized to 0 (OK)
        step_status[depth]=0

        echo BEGIN "$title"
}


# Args:
#  $1  Status code
function END
{
        typeset -i depth=${#step_stack[*]}-1

        # Default value for status is the OR-accumulation of the sub-steps
        typeset -i status=${1:-${step_status[depth]}}

        typeset title="${step_stack[depth]}"
        echo END "$title: $status"

        unset step_stack[depth]

        # Drops the status of the sub-steps
        unset step_status[depth]

        # If stack is now empty, exit
        if [[ depth -eq 0 ]] ; then
                echo EXIT $status
                echo Disable log
                exit $status
        fi

        # Reports status to parent step
        STEP_STATUS $status
}

# Args:
#  $1  Status code
function ABORT
{
        END "$@"
        while [[ ${#step_stack[*]} -gt 1 ]]
        do
                END
        done
}


# Args:
#  $1  Task name
#  $@  Command
function STEP
{
        BEGIN "$1"
        shift
        "$@"
        END $?
}

# Args:
#  $1  Status code
function STEP_STATUS
{
        typeset -i status=$1
        # Accumulates step status in the current context with OR
        [[ ${#step_status[*]} -gt 0 ]] && let step_status[${#step_status[*]}-1]\|=status
        return status
}

This is just a proof of concept. Once your program uses this kind of framework, you can just now modify the framework to change the output without touching the program anymore. If your program is critical, you probably want to avoid to change it (typos are a big problem in shell scripting). Thanks to a framework and a good test suite for the framework you can improve your program by changing the framework and still avoid any breakage (it depends on the quality of your test suite).

Here are some suggestions:

  • use indentation dependending of the depth level 
  • use different output style when printing the task/result BEGIN/END depending on the depth
  • ...