Dry-Wit

Introduction

dry-wit is a framework on top of GNU Bash. It gives two features Bash lacks: a common life-cycle for your scripts, and general-purpose functions.

Bash provides great flexibility, but it comes with a price: most shell scripts you'll see are difficult to read and risky to modify. They don't follow any standard: some would use getopts and some wouldn't, some are well documented and even include man pages, but the majority are pretty the opposite: something that work for certain input values, and nobody (even the author after some time) will find them easy to understand or maintain.

The art of writing shell scripts gets better if you follow some guidelines, declare some meta-information about your script, and focus on what it has to do when the input it requires is valid. That's what dry-wit provides.

Features

Parsing of parameters and flags

It uses hand-made logic, instead of getopts, to simplify support for flags in both short and long formats. Built-in flags provided for free: verbosity (-v, -vv, -vvv), and help (-h|--help).

Error handling

The script must declare all its possible errors as constants. This way, every possible exit condition gets its own error code and description, so that the user get detailed information about the error, and also makes the script more suitable of being invoked from other scripts. The error codes are generated sequentially, and can be retrieved via -vvv -h.

Environment variables

Another requirement for the script is to declare the environment settings it uses, as well as their description and default values. They are displayed to the user as well with -vvv -h.

Dependency check

Before the script starts, all external tools and programs it requires are checked beforehand, as long as the script has declared them.

General-purpose functions

A collection of useful functions are provided, ranging from temporary file/folder creation (and automatic removal) to date logic.

Support for user-triggered interruption

If a ctrl-c is pressed, the script stops its execution and performs a managed shutdown, removing any temporary files or folders.

Contract

All Bash scripts using dry-wit have to implement the following requirements:

First line of the script

In order to use dry-wit, you need it installed somewhere in your PATH. We recommend to place it in /usr/local/bin.

Then, your scripts will need to declare they are written on top of dry-wit:

#!/bin/bash /usr/local/bin/dry-wit

This simple trick makes the shell to execute

/bin/bash /usr/local/bin/dry-wit path-to-your-script [your-script-params]

Script's self-documenting functions

usage

Basically, this function provides the user-oriented information explaining the script purpose and expected parameters.

You can use the following as a template:

function usage() {
  cat <<EOF
$SCRIPT_NAME [-v[v[v]]]
   [-f|--sampleFlag flagValue]
   [-s|--simpleFlag]
   param1
$SCRIPT_NAME [-h|--help]
(c) 2008 YOUR LICENSE

Human-readable script description.
Where:
 - sampleFlag: What sampleFlag is about.
 - simpleFlag: The same for simpleFlag.
 - param1: The meaning of the first parameter.

EOF
}

Such description will be extended with information about the environment settings it uses, and the error details.

checkRequirements

This function is probably impossible to make it simpler: just call checkReq with the required executable name, and the error constant to use in case it's not installed.

A simple example:

function checkRequirements() {
  # sed:
  checkReq sed    SED_NOT_INSTALLED;
  # java:
  checkReq java   JAVA_NOT_INSTALLED;
  # mktemp:
  checkReq mktemp MKTEMP_NOT_INSTALLED;
}

defineEnv

function defineEnv() {
  export JAVA_HOME_DEFAULT="$PWD";
  export JAVA_HOME_DESCRIPTION="The Java installation folder";

  if [ "${JAVA_HOME+1}" != "1" ]; then
    export JAVA_HOME="${JAVA_HOME_DEFAULT}";
  fi

  ENV_VARIABLES=(\
    JAVA_HOME \
  );

  export ENV_VARIABLES;
}

defineErrors

function defineErrors() {
  # Error messages
  export SED_NOT_INSTALLED="sed is not installed";
  export JAVA_NOT_INSTALLED="java is not installed";
  export MKTEMP_NOT_INSTALLED="mktemp is not installed";
  export PARAM1_IS_MANDATORY="param1 is required";
  ERROR_MESSAGES=(\
    SED_NOT_INSTALLED \
    JAVA_NOT_INSTALLED \
    MKTEMP_NOT_INSTALLED \
    PARAM1_IS_MANDATORY \
  );

  export ERROR_MESSAGES;
}

Life-cycle functions

Any script based on dry-wit needs only to write two functions regarding its life-cycle: one to parse and validate its input, and one to perform the actual logic.

checkInput

This function takes care of the input parameters and flags the input expects. It should check for mandatory parameters, and perform any custom validation (checking numeric values, available URLs, correctly-formatted dates, etc). In case of error, it should stop the script with

exitWithErrorCode [ERROR_CONSTANT];

main

Here you'll write the actual code for your script, assuming all environment variables are set and its values are correct. You should divide the flow in well-structured blocks so that the script can be read easily.

Best practices

Logging

dry-wit provides interesting built-in capabilities related to logging. If the terminal supports it, it'll use colors to make the script output look better.

There're some functions you'll use:

  • logTrace -> -vvv
  • logDebug -> -vv
  • logInfo -> -v
  • log

In case of output messages showing the progress of the script (such as checking for free space), you can use the -n flag, and dry-wit will manage the result of the action, printing it using the widely-used green/red coloring scheme for success/failure outcomes, respectively.

Reusable functions

It's recommended to avoid, if possible, assuming which script includes a general-purpose function. Each function should declare explicitly its input parameters as local, and use result or export RESULT for the output variable (depending if its a numeric value or not).

man pages

We're planning to provide built-in support for automatically-generated man pages. We're still discussing the best approach to take.