Skip to main content

Creating BASH programs that work. Methods to defend your programs from braking, and keep the code tidy and clean.

[Source](http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming "Permalink to Defensive BASH programming - Say what?")

# Defensive BASH programming

Here is my Katas for creating BASH programs that work. Nothing is new here, but
from my experience pepole like to abuse BASH, forget computer science and create
a [Big ball of mud][1] from their programs. Here I provide methods to defend
your programs from braking, and keep the code tidy and clean.

## Immutable global variables

* Try to keep globals to minimum
* UPPER_CASE naming
* readonly decleration
* Use globals to replace cryptic $0, $1, etc.
* Globals I allways use in my programs:

```bash
readonly PROGNAME=$(basename $0)
readonly PROGDIR=$(readlink -m $(dirname $0))
readonly ARGS="$@"
```

## Everything is local

All variable should be local.

```bash
change_owner_of_file() {
    local filename=$1
    local user=$2
    local group=$3

    chown $user:$group $filename
}
```

```bash
change_owner_of_files() {
    local user=$1; shift
    local group=$1; shift
    local files=$@
    local i

    for i in $files
    do
        chown $user:$group $i
    done
}
```

* self documenting parameters
* Usually for loop use i variable, so it is very important that you declare it
  as local.
* local does not work on global scope.

```bash
kfir@goofy ~ $ local a
bash: local: can only be used in a function
```

## main()

* Help keep all variables local
* Intuitive for functional programming
* The only global command in the code is: main

```bash
main() {
    local files="/tmp/a /tmp/b"
    local i

    for i in $files
    do
        change_owner_of_file kfir users $i
    done
}
main
```

## Everything is a function

* The only code that is running globaly is:
    * Global declerations that are immutable.
    * main
* Keep code clean
* procedures become descriptive

```bash
main() {
    local files=$(ls /tmp | grep pid | grep -v daemon)
}
```

```bash
temporary_files() {
    local dir=$1

    ls $dir
        | grep pid
        | grep -v daemon
}
```

```bash
main() {
    local files=$(temporary_files /tmp)
}
```

* _Second example is much better_. Finding files is the problem of
  `temporary_files()` and not of `main()`'s. This code is also testable, by unit
  testing of temporary_files().
* If you try to test the first example, you will mish mash finding temporary
  files with main algorithm.

```bash
test_temporary_files() {
    local dir=/tmp

    touch $dir/a-pid1232.tmp
    touch $dir/a-pid1232-daemon.tmp

    returns "$dir/a-pid1232.tmp" temporary_files $dir

    touch $dir/b-pid1534.tmp

    returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" temporary_files $dir
}
```

As we see, this test does not concern main().

## Debugging functions

* Run program with `-x` flag:
* debug just a small section of code using set `-x` and set `+x`, which will print
  debug info just for the current code wrapped with set `-x` … set `+x`.

```bash
temporary_files() {
    local dir=$1

    set -x
    ls $dir
        | grep pid
        | grep -v daemon
    set +x
}
```

* Printing function name and its arguments:

```bash
temporary_files() {
    echo $FUNCNAME $@
    local dir=$1

    ls $dir
        | grep pid
        | grep -v daemon
}
```

So calling the function:

will print to the standard output:

## Code clarity

What this code do?

```bash
main() {
    local dir=/tmp

    [[ -z $dir ]]
        && do_something...

    [[ -n $dir ]]
        && do_something...

    [[ -f $dir ]]
        && do_something...

    [[ -d $dir ]]
        && do_something...
}
main
```

Let your code speak:

```bash
is_empty() {
    local var=$1

    [[ -z $var ]]
}

is_not_empty() {
    local var=$1

    [[ -n $var ]]
}

is_file() {
    local file=$1

    [[ -f $file ]]
}

is_dir() {
    local dir=$1

    [[ -d $dir ]]
}

main() {
    local dir=/tmp

    is_empty $dir
        && do_something...

    is_not_empty $dir
        && do_something...

    is_file $dir
        && do_something...

    is_dir $dir
        && do_something...
}
main
```

## Each line does just one thing

* Break expression with back slash ``

For example:

```bash
temporary_files() {
    local dir=$1

    ls $dir | grep pid | grep -v daemon
}
```

Can be written much cleaner:

```bash
temporary_files() {
    local dir=$1

    ls $dir
        | grep pid
        | grep -v daemon
}
```

* Symbols at the start of the line indented

```bash
# Bad example of symbols at the end:
temporary_files() {
    local dir=$1

    ls $dir |
        grep pid |
        grep -v daemon
}
```

Good example where we clearly see the connection between lines and the
connecting rods:

```bash
print_dir_if_not_empty() {
    local dir=$1
    is_empty $dir
        && echo "dir is empty"
        || echo "dir=$dir"
}
```

## Printing usage

Don't do this:

```bash
echo "this prog does:..."
echo "flags:"
echo "-h print help"
```

It should be a function:

```bash
usage() {
    echo "this prog does:..."
    echo "flags:"
    echo "-h print help"
}
```

echo is repeated in each line. For that we have Here Document:

```bash
usage() {
    cat <<- EOF
    usage: $PROGNAME options

    Program deletes files from filesystems to release space.
    It gets config file that define fileystem paths to work on, and whitelist rules to
    keep certain files.

    OPTIONS:
       -c --config              configuration file containing the rules. use --help-config to see the syntax.
       -n --pretend             do not really delete, just how what you are going to do.
       -t --test                run unit test to check the program
       -v --verbose             Verbose. You can specify more then one -v to have more verbose
       -x --debug               debug
       -h --help                show this help
          --help-config         configuration help

    Examples:
       Run all tests:
       $PROGNAME --test all

       Run specific test:
       $PROGNAME --test test_string.sh

       Run:
       $PROGNAME --config /path/to/config/$PROGNAME.conf

       Just show what you are going to do:
       $PROGNAME -vn -c /path/to/config/$PROGNAME.conf
    EOF
}
```

Pay attention that there should be real tab 't' in the start of the line for
each line. In vim you can use this replace command if your tab is 4 spaces:

## Command line arguments

Here is an example to complement the usage function above. I got this code from
[Kirk's blog post - bash shell script to use getopts with gnu style long
positional parameters][2]:

```bash
cmdline() {
    # got this idea from here:
    # http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/
    local arg=
    for arg
    do
        local delim=""
        case "$arg" in
            #translate --gnu-long-options to -g (short options)
            --config)         args="${args}-c ";;
            --pretend)        args="${args}-n ";;
            --test)           args="${args}-t ";;
            --help-config)    usage_config && exit 0;;
            --help)           args="${args}-h ";;
            --verbose)        args="${args}-v ";;
            --debug)          args="${args}-x ";;
            #pass through anything else
            *) [[ "${arg:0:1}" == "-" ]] || delim="""
                args="${args}${delim}${arg}${delim} ";;
        esac
    done

    #Reset the positional parameters to the short options
    eval set -- $args

    while getopts "nvhxt:c:" OPTION
    do
         case $OPTION in
         v)
             readonly VERBOSE=1
             ;;
         h)
             usage
             exit 0
             ;;
         x)
             readonly DEBUG='-x'
             set -x
             ;;
         t)
             RUN_TESTS=$OPTARG
             verbose VINFO "Running tests"
             ;;
         c)
             readonly CONFIG_FILE=$OPTARG
             ;;
         n)
             readonly PRETEND=1
             ;;
        esac
    done

    if [[ $recursive_testing || -z $RUN_TESTS ]]; then
        [[ ! -f $CONFIG_FILE ]]
            && eexit "You must provide --config file"
    fi
    return 0
}
```

You use it like this, using the immutable ARGS variable we defined at the top:

```bash
main() {
    cmdline $ARGS
}
main
```

## Unit Testing

* very important in higher level languages
* Use [shunit2][3] for unit testing

```bash
test_config_line_paths() {
    local s='partition cpm-all, 80-90,'

    returns "/a" "config_line_paths '$s /a, '"
    returns "/a /b/c" "config_line_paths '$s /a:/b/c, '"
    returns "/a /b /c" "config_line_paths '$s   /a  :    /b : /c, '"
}

config_line_paths() {
    local partition_line="$@"

    echo $partition_line
        | csv_column 3
        | delete_spaces
        | column 1
        | colons_to_spaces
}

source /usr/bin/shunit2
```

Here is another example using df command:

```bash
DF=df

mock_df_with_eols() {
    cat <<- EOF
    Filesystem           1K-blocks      Used Available Use% Mounted on
    /very/long/device/path
                         124628916  23063572 100299192  19% /
    EOF
}

test_disk_size() {
    returns 1000 "disk_size /dev/sda1"

    DF=mock_df_with_eols
    returns 124628916 "disk_size /very/long/device/path"
}

df_column() {
    local disk_device=$1
    local column=$2

    $DF $disk_device
        | grep -v 'Use%'
        | tr 'n' ' '
        | awk "{print $$column}"
}

disk_size() {
    local disk_device=$1

    df_column $disk_device 2
}
```

Here I have exception, for testing, I declare DF in the global scope not
readonly. This is because of shunit2 not allowing to change global scope
functions.

[1]: http://en.wikipedia.org/wiki/Big_ball_of_mud
[2]: http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/ "bash shell script to use getopts with gnu style long positional parameters"
[3]: http://code.google.com/p/shunit2/