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.
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.shBad examples:
My_Useful_Bin
myUsefulLibraries.shFile 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 -eLicense 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 \
| command4Loops
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
doneCase 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}'"
;;
esacFunction 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" featureNaming
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}"
doneConstants 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
fiLocal 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}"
fiConditional Tests
Prefer [[ ... ]] over [ ... ] or test for better syntax and regex support.
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
echo "Match"
fiString Tests
Use -z for empty strings, -n for non‑empty, and avoid unnecessary quoting.
if [[ -z "${my_var}" ]]; then
do_something
fiFilename 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}"
fiPrefer 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.shContent 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.confProper 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
cleanupTools
ShellCheck
Source: http://itxx00.github.io/blog/2020/01/03/shell-standards/
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
