Unix: Writing more maintainable shell scripts

If you often find yourself writing scripts from scratch when you know you wrote something similar not all that long ago, maybe it's time to make your scripts a bit more reusable. The usefulness of an old script -- one that you wrote to serve some particular purpose and then more or less forgot about -- can be greatly enhanced by a few techniques that enhance a script's readability and maintainability. Here are some tricks that I've run across. One technique that I've found that helps to keep scripts useful is to define its major variables at the top of the script -- making them easier to find and change. If the values are variables are defined at the top of a script with some quick explanations, you often don't have to read deeply into the script to change how it works.

#!/bin/bash

loop=50
log=/tmp/$0.tmp

Always indent your if, while, until, and case commands so that they form a visual block of code. This greatly facilitates following the logic from beginning to end. Use case statements in lieu of long embedded if-thens whenever you can. They are muxh easier to parse and easier still to insert additional logic into. Use exit codes that help identify why a script terminates prematurely. When you test a script and see it's exited with a return code of 3, you will easily be able to identify the commands that were being run when the script failed. Use the longer spelled out options on commands rather than the shorter versions if it makes the commands more clear (especially for less commonly used commands). A --verbose might make a lot more sense than -v to someone who is not too clear on some command that you're using. Try using the set -e command to make your script exits when a command fails. Instead of having to look back through all of a script's output to find the errors, you stop immediately when there's a problem. You can always then add || true for commands that you want to allow to fail without exiting. Use the set -o nounset (no unset) setting if you want your script to exit when it tries to use an undeclared variable. This can also save you a lot of time when trying to figure out what went wrong, particularly if you haven't looked at the script in several years. Use functions for chunks of code you might otherwise need to repeat, but make sure the name of your function is clear enough that you can easily understand what it's doing. Functions are also very useful if you want to use the same code in numerous scripts. You can create a "library" of your better functions and source it in whatever scripts require their use. If you have a function like this defined in your functions file, you can use it in other scripts by just inserting the line . myFunctions (assuming you call your functions file myFunctions.

#!/bin/bash

function ask () {
    echo -n "$question> "
    read ans
    case $ans in
        [Yy]*) ans="Y";;
        [Nn]*) ans="N";;
        *)     ans="-";;
    esac
}

Try it like this:

#!/bin/bash

. myFunctions

question="Continue?"; ask
if [ $ans != "Y" ]; then
    echo "OK, have a nice day"
    exit
fi

You can add as many functions as you like to your function "libraries" or group your functions in a series of files that represent the various types of data processing you perform.

#!/bin/bash

function ask () {
    echo -n "$question> "
    read ans
    case $ans in
        [Yy]*) ans="Y";;
        [Nn]*) ans="N";;
        *)     ans="-";;
    esac
}

function lower()
{
    local str="$@"
    local output
    output=$(tr '[A-Z]' '[a-z]'<<<"${str}")
    echo $output
}

function is-root {
    if [ "$UID" -ne 0 ]
        then echo "Please run as root"
        exit 1
    fi
}

function greet {
    echo Hello, $USER
}

Give your scripts understandable names and they'll be easier to find and easier to use. I often start my scripts by calling them "tryme" until they've been proven to work pretty well. Then I give them meaningful names like "findProblems" or "genStats" -- something that reminds me of what they're intended to do for me. Also use meaningful names for your variables. $loopcounter is probably better than $lc and the longer names really aren't much of a hardship since you won't be typing them again and again. Use comments, but don't get carried away. If you put in too many comments, probably no one will read them. Only comment major things that are happening in your scripts or commands that are complex. Put your scripts in a reasonable location -- /usr/local/bin or your own bin directory -- and maybe tar them up from time to time so you have a handy backup. I actually prepare documents describing some of the more complex things that I do with scripts. If I start with a huge log file, summarize it in some meaningful way and then pass the results through another script that prepares a report, I want to remember the entire flow -- where the scripts run, how output is exchanged -- especially if multiple systems are involved in the overall process. Printouts are not evil and samples of the data at various stages of processing can be a big help when the process breaks down several years later and you need to track down what might have changed. Some up-front attention to your scripts might give them a longer life span and save you some work in the long run.

Read more of Sandra Henry-Stocker's Unix as a Second Language blog and follow the latest IT news at ITworld, Twitter and Facebook.

Insider: How the basic tech behind the Internet works
Join the discussion
Be the first to comment on this article. Our Commenting Policies