Bash Scripting Styles and Conventions
Style guide and best practices for writing Bash scripts in DocOps Lab projects
A guide to writing clean, consistent, and maintainable Bash scripts, based on best practices.
Bash Version
Use Bash 4.0 or later to take advantage of modern features like associative arrays and improved string manipulation.
File and Script Structure
Shebang
Always start your scripts with a shebang.
For scripts that require Bash-specific features, use #!/usr/bin/env bash.
#!/usr/bin/env bash
Script Header
Include a header comment block at the top of your script. This block should briefly explain:
-
The script’s purpose.
-
Any dependencies required to run it.
-
License information.
-
Usage examples or a pointer to more detailed documentation.
Code Organization
Structure your script into logical sections to improve readability.
The preferred order is:
-
Global Constants and Variables: Define all global variables and constants at the top.
-
Function Definitions: Group all functions together.
-
Argument Parsing: Handle command-line arguments and flags.
-
Main Logic: The main execution block of the script. Often a
casestatement that dispatches commands.
#!/usr/bin/env bash
#
# script-name
#
# Brief description of what the script does.
# Depends on: curl, jq
# --- GLOBAL VARIABLES ---
readonly SCRIPT_VERSION="1.0.0"
LOG_FILE="/var/log/script-name.log"
# --- FUNCTION DEFINITIONS ---
my_function() {
# ...
}
# --- ARGUMENT PARSING ---
if [[ "$1" == "--help" ]]; then
# ...
fi
# --- MAIN LOGIC ---
main() {
# ...
}
main "$@"
Naming Conventions
- Global Variables and Constants
-
Use
SCREAMING_SNAKE_CASE. Usereadonlyfor constants.-
readonly MAX_RETRIES=5 -
APP_CONFIG_PATH=".env"
-
- Local Variables
-
Use
snake_caseandlocaldeclaration.-
local user_name="$1"
-
- Function Names
-
Use
snake_case.-
get_user_details()
-
Variables and Data
Declaration and Scoping
Always use local to declare variables inside functions.
This prevents polluting the global scope and avoids unintended side effects.
my_function() {
local file_path="$1" # Good: variable is local to the function
count=0 # Bad: variable is global by default
}
Quoting
Always quote variable expansions ("$variable") and command substitutions ("$(command)") to prevent issues with word splitting and unexpected filename expansion (globbing).
# Good: handles spaces and special characters in filenames
echo "$file_name"
touch "$new_file"
# Bad: will fail if file_name contains spaces
echo $file_name
touch $new_file
| Files created by DocOps Lab should never include spaces, but this habit is important for dealing with user input or external data. |
Arrays
Use standard indexed arrays for lists of items.
Use associative arrays (declare -A) for key-value pairs (i.e., maps).
# Indexed array
local -a packages=("git" "curl" "jq")
echo "First package is: ${packages[0]}"
# Associative array
declare -A user_details
user_details["name"]="John Doe"
user_details["email"]="john.doe@example.com"
echo "User email: ${user_details["email"]}"
Functions
Syntax
Use the function_name() { … } syntax for clarity.
Arguments
Access arguments using positional parameters ($1, $2, etc.).
Use "$@" to forward all arguments.
log_message() {
local level="$1"
local message="$2"
echo "[$level] $message"
}
log_message "INFO" "Process complete."
Returning Values
To return a string or data, use echo or printf and capture the output using command substitution.
get_user_home() {
local user="$1"
# ... logic to find home directory ...
echo "/home/$user" # Returns string via stdout
}
To return a status, use return with a numeric code. 0 means success, and any non-zero value (1-255) indicates failure.
check_file_exists() {
if [[ -f "$1" ]]; then
return 0 # Success
else
return 1 # Failure
fi
}
Conditionals
Use [[ … ]] for conditional tests.
It is more powerful, prevents many common errors, and is easier to use than the older [ … ] or test builtins.
# Good
if [[ "$name" == "admin" && -f "$config_file" ]]; then
# ...
fi
# Avoid
if [ "$name" = "admin" -a -f "$config_file" ]; then
# ...
fi
For dispatching based on a command or option, case statements are often cleaner than long if/elif/else chains.
case "$command" in
build)
build_image
;;
run)
run_container
;;
*)
echo "Error: Unknown command '$command'" >&2
exit 1
;;
esac
Error Handling
-
Use
set -o errexit(orset -e) at the top of your script to make it exit immediately if a command fails. -
Use
set -o pipefailto cause a pipeline to fail if any of its commands fail, not just the last one. -
Print error messages to standard error (
stderr). -
Exit with a non-zero status code on failure.
#!/usr/bin/env bash
set -o errexit
set -o pipefail
echo "Error: Something went wrong." >&2
exit 1
Practices to Avoid
Avoid eval
The eval command can execute arbitrary code and poses a significant security risk if used with external or user-provided data.
It also makes code difficult to debug.
Avoid it whenever possible.
Modern Bash versions provide safer alternatives like namerefs (declare -n) for indirect variable/array manipulation.
Avoid Backticks
Use $(...) for command substitution instead of backticks (`...`).
It is easier to read and can be nested.
# Good
current_dir="$(pwd)"
# Avoid
current_dir=`pwd`