29 May 2015
Simple Shell Functions
One of the steps along the journey from writing the occasional shell script to being a proficient shell scripter, is the creation of custom functions to create your own toolset. Here is a small but useful example of such a thing. If you want to run a set of tests (on how a system has been configured, or how well a service is running, or anything at all) then an info()
function can be very useful to standardize the way in which this is reported.
This simple script runs a set of tests, and produces a formatted output to different files - pass.log.TIMESTAMP and fail.log.TIMESTAMP. Anything not clearly a pass or a fail is logged to unknown.log.TIMESTAMP.
It starts off by defining $TIMESTAMP
once, then defines the PASS, FAIL and UNKNOWN log filenames accordingly. It then defines the info()
function.
First off, info()
grabs the first argument passed to it, and stores it in a local variable called $STATUS
. Declaring $STATUS
as local
ensures that it doesn't interfere with any variable called $STATUS
elsewhere in your script, and allows you to save the value. The next command, shift
, shifts the arguments up by one, so the status is lost from $1
(that's why we saved it in $STATUS
), and $@
is all arguments left.
Then the function goes into a pretty straight-forward case
construct. If $STATUS
is one of the "good" words we are looking for, then the test has passed. This will be written to the output, as well as appended (via tee -a
) to the $PASS_LOG
file. Note that if $STATUS
is zero, this pattern is matched, and no other case
options are considered.
This means that any number other than zero (even if it starts with zero) is a failure, as is "fail" or "FAIL". That's what "[0-9]|FAIL|fail)" matches. The second case
deals with failures.
The third and final case is "*"
, which matches anything else. This will be logged to the $UNKNOWN_LOG
file.
After that, it's just a question of writing your tests, then calling the info()
script with its status and a comment about what you were testing.
#!/bin/bash TIMESTAMP=$(date +%d%M%Y.%H%M%S) PASS_LOG=pass.log.${TIMESTAMP} FAIL_LOG=fail.log.${TIMESTAMP} UNKNOWN_LOG=unknown.log.${TIMESTAMP} info() { # Call this function with the status, followed by any comments local STATUS=$1 # First argument is the status shift # Anything else is commentary case "$STATUS" in 0|PASS|pass|ok) echo "OK:${STATUS}:$@" | tee -a $PASS_LOG ;; [0-9]*|FAIL|fail) echo "FAIL:${STATUS}:$@" | tee -a $FAIL_LOG ;; *) echo "Unknown status:${STATUS}:$@" | tee -a $UNKNOWN_LOG ;; esac } # Two simple tests: "true" and "false": /bin/true # always returns zero (i.e., success) info $? test1 passed /bin/false # always returns non-zero info 1 test2 failed # Your /etc/hosts is likely to contain "127": grep 127 /etc/hosts > /dev/null 2>&1 info $? Searching for 127 in /etc/hosts file # You can call info() with all sorts of results: info pass test4 passed info ok test5 passed info fail test6 failed info 0123 test 7 failed because it began with zero but is not zero info FAIL test8 failed info hello test9 gave a wrong status echo echo "RESULTS: " [ -f $FAIL ] && echo "Some tests failed: See \"$FAIL_LOG\"" [ -f $UNKNOWN_LOG ] && echo "Some tests produced unexpected results: See \"$UNKNOWN_LOG\""
Simple functions like this, as pointless as they might seem at one level (it doesn't achieve anything much in itself), are a significant part of becoming an efficient and effective shell scripter. Without functions at all, shell scripts are just a sequence of statements, possibly with some if / then / else
logic. By creating your own functions which define how the rest of the script works, you are configuring your environment to suit the task at hand. No more worrying about which log file to write to, or how pass/test comparisons are made - if any of those details need to be changed, you can do it in one place and be sure that your main code is calling the info()
function in a standardized way, just like any other API.
Invest in your career. Buy my Shell Scripting Tutorial today: