Wednesday, October 17, 2012

Bash programming tips - part 5

Combining parts 1, 2,3 and 4 we almost have a script. Now to parameter parsing. There are more than one way to parse input parameters. You should check this post for excellent tutorial on parameters parsing. I will just discus my implementation. Now I will repost the snippet from first part.

while [ $# -ne 0 ]; do
   case -o|--option)
         _TARGET="$1"
         shift
         ;;
      -h|--help)
         usage;
         exit 1
         ;;
      (--) usage; exit 1;;
      (-*) usage; exit 1;;
      (*)
          usage;
          exit 1;;
   esac
done

As shown in code above we are looping trough the script parameters one by one, until there are none left. We are matching the parameter to the expected options (-o for example) and shifting the parameter value if option demands it. We display usage if we have unexpected option. With slight modification we can support multiple values for single parameter. I prefer shifting over i.e. getopts because this way I can support long version options (i.e. --help), but that is the matter of preference. This version does not support white spaces in parameter values.

Tuesday, October 16, 2012

Bash programming tips - part 4

In part 1, 2 and 3 we have defined script outline, set up configuration and defined helper functions. Now will discuss error handling. It is amazing that almost none of scripts I have seen have none. It is really simple, just look at code snippet bellow.

#catch script return value
  _ERROR=$?

  #check if commands executed successfully
  if [ $_ERROR -ne 0 ]
  then
    debug "ERROR" "We have an error. Handle it. !!!!"
  fi

With _ERROR=$? we have stored the exit code of the last command executed to variable _ERROR. In Linux world all commands return 0 on success or positive integer on failure.

Bash programming tips - part 3

In part 1 and 2 we have presented the script outline and config section. Now to introduce helper functions.

First is usage

## Usage
function usage() {
clear
cat << USAGE
NAME
       deploy - deploy projects to servers

SYNOPSIS
       ./deploy.sh PROJECT_NAME [-t|--target] TARGET [--tag] [-d|dump]
       ./deploy.sh PROJECT_NAME [-t|--target] [-m|--maintenance]
       ./deploy.sh PROJECT_NAME [-t|--target] [-d|--dump]

DESCRIPTION
       Deploy Drupal projects (${_PROJECTS[@]}) to production.

       Mandatory arguments to long options are mandatory for short options too.

       -t, --target production or hostname for stageing
       -m, --maintenance put destination server to maintance mode
       -d, --dump make backup of project database at target
       -f, --features list of features to revert
       -h, --help display this help and exit
           --tag tag to deploy
           --force force command (i.e. dump on master)

EXAMPLES
       Deploy to production from tag on master branch and make db backup
           /deploy.sh project1 -t production --tag 20120829 -d

       Put production to maintenance mode
          /deploy.sh project1 -t production -m

       Doploy development branch to staging server
         /deploy.sh -t project1 --tag user/branch
AUTHOR
       Written by Author1, Author2, Author3
USAGE
}

Since the example is an extract of deploy script help will usage will return something related to the functionality. This function is quite simple, only cool thing about it is that we listed array elements with ${_PROJECTS[@]}, to display projects available

For input validation we can use something like script bellow since as mentioned in previous post I believe its a good practice to be able to run script without parameters and get usage.

## Validate input
function validate_input() {

   # Exit if there is some invalid arguments
   if [ $# -lt 3 ]; then
      usage
      exit 0
   fi
   #if first argument is not project name show usage
   in_array $1 "${_PROJECTS[@]}" && return 0 || usage; exit;
}

Function above checks if function has at least one parameter, which most be the project name

For debug output to console we can use something like:

#output messages to console
function debug() {

   local _LEVEL=0
   case "$1" in
    ERROR )
        _LEVEL=2 ;;
    INFO )
        _LEVEL=1 ;;
    DEBUG )
        _LEVEL=0 ;;
    esac

   if [ $_DEBUG -gt 0 ] && [ $_LEVEL -ge $_DEBUG_LEVEL ]
   then
      while [ $# -ne 0 ]; do
        echo -n " $1"
        shift
      done
      echo
   fi
   return 0
}

Good place to put these things is separate file in order to keep code clean.

To have full working project you will probably need at least function to become sudo or to catch user input.


Bash programming tips part 2

In part 1 we have introduced the basic script outline. In configuration section we specify variables and configuration options. Example bellow in an extract (short version) of some deployment script configuration, but it is a good example of how complex configuration can be.


# <config>

###
# CONSTANTS
###

#turn debuging on/off
_DEBUG=1
#two levels ERROR=2/INFO=1/DEBUG=0
_DEBUG_LEVEL=0

#custom return codes
_YES=100
_NO=200

_LOCK=/tmp/.lock.deploy

_PROGNAME=$(basename $0)

_MASTER=
_CLUSTER_MODE="cluster"
_SINGLE_MODE="single"

#local environment variables
_LOCALIP=
_LOCALUSER=
_HOSTNAME=

#script specific parameters
_TARGET=
_DUMP=
_MAINTENANCE=
_TAG=

#arrays
declare -a _FEATURES=()
declare -a _INTERFACES=()


#user for deploy
_REMOTE_USER=root

###
# Input dependent arrays
###

#all possible known projects
_PROJECTS=(project1 project2 project3 project4 )

# Project source
declare -A _PROJECT_PATH
#sn
_PROJECT_PATH[${_PROJECTS[0]}]="/var/www/vhosts/project1/www/"
#polet
_PROJECT_PATH[${_PROJECTS[1]}]="/var/www/vhosts/project2/www/"
#deloidom
_PROJECT_PATH[${_PROJECTS[2]}]="/var/www/vhosts/project3/www/"
#pogledi
_PROJECT_PATH[${_PROJECTS[3]}]="/var/www/vhosts/project4/www/"

declare -A _PROJECT_WEB_SERVERS
_PROJECT_WEB_SERVERS[${_PROJECTS[0]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[1]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[2]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[3]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[4]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[5]}]="server1 server2"
_PROJECT_WEB_SERVERS[${_PROJECTS[6]}]="server1 server2"

declare -A _PROJECT_DUMP_LOCATION
_PROJECT_DUMP_LOCATION[${_PROJECTS[0]}]="/tmp"
_PROJECT_DUMP_LOCATION[${_PROJECTS[1]}]="/tmp"
_PROJECT_DUMP_LOCATION[${_PROJECTS[2]}]="/tmp"
_PROJECT_DUMP_LOCATION[${_PROJECTS[3]}]="/tmp"

declare -A _PROJECT_OTHER_SERVERS
_PROJECT_OTHER_SERVERS[${_PROJECTS[0]}]="10.0.0.1 10.0.0.2"
_PROJECT_OTHER_SERVERS[${_PROJECTS[1]}]="10.0.0.1 10.0.0.2"
_PROJECT_OTHER_SERVERS[${_PROJECTS[2]}]="10.0.0.1 10.0.0.2"
_PROJECT_OTHER_SERVERS[${_PROJECTS[3]}]="10.0.0.1 10.0.0.2"

In script above I've used powerful bash feature - array. This allows me to easily group configurations, without having to use long names like PROJECT1_WEB_SERVER1 to keep clarity.




Bash programming tips - part 1

As industry standard all scripts that are not for single use should consist of at least 2 things - usage and configuration.


As a good practice I would expect the script when executed to show help in man format, although most of standard shell commands like ls, pwd execute immediately without requiring any parameters.


To start writing bash script we need editor (Kate, vim i.e.) and bash, which is a part of any modern Linux distribution.


Typical bash script looks something like:

#!/bin/bash

## Includes
#
source scripts/_config.sh

#
# Main
#

## Validate input
validate_input $@

# Settings
while [ $# -ne 0 ]; do
   case -o|--option)
         _TARGET="$1"
         shift
         ;;
      -h|--help)
         usage;
         exit 1
         ;;
      (--) usage; exit 1;;
      (-*) usage; exit 1;;
      (*)
          usage;
          exit 1;;
   esac
done

exit 0

Code above is pretty much self explanatory. Bash allows you to include external files into script and this is a good practice to keep code clean and readable. This is where you should put your configuration (at least if it is not trivial, so use your brains)

At the beginning of main section I usually put some sort of input validation (discussed in detail in later posts) to terminate script immediately if input is invalid. Next is parameter parsing either manually like above or with functions like getopts. Script the continues depending on selected options.