Skip to content
This repository has been archived by the owner on Jan 6, 2023. It is now read-only.

Shell Code Style and Considerations

Murilo Belluzzo edited this page Jun 1, 2019 · 4 revisions

Shell Code Style and considerations

Which Shell to use

Bash is the only shell scripting language permitted.

Indentation

Indent 4 spaces. No tabs.

Use blank lines between blocks to improve readability. Indentation is four spaces. Whatever you do, don't use tabs. For existing files, stay faithful to the existing indentation.

Line Length

The preferable line length is 80 characters, but lines up to 120 characters is acceptable.

Source Files

Lowercase, with underscores to separate words and terminated in '.sh' (optional for executable scripts). A executable must start with #!/usr/bin/env bash followed by Intel's copyright notice, all other must start with Intel's copyright notice.

Example:

#!/usr/bin/env bash
# Copyright (C) 2019 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

Implementation Comments

Comment tricky, non-obvious, interesting or important parts of your code.

Braces

The open parenthesis is always on the same line as the function name. The closing parenthesis is always on a separate line.

For example:

my_func() {  % OK
    ...
}

my_func2()
{  % NOT OK
    ...
}

Conditionals and Loops

Put ; do and ; then on the same line as the while, for or if.

Examples:

if something ; then
    do_stuff
fi

if something ; then
    do_stuff
elif something_else ; then
    do_other_stuff
elif full_moon ; then
    howl
else
    turn_into_a_newt
fi
for myvar in "the first" "the second" "and the third" ; do
    einfo "This is ${myvar}"
done

for (( i = 1 ; i <= 10 ; i++ )) ; do
    einfo "i is ${i}"
done
while hungry ; do
    eat_cookies
done

Case Statement

Indent alternatives by 4 spaces and ;; on a separate line.

Example:

case "$var" in
    up | down)
	;;
    left | right)
        do_something
	;;
    debug)
	debug_something
	;;
    *)
	exit 1
	;;
esac

Naming Conventions

Function Names

Lower-case, with underscores to separate words. Separate libraries with ::. Parentheses are required after the function name. The keyword function is optional, but must be used consistently throughout a project.

For example:

# Single function
my_func() {
    ...
}

# Part of a package
mypackage::my_func() {
    ...
}

Constants and Environment Variable Names

All caps, separated with underscores, declared at the top of the file. Constants and anything exported to the environment should be capitalized.

# Constant
readonly PATH_TO_FILES='/some/path'

# Both constant and environment
declare -xr ORACLE_SID='PROD'

Some things become constant at their first setting (for example, via getopts). Thus, it's OK to set a constant in getopts or based on a condition, but it should be made readonly immediately afterwards. Note that declare doesn't operate on global variables within functions, so readonly or export is recommended instead.

VERBOSE='false'
while getopts 'v' flag; do
    case "${flag}" in
        v)
            VERBOSE='true'
            ;;
    esac
done
readonly VERBOSE

Read-only variables

Use readonly or declare -r to ensure they're read only.

As globals are widely used in shell, it's important to catch errors when working with them. When you declare a variable that is meant to be read-only, make this explicit.

zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
    error_message
else
    readonly zip_version
fi

Use Local Variables

Declare function-specific variables with local. This avoids polluting the global name space and inadvertently setting variables that may have significance outside the function.

Declaration and assignment must be separate statements when the assignment value is provided by a command substitution, as the local builtin does not propagate the exit code from the command substitution.

my_func2() {
    local name="$1"

    # Separate lines for declaration and assignment:
    local my_var
    my_var="$(my_func)" || return

    # DO NOT do this: $? contains the exit code of 'local', not my_func
    local my_var="$(my_func)"
    [[ $? -eq 0 ]] || return

    ...
}

Command Substitution

Use $(command) instead of backticks.

Nested backticks require escaping the inner ones with \. The $(command) format doesn't change when nested and is easier to read.

Example:

# This is preferred:
var="$(command "$(command1)")"

# This is not:
var="`command \`command1\``"

Single versus Double Brackets in bash

Important: The [[ ]] form is generally safer than [ ] and should be used in all new code. This is because [[ ]] is a bash syntax construct, whereas [ ] is a program which happens to be implemented as an internal -- as such, cleaner syntax is possible with the former. For a simple illustration, consider:

bash$ [ -n $foo ] && [ -z $foo ] && echo "huh?"
huh?
bash$ [[ -n $foo ]] && [[ -z $foo ]] && echo "huh?"
bash$

However, if you need either path name expansion or word splitting, then [ is required, but be aware of what you are doing to avoid classical mistakes.

Example:

# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
    echo "Match"
fi

# This gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; then
    echo "Match"
fi

Testing Strings

Use quotes rather than filler characters where possible.

Bash is smart enough to deal with an empty string in a test. So, given that the code is much easier to read, use tests for empty/non-empty strings or empty strings rather than filler characters.

# Do this:
if [[ "${my_var}" == "some_string" ]]; then
    do_something
fi

# -z (string length is zero) and -n (string length is not zero) are
# preferred over testing for an empty string
if [[ -z "${my_var}" ]]; then
    do_something
fi

# This is OK (ensure quotes on the empty side), but not preferred:
if [[ "${my_var}" == "" ]]; then
    do_something
fi

# Not this:
if [[ "${my_var}X" == "some_stringX" ]]; then
    do_something
fi

To avoid confusion about what you're testing for, explicitly use -z or -n.

# Use this
if [[ -n "${my_var}" ]]; then
    do_something
fi

# Instead of this as errors can occur if ${my_var} expands to a test
# flag
if [[ "${my_var}" ]]; then
    do_something
fi

Pipelines and Compound commands

Pipelines should be split one per line if they don't all fit on one line.

If a pipeline all fits on one line, it should be on one line.

If not, it should be split at one pipe segment per line with the pipe on the newline and a 4 space indent for the next section of the pipe. This applies to a chain of commands combined using | as well as to logical compounds using || and &&.

Example:

# All fits on one line
command1 | command2

# Long commands
command1 \
    | command2 \
    | command3 \
    | command4

SUID/SGID

SUID and SGID are forbidden on shell scripts.

There are too many security issues with shell that make it nearly impossible to secure sufficiently to allow SUID/SGID. While bash does make it difficult to run SUID, it's still possible on some platforms which is why we're being explicit about banning it.

Use sudo to provide elevated access if you need it.

Always

Use common sense and BE CONSISTENT.

Other References