| name | bash-style |
| description | Required style guidelines for writing shell scripts where POSIX-compliance is not an explicit requirement |
Bash Script Style Guide
When to Use Shell
Shell should only be used for small utilities or simple wrapper scripts:
- Use shell when: You're mostly calling other utilities and doing relatively little data manipulation
- Don't use shell when: Performance matters, or you're writing scripts longer than 100 lines
- Consider rewriting: Scripts with non-straightforward control flow logic should be rewritten in a more structured language
The complexity threshold is about maintainability by people other than the author.
IMPORTANT - LACK OF POSIX COMPLIANCE: This rule only applies when POSIX-compatibility is NOT a requirement. These guidelines are specifically for bash-based shell scripts that can leverage bash-specific features and "bashisms."
Core Requirements
1. Shebang and Shell Selection
Always use bash for executable scripts:
#!/bin/bash
Use 'set' for shell options:
#!/bin/bash
set -euo pipefail
2. File Extensions
- Executables: Use
.bash extension OR no extension
- Use
.bash if build rules will rename the file
- Use no extension if script goes directly into user's PATH
- Libraries: Must have
.bash extension and should NOT be executable
3. File Header Comments
Every file must start with a description:
#!/bin/bash
Formatting Standards
1. Indentation
Use tabs for initial indentation:
if [[ "${1}" == "start" ]]; then
echo "Starting service..."
if systemctl start myservice; then
echo "Service started successfully"
else
echo "Failed to start service" >&2
return 1
fi
fi
Use spaces for subsequent indentation:
This is rare; usually only used for visual alignment.
if [[ "${1}" == "start" ]]; then
echo "Starting service..."
if systemctl start myservice; then
report_start "myservice"
log "myservice started"
monitor_service "myservice"
else
echo "Failed to start service" >&2
return 1
fi
fi
ALWAYS use spaces for indentation within multi-line comments!
if [[ "${1}" == "start" ]]; then
echo "Starting service..."
if systemctl start myservice; then
echo "Service started successfully"
else
echo "Failed to start service" >&2
return 1
fi
fi
2. Line Length
Keep lines under 80 characters when possible:
- DO NOT compromise readability or maintainability just to stay under 80, especially where subshells come into play
- PREFER to break sentences at sentence ends or logical subjects, rather than just at 80 characters
- PREFER to let a line go a little bit above 80 rather than having a stupid-short 2nd line
Break lines logically, not arbitrarily at 80 characters:
if [[ "${enable_logging}" == "true" ]] && [[ -w "${log_directory}" ]]; then
echo "Logging enabled to ${log_directory}"
fi
if [[ "${enable_logging}" == "true" ]] && [[ -w "${log_directory}" \
]]; then
echo "Logging enabled to ${log_directory}"
fi
Prefer slightly longer lines over awkwardly short continuation lines:
echo "Processing configuration file: ${config_file} with options: ${options}"
echo "Processing configuration file: ${config_file} with options: \
${options}"
For long commands, use line continuation with proper indentation:
command \
--option1 value1 \
--option2 value2 \
--option3 value3
command --option1 value1 --option2 value2 \
--option3 value3
For long strings, use here documents:
cat <<EOF
This is a long message that would exceed the 80 character limit
if written on one line.
EOF
echo "This is a long message that would exceed \
the 80 character limit if written on one line."
Break sentences at natural boundaries:
cat <<EOF
Starting backup process for database ${db_name}.
This may take several minutes depending on database size.
EOF
cat <<EOF
Starting backup process for database ${db_name}. This may take
several minutes depending on database size.
EOF
3. Pipelines
Put pipelines on separate lines when they become long:
ps aux | grep nginx
command1 \
| command2 \
| command3 \
| command4
4. Control Flow
Use proper spacing and alignment:
if [[ "${condition}" ]]; then
elif [[ "${other_condition}" ]]; then
else
fi
for file in "${files[@]}"; do
process_file "${file}"
done
while read -r line; do
echo "Processing: ${line}"
done < "${input_file}"
5. Case Statements
Align and indent consistently:
case "${1}" in
start)
start_service
;;
stop)
stop_service
;;
restart)
stop_service
start_service
;;
*)
echo "Usage: ${0} {start|stop|restart}" >&2
exit 1
;;
esac
Variable and Quoting Rules
1. Variable Expansion
Always use braces for variable expansion:
echo "Hello ${name}!"
echo "File: ${file}.backup"
echo "Hello $name!"
echo "File: $file.backup"
2. Quoting
Quote variables to prevent word splitting:
if [[ -f "${config_file}" ]]; then
cp "${config_file}" "${backup_dir}/"
fi
if [[ -f $config_file ]]; then
cp $config_file $backup_dir/
fi
Quote all strings except in specific contexts:
echo "Starting process: ${process_name}"
grep "pattern" "${file}"
(( count = count + 1 ))
if (( count > 10 )); then
echo "Count exceeded limit"
fi
Function Standards
1. Function Names
Use lowercase with underscores (snake_case):
process_file() {
local file="${1}"
}
2. Function Structure and Documentation
Use consistent formatting with comprehensive documentation:
Any function that is not both obvious and short must have a function header comment. All functions in libraries must have a function header comment regardless of length or complexity.
All function header comments must describe the intended API behavior using these required sections (always present, even if not applicable):
- Description: What the function does
- Globals: List of global variables used and modified
- Arguments: Arguments taken
- Outputs: Output to STDOUT or STDERR
- Returns: Returned values other than the default exit status
process_log_file() {
local log_file="${1}"
local format="${2:-text}"
if [[ ! -f "${log_file}" ]]; then
echo "Error: Log file '${log_file}' not found" >&2
return 1
fi
if [[ "${format}" != "text" && "${format}" != "json" ]]; then
echo "Error: Invalid format '${format}'. Use 'text' or 'json'" >&2
return 2
fi
case ""
json)
grep \
| sed \
| -u \
| jq -R
;;
text)
grep \
| sed \
| -u
;;
0
}
() {
message=
}
3. Local Variables
Always use local variables in functions:
process_data() {
local input_file="${1}"
local output_file="${2}"
local temp_file
temp_file="$(mktemp)"
sort "${input_file}" > "${temp_file}"
mv "${temp_file}" "${output_file}"
}
Naming Conventions
1. Variables
Use lowercase with underscores (snake_case):
user_name="john_doe"
config_file="/etc/myapp/config.conf"
temp_directory="/tmp/myapp_$$"
2. Constants and Environment Variables
Use uppercase with underscores:
readonly CONFIG_DIR="/etc/myapp"
readonly MAX_RETRIES=3
declare -xr LOG_LEVEL="INFO"
3. Loop Variables
Name descriptively:
for zone in "${availability_zones[@]}"; do
deploy_to_zone "${zone}"
done
for i in "${availability_zones[@]}"; do
deploy_to_zone "${i}"
done
Error Handling and Return Values
1. Check Return Values
Always check command return values:
if ! mv "${source_file}" "${dest_dir}/"; then
echo "Error: Unable to move ${source_file} to ${dest_dir}" >&2
exit 1
fi
cp "${file}" "${backup_location}"
if (( $? != 0 )); then
echo "Error: Failed to backup ${file}" >&2
exit 1
fi
2. Pipeline Error Handling
Use PIPESTATUS for pipeline error checking:
tar -czf - "${directory}" | ssh user@host "cat > backup.tar.gz"
if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then
echo "Error: Backup pipeline failed" >&2
exit 1
fi
long_command | filter_command | output_command
return_codes=( "${PIPESTATUS[@]}" )
if (( return_codes[0] != 0 )); then
echo "Error: long_command failed" >&2
elif (( return_codes[1] != 0 )); then
echo "Error: filter_command failed" >&2
fi
3. Error Reporting
Send errors to STDERR with timestamps:
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}
if ! create_backup "${database}"; then
err "Failed to create backup for database: ${database}"
exit 1
fi
4. Returning Values
When returning status from a function, use numerical return codes:
is_even() {
if (( ${1} % 2 == 0 )); then
return 0
else
return 1
fi
}
file_exists() {
if [[ -f "${1}" ]]; then
echo "true"
else
echo "false"
fi
}
When returning strings from a function with stdout, ensure that the function only ever returns the proper string:
mentions_cursor() {
git fetch --all >&2
if grep -q -lr "cursor"; then
echo "true"
else
echo "false"
fi
}
mentions_cursor() {
if grep -lr "cursor" . >/dev/null; then
echo "true"
else
echo "false"
fi
}
mentions_cursor() {
if grep -q -lr "cursor"; then
echo "true"
else
echo "false"
fi
}
mentions_cursor() {
if grep -lr "cursor"; then
echo "true"
else
echo "false"
fi
}
Feature Usage Guidelines
1. Command Substitution
Use $(...) instead of backticks:
current_date="$(date '+%Y-%m-%d')"
file_count="$(find "${dir}" -type f | wc -l)"
current_date=`date '+%Y-%m-%d'`
file_count=`find "${dir}" -type f | wc -l`
2. Test Constructs
Prefer [[ ]] over [ ]:
if [[ "${file}" =~ \.txt$ ]]; then
echo "Text file detected"
fi
if [[ -n "${variable}" && "${variable}" != "default" ]]; then
process_variable "${variable}"
fi
if [[ -z "${string}" ]]; then
echo "String is empty"
fi
if [[ "${string1}" == "${string2}" ]]; then
echo "Strings are equal"
fi
if [[ -f "${file}" ]]; then
echo "File exists"
fi
if [[ -d "${directory}" ]]; then
echo "Directory exists"
fi
3. Arithmetic
Use (( )) for arithmetic operations:
(( total = count * price ))
(( i += 1 ))
if (( count > threshold )); then
echo "Threshold exceeded"
fi
4. Arrays
Use bash arrays when appropriate:
declare -a files
files=( "/path/one" "/path/two" "/path/three" )
files=(
"/path/one"
"/path/two"
"/path/three"
)
for file in "${files[@]}"; do
echo "Processing: ${file}"
done
echo "Total files: ${#files[@]}"
Main Function Pattern
Use main function for excecutable scripts with multiple functions:
#!/bin/bash
setup_environment() {
}
process_arguments() {
}
cleanup() {
}
main() {
setup_environment
process_arguments "$@"
cleanup
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Security Considerations
1. Avoid SUID/SGID
Never use SUID/SGID on shell scripts:
if ! sudo systemctl restart nginx; then
echo "Error: Failed to restart nginx" >&2
exit 1
fi
2. Validate Inputs
Always validate and sanitize inputs:
validate_input() {
local input="${1}"
if [[ -z "${input}" ]]; then
echo "Error: Input required" >&2
return 1
fi
if [[ ! "${input}" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "Error: Invalid input format" >&2
return 1
fi
return 0
}
Built-in Preferences
Prefer bash built-ins over external commands:
string_length="${#variable}"
substring="${variable:0:10}"
replacement="${variable/pattern/replacement}"
Advanced Features
1. Wildcard Expansion
Be careful with filename expansion:
for file in /path/to/files/*.txt; do
[[ -f "${file}" ]] || continue
process_file "${file}"
done
set -f
echo "This * will not expand"
set +f
2. Process Substitution
Use process substitution instead of pipes to while:
while read -r line; do
echo "Processing: ${line}"
done < <(some_command)
some_command | while read -r line; do
echo "Processing: ${line}"
done
3. Here Documents and Here Strings
Use here documents for multi-line strings:
cat <<EOF
This is a multi-line
string that can contain
variable substitutions: ${variable}
EOF
cat <<'EOF'
This text is literal:
${variable} will not be expanded
EOF
grep "pattern" <<<"${string_to_search}"
Common Pitfalls to Avoid
- Don't use aliases in scripts - Use functions instead
- Avoid eval - Find alternative approaches
- Don't ignore return values - Always check command success
- Avoid pipes to while loops - Use process substitution or arrays
- Extraneous stdout in functions that return strings - redirect or discard all output except the return value
- Don't use ls for file operations - Use globs or find instead
for file in $(ls *.txt); do
process_file "${file}"
done
for file in *.txt; do
[[ -f "${file}" ]] || continue
process_file "${file}"
done
Testing and Validation
Write testable shell scripts:
#!/bin/bash
add() {
echo $(( $1 + $2 ))
}
subtract() {
echo $(( $1 - $2 ))
}
main() {
case "${1}" in
add) add "${2}" "${3}" ;;
sub) subtract "${2}" "${3}" ;;
*) echo "Usage: ${0} {add|sub} num1 num2" >&2; exit 1 ;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Example test file (using bats or similar):
#!/usr/bin/env bats
source "$(dirname "$BATS_TEST_FILENAME")/calculator.sh"
@test "add function works correctly" {
result="$(add 5 3)"
[ "$result" -eq 8 ]
}
@test "subtract function works correctly" {
result="$(subtract 10 4)"
[ "$result" -eq 6 ]
}
ShellCheck Integration
Always use ShellCheck for static analysis:
shellcheck myscript.sh
readonly UNUSED_VAR="value"
Common ShellCheck fixes:
cp $file $destination
cp "${file}" "${destination}"
local var="$(command_that_might_fail)"
local var
var="$(command_that_might_fail)"