Skip to content

Instantly share code, notes, and snippets.

@nebez
Forked from leolorenzoluis/Bash best practices
Last active January 9, 2023 19:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nebez/140c8e344739a1b36585c0327da25e26 to your computer and use it in GitHub Desktop.
Save nebez/140c8e344739a1b36585c0327da25e26 to your computer and use it in GitHub Desktop.

Bash best practices and style-guide

Just simple methods to keep the code clean.

Inspired by progrium/bashstyle and Kfir Lavi post.

Quick big rules

  • All code goes in a function
  • Always double quote variables
  • Avoid global variables and declare them as readonly
  • Always have a main() function for runnable scripts
  • Always use set -eo pipefail : fail fast and be aware of exit codes
  • Define functions as myfunc() { ... }, not function myfun {...}
  • Always use [[ instead of [ or test
  • Use $( ... ) instead of backticks
  • Prefer absolute paths and always qualify relative paths with ./.
  • Warnings and errors should go to STDERR, anything parsable should go to STDOUT
  • Use .sh or .bash extension if file is meant to be included or sourced

More specific rules with some example

Global variables

  • Avoid global vars
  • Always UPPER_CASE naming
  • Readonly declaration
  • Globals that can be always use in any program :
readonly PROGNAME=$(basename $0)
readonly PROGDIR=$(readlink -m $(dirname $0))
readonly ARGS="$@"

Other variables

  • All variables should be local (they can only be used in functions)
  • Always lowercase naming
  • Self documenting parameters
fn_example() {
    local explicit_name = $1 ;
    local expName = $1 ;
}
  • Usually use i for loop, so it is very important to declare it as local

Main()

  • Use always a main() function
  • The only global command in the code is : main or main "$@"
  • If script is also usable as library, call it using [[ "$0" == "$BASH_SOURCE" ]] && main "$@"

Everything is a function

  • Only the main() function and global declarations are run globaly
  • Short code portion can be functions
  • Define functions as myfunc() { ... }, not function myfun {...}

Debugging

  • Run with -x flag : bash -x prog.sh
  • Debug just a small section of code using set -x and set +x
  • Printing function name and its arguments echo $FUNCNAME $@

Each line of code does just one thing

  • Break expression with back slash \
  • Use symbols at the start of the indented line
print_dir_if_not_empty() {
    local dir=$1
    is_empty $dir \
        && echo "dir is empty" \
        || echo "dir=$dir"
}

Command line arguments

cmdline() {
    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
}

Unit Testing

  • Very important in higher level languages

  • Use shunit2 for unit testing

  • Good intro to shunit2 : shUnit2 - Bash Testing

  • Another good ressource : Test Driving Shell Scripts

  • The list of current assertions (as of version 2.1.6) :

    • assertEquals [message] expected actual
    • assertSame [message] expected actual
    • assertNotEquals [message] expected actual
    • assertNotSame [message] expected actual
    • assertNull [message] value # used to compare a null in bash which is a zero length string
    • assertNotNull [message] value # used to compare a null in bash which is a zero length string
    • assertTrue [message] condition
    • assertFalse [message] condition
  • The list of current failures (do not use them for value comparisons, use assertions for this) :

    • fail [message]
    • failNotEquals [message] unexpected actual
    • failSame [message] expected actual
    • failNotSame [message] unexpected actual
  • More specific functions :

    • setUp : run automatically before each test
    • tearDown run automatically after each test
    • || startSkipping automatically skip after a test failure (default is to continue)

Usefull links and good references

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment