shell Scripting Series: Programming Style

Function name

Lowercase letters, underscores:

# Single function
my_func() {
  ...
}

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

variable name

Lowercase letters, underscores, loop variables:

for zone in ${zones}; do
    something_with "${zone}"
done

read-only variable

Use readonly or declare -r statement:

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

Constants and Environment Variables

uppercase letter:

# Constant
readonly PATH_TO_FILES='/some/path'

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

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

Use local variables

local declaration:

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

...
}

source file name

Lowercase letters, underscores:

make_template.sh

print error message

err() {
    echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
}

if ! do_something; then
    err "Unable to do_something"
    exit "${E_DID_NOTHING}"
fi

Basic Statement Format

Indent four spaces, or a tab, and a tab is set to four spaces.

Lines can be up to 80 characters long, use \ to wrap.

Pipes: If the entire pipeline operation can fit on one line, write the entire pipeline operation on the same line. Otherwise, the entire pipeline operation should be split into one segment per line, and the next part of the pipeline operation should place the pipe character on a new line and indent by 2 spaces. This applies to the combined command chain of the use pipe character '|' and the logical operation chain using '|' and '& &'

# All fits on one line
command1 | command2

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

for loop

Please put ;do, ;then and while, for, if on the same line.

for dir in ${dirs_to_cleanup}; do
    if [[ -d "${dir}/${ORACLE_SID}" ]]; then
        log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
        rm "${dir}/${ORACLE_SID}/"*
        if [[ "$?" -ne 0 ]]; then
            error_message
        fi
    else
        mkdir -p "${dir}/${ORACLE_SID}"
        if [[ "$?" -ne 0 ]]; then
        error_message
        fi
    fi
done

case statement

case "${expression}" in
    a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
    absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
    *)
        error "Unexpected expression '${expression}'"
        ;;
esac
verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
    case "${flag}" in
        a) aflag='true' ;;
        b) bflag='true' ;;
        f) files="${OPTARG}" ;;
        v) verbose='true' ;;
        *) error "Unexpected option ${flag}" ;;
    esac
done

variable expansion

Use \({var} instead of \)var , as detailed below:

  • Be consistent with what you find in existing code.
  • Reference variables See the next section, References.
  • Do not enclose single-character shell special or positional variables in curly braces unless absolutely necessary or to avoid deep confusion. It is recommended to enclose all other variables in curly brackets.

quote

set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@"
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@"

command substitution

use $(command) instead of backticks`command`. 

test, [ and [[

[[ ... ]] is recommended instead of [ , test , and /usr/bin/[ .

if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
    echo "Match"
fi

if [[ "filename" == "f*" ]]; then
    echo "Match"
fi

test string

if [[ "${my_var}" = "some_string" ]]; then
    do_something
fi

if [[ -n "${my_var}" ]]; then
    do_something
fi
# true if string is not empty

if [[ -z "${my_var}" ]]; then
    do_something
fi
# true when string is empty

if [[ "${my_var}" = "" ]]; then
    do_something
fi
# true when string is empty

Wildcard expansion of filenames

Since filenames may start with - , it is much safer to use the extended wildcard ./* than * .

rm -v ./*    # it is good
rm -v *    # not good

pipe-directed while loop

Pipes lead to implicit sub shell s in while loops that make tracking down bug s difficult.

last_line='NULL'
your_command | while read line; do
    last_line="${line}"
done

echo "${last_line}"

If you are sure that the input does not contain spaces or special symbols (usually meaning that it was not entered by the user), then you can use a for loop.

total=0
for value in $(command); do
    total+="${value}"
done

Using procedure substitution allows redirecting output, but put the command into an explicit subshell instead of the implicit subshell that bash creates for while loops.

total=0
last_file=
while read count filename; do
    total+="${count}"
    last_file="${filename}"
done < <(your_command | uniq -c)

echo "Total = ${total}"
echo "Last one = ${last_file}"

A while loop can be used when there is no need to pass complex results to the parent shell. This usually requires some more complex "parsing". Note that simple examples may be easier to accomplish using tools such as awk. This may also be useful when you specifically don't want to change the parent shell's scope variables.

cat /proc/mounts | while read src dest type opts rest; do
    if [[ ${type} == "nfs" ]]; then
        echo "NFS ${dest} maps to ${src}"
    fi
done

Check the return value

mv "${file_list}" "${dest_dir}/"
if [[ "$?" -ne 0 ]]; then
    echo "Unable to move ${file_list} to ${dest_dir}" >&2
    exit "${E_BAD_MOVE}"
fi

Tags: shell

Posted by cueball2000uk on Sat, 02 Jul 2022 02:35:35 +0930