/ Documentation / Coding style guidelines - Shell script
en

Coding style guidelines: Shell script

Inquisitor is a fairly complex system written in multiple languages, so all developers MUST use the same set of coding style standards to avoid unnecessary arguments and fights.

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Standards compliance

We aim to be portable among POSIX shells and thus we SHOULD use only features specified in POSIX shell standard. The common way to check for standards compliance is:

  • Usage of /bin/dash – Debian Almquist Shell
  • Usage of following lints to check for bashisms and other unportable constructions:

General formatting style

We prefer K&R-inspired general formatting style, with standard C-like exception for functions, as they can’t be nested in each other. This example gives an overview of general pattern:

some_function()
{
    local i
    local count=0
    while [ $# -gt 0 ]; do
        case "$1" in
        opt1) OPT1=1 ;;
        opt2) OPT2=1; count=$(($count + 1)) ;;
        longopt)
            do_something
            and_something_more --with "$OPT2" 
            if [ "$OPT2" = 1 ]; then
                do_something
                and_something_more
            else
                OPT2=1
            fi
            ;;
        list)
            for i in $LIST; do
                echo $i
            done
            ;;
        esac
        shift
    done
}

Indentation

Indentation MUST be always 1 tab ("\t"). One can use whatever (s)he likes to show tabs in his/her favorite text editor: 8 spaces per tab, 4 spaces, special symbols, proportional font, etc.

Indentation is REQUIRED for bodies of the following constructions on distinct lines:

  • functions
  • if
  • while
  • for

Choices of case MUST NOT be indented, as shown in example above.

Line length

One SHOULD use lines of 72 characters long, but use common sense if in doubt. It’s common to use longer lines if it helps, not hurts readability.

Continuation of long lines MUST be indented with one extra level of indentation. starting with second line. One SHOULD use this technique, for example, to group all options of a long command line, one option per line of code:

./configure \
    --with-libs=$LIBS \
    --with-feature=$FEATURE \
    --enable-debug \
    --disable-optimizations

In case when it’s possible to omit \, one SHOULD omit it:

[ -n "$VAR1" ] ||
    do_something --with=argument ||
    last_possible_fallback
cat some-file |
    grep something |
    sed 's/word1/word2/' |
    sort | uniq -c | sort -rn

Comments

All comments SHOULD be on the same level of indentation as the code it refers to. All comments must start with # (i.e. a hash symbol and a space):

# Comment

One SHOULD NOT use inline comments. All comments that refer to a specific line should go above that line.

For comments that mark up logical sections of a program, one SHOULD use the following layout:


# ======================================================================
# Header text for the following section
# ======================================================================

That is: line full of = up to maximum characters allowed, header text describing following section, line full of =. Whole header SHOULD be delimited with blank line before and after it.

Functions

Functions MUST be declared without optional function keyword:

monitoring_start()
{
    ...
}

Globally useful functions should be declared in client/lib/main/functions or similar file. All these functions should have a descriptive comment in ShellDoc format.

Function names SHOULD be descriptive and SHOULD be generated as class_method, if possible: monitoring_start, test_promise_time, memory_amount, etc.

Functions SHOULD NOT be more than a screen (~24-30 lines) long and SHOULD NOT use more than 5-6 levels of indentation. If algorithm is very complex, try splitting it into more functions.

Functions SHOULD return:

  • Boolean results – using exit status: 0 = true, 1 = false, other values for different failure codes. Such check functions SHOULD be named with prefix like is_ or has_ that clearly depicts that it’s a check.
  • All other results (strings, numbers, etc) – using stdout. Such functions should be executed using command substitution syntax.

Variables

Local variables MUST be declared local in a function and MUST be in lower_case. If a variable needs to be initialized, initializion SHOULD be combined with declaration:

local count=0
local i

Global variables MUST be in UPPER_CASE. No extra declarations required.

It is RECOMMENDED to use global variables, unless dealing with library functions that SHOULD avoid messing up global variables and thus SHOULD use local variables.

When accessing the variables, one SHOULD NOT use extra braces around variable name (${VAR}) if it’s possible to do so: $VAR. One SHOULD use double quotes " to access the variables that could contains spaces (especially file/path names):

cp "$SRC" "$DEST" 

Conditions

All test invocations MUST use [ expression ] syntax, not test call:

[ "$answer" = 42 ] || echo fail
if [ "$answer" != 42 ]; then
    echo fail
    exit 1
fi

For “and” and “or” logic operators, one SHOULD use shell operators (i.e. && and ||), and not test operations (-a and -o):

[ "$answer" = 42 ] && [ -z "$question" ]

Note that most of our scripts are executed with -e option (exit immediately after command fail).

Arithmetic expansion

One MUST use $((...)) for arithmetic expansion, and one MUST reference variables inside this block as $VAR:

x=$(($x + 2))

One MUST delimit all arithmetic operators with spaces before and after an operator.

One SHOULD be aware of limitation of shell arithmetic, in particular that it can operate only on integers and on some platforms these integers are only 32-bit signed int: so, for example, it’s a bad idea to use shell arithmetics to make calculations with values like number of bytes on a modern HDD, etc.

Command substitution

One SHOULD use `command` syntax for command substitution. Usage of $(command) syntax is permitted for nested command substitutions, but generally, nested command substitutions are discouraged: one SHOULD use a function, variable or temporary file in this case.

Pipelining

One MUST use the following rules for pipelining commands and filenames:

  • | pipe character MUST be delimited with spaces both before and after it:
grep something from-file | sed 's/1/2/' | sort -u | wc -l
  • <, > and 2> redirection operators MUST be delimited with space before, but not after:
sed 's/1/2/' <input >output 2>errors

Complex substitutions

Complex string substitutions, such as:

  • ${parameter%word}
  • ${parameter%%word}
  • ${parameter#word}
  • ${parameter##word}
  • ${#parameter}

SHOULD NOT be used – one SHOULD use calls to external tools, such as sed, cut, wc or scripting languages.

Sourcing

Sourcing (inclusion of another script file to run in current shell interpreter) MUST be done using . builtin, not by source.

Complex algorithms

Shell scripting is fairly limited, so it’s generally agreed upon that it’s best to use shell what it’s designed for: to call other programs and track the results. If solving a particular task with shell script seem to be too awkward, one SHOULD use other scripting languages.

Inquisitor-specific style notes

See additional style guidelines, specific for various kinds of Inquisitor scripts:

Links to other shell script coding style guidelines

  • http://lug.fh-swf.de/vim/vim-bash/StyleGuideShell.en.pdf
  • http://www.shelldorado.com/goodcoding/
  • http://hub.opensolaris.org/bin/view/Community+Group+on/shellstyle