Operations 25 min read

Essential Shell Script Coding Standards for Reliable Maintenance

This guide outlines comprehensive shell scripting standards—including why they matter, core principles, file naming, encoding, line length, indentation, naming conventions, comment rules, error handling, and best‑practice patterns—to improve readability, maintainability, and consistency across Bash scripts in production environments.

Efficient Ops
Efficient Ops
Efficient Ops
Essential Shell Script Coding Standards for Reliable Maintenance

Preface

Like other coding standards, this document focuses not only on aesthetic formatting but also on conventions and rules that we commonly follow; undocumented rules are treated as mandatory.

Why Have Coding Standards?

80% of a software's cost is spent on maintenance.

Rarely does the original author maintain the software for its entire life.

Standards improve readability and help developers quickly understand new code.

When source is shipped as a product, it must be well‑packaged and clear.

Coding Standard Levels

Optional: reference only, adopt at discretion.

Preferable: should be adopted unless special circumstances apply.

Mandatory: must be adopted except for very rare special cases.

Note: Unspecified items default to Mandatory.

Source Files

Basics

Use Cases

Shell scripts are recommended for simple utilities or wrappers; individual scripts should not become overly complex.

If the script mainly calls other tools and processes small data, shell is appropriate.

If performance is critical, choose another language.

If handling complex data structures, choose another language.

If the script grows, rewrite it in another language early.

File Names

Executable files should not have an extension; library files must use .sh as a language‑specific suffix. Names must be lowercase and may contain underscores ( _) or hyphens ( -); use hyphens for executables and underscores for libraries.

my-useful-bin
my_useful_libraries.sh
myusefullibraries.sh

Bad examples:

My_Useful_Bin
myUsefulLibraries.sh

File Encoding

Source files must be UTF‑8 with LF line endings.

Line Length

Limit lines to 120 characters, except for import statements or URLs. Use here‑documents or embedded newlines for longer strings.

# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END
# Embedded newlines are ok too
long_string="I am an exceptionally
  long string."

Whitespace

Only spaces are allowed as whitespace inside source files.

Tabs are prohibited; if used, one tab equals four spaces.

Trailing spaces are disallowed.

Garbage Cleanup (Recommended)

Remove unused or commented‑out code, variables, and functions to avoid clutter.

Structure

Using Bash

All executable scripts must start with #!/bin/bash and set options with set to ensure consistent behavior.

#!/bin/bash
set -e

License or Copyright (Recommended)

#
# Licensed under the BSD 3‑Clause License (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#

Indentation

Block Indentation

Increase indentation by four spaces for each new block; never use

. Example:

main() {
    # 4‑space indent
    say="hello"
    flag=0
    if [[ $flag = 0 ]]; then
        # 4‑space indent
        echo "$say"
    fi
}

Pipelines

Place spaces around |. For long pipelines, break after the pipe with a four‑space indent.

# single‑line pipeline
command1 | command2

# multi‑line pipeline
command1 \
    | command2 \
    | command3 \
    | command4

Loops

Place ; do, ; then on the same line as while, for, or if. else starts a new line.

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

Case Statements

Indent options by four spaces; place pattern, commands, and ;; on separate lines.

case "${expression}" in
    a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
    absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
    *)
        error "Unexpected expression '${expression}'"
        ;;
esac

Function Placement

Place all functions after constants; avoid executable code between functions. Define a main function for scripts with multiple functions.

main "$@"

Comments

Make comments clear and concise.

Avoid overly decorative comments.

Write comments before coding.

Explain design intent, not just code behavior.

Align comment indentation with surrounding code; use a single space after #.

File Header

Each file must start with a top‑level comment describing its purpose, in addition to any license.

#!/bin/bash
# Perform hot backups of databases.

Function Comments

Every function must include a comment block describing its purpose, global variables used, parameters, and return values.

# ---------------------------------------
# Cleanup files from the backup dir
# Globals: BACKUP_DIR BACKUP_SID
# Arguments: None
# Returns: None
# ---------------------------------------
cleanup() {
    ...
}

TODO Comments

Use uppercase TODO with optional identifier in parentheses.

# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
# TODO(--bug=123456): remove the "Last visitors" feature

Naming

Function Names

Use lowercase with underscores; for packages use double colons ::. No space before parentheses.

# Single function
my_func() {
    ...
}

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

Variable Names

Same rules as function names; loop variables should be similar to the iterated collection.

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

Constants and Environment Variables

All caps with underscores, declared at the top. Use readonly or declare -xr after initial assignment.

# Constant
readonly PATH_TO_FILES='/some/path'

# Exported constant
declare -xr BACKUP_SID='PROD'

Read‑only Variables

Use readonly or declare -r to enforce immutability.

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

Local Variables

Declare one variable per line; use local inside functions and separate declaration from assignment.

my_func2() {
    local name="$1"
    local my_var
    my_var="$(my_func)" || return
    ...
}

Error and Logging

Send error messages to STDERR and use functions for consistent formatting.

err() {
    echo "[$(date +'%FT%T%z')]: $@" >&2
}

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

Conditional Tests

Prefer [[ ... ]] over [ ... ] or test for better syntax and regex support.

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

String Tests

Use -z for empty strings, -n for non‑empty, and avoid unnecessary quoting.

if [[ -z "${my_var}" ]]; then
    do_something
fi

Filename Expansion

Specify explicit paths when using globbing to avoid accidental matches.

# Safe removal
rm -v ./*

Avoid eval

Prefer explicit commands over eval to maintain clarity and safety.

# Bad
eval $(set_my_variables)

Avoid Pipe‑to‑While Loops

Use process substitution or for loops instead, as pipe‑connected while runs in a subshell.

# Bad
your_command | while read line; do
    last_line="${line}"
 done

echo "${last_line}"  # prints NULL

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

Check Return Values

Always verify command success and provide meaningful exit codes.

if ! mv "${file_list}" "${dest_dir}/"; then
    echo "Unable to move ${file_list} to ${dest_dir}" >&2
    exit "${E_BAD_MOVE}"
fi

Prefer Built‑ins Over External Commands

Use Bash arithmetic expansion and parameter substitution instead of external utilities.

addition=$((X + Y))
substitution="${string/#foo/bar}"

File Loading

Use source instead of . for clarity.

source my_libs.sh

Content Filtering and Statistics

Prefer single commands with appropriate options over unnecessary pipelines.

grep net.ipv4 /etc/sysctl.conf
grep -c net.ipv4 /etc/sysctl.conf
wc -l /etc/sysctl.conf

Proper Return vs Exit

Functions should return error codes rather than exit, allowing callers to handle failures.

# Good
my_func() { [[ -e /dummy ]] || return 1; }
cleanup() { ... }
my_func
cleanup

# Bad
my_func() { [[ -e /dummy ]] || exit 1; }
cleanup() { ... }
my_func
cleanup

Tools

ShellCheck

Source: http://itxx00.github.io/blog/2020/01/03/shell-standards/

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

coding standardscode qualityBashShell scripting
Efficient Ops
Written by

Efficient Ops

This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.