Best Practices
Writing Bash scripts that work is one thingβwriting scripts that are maintainable, readable, and robust is another. This lesson covers professional coding standards that will make your scripts production-ready. Following these practices helps you and others understand, debug, and extend your code!
Code Style Guidelines
Consistent style makes code easier to read and maintain. Here are industry-standard conventions.
Click Run to execute your code
Essential Style Rules
| Rule | Good | Bad |
|---|---|---|
| Indentation | 2 or 4 spaces, consistent | Mixed tabs/spaces |
| Line length | Max 80-100 characters | Very long single lines |
| Quoting | "$var" |
$var |
| Test syntax | [[ ]] |
[ ] |
| Function style | func_name() { } |
function func_name { } |
#!/usr/bin/env bash for portability, or #!/bin/bash if you need a specific path. Never omit the shebang!
Naming Conventions
Clear, consistent naming makes code self-documenting.
# Variables: lowercase with underscores
user_name="john"
max_retries=3
config_file="/etc/app.conf"
# Constants: UPPERCASE with underscores
readonly MAX_CONNECTIONS=100
readonly DEFAULT_TIMEOUT=30
declare -r LOG_DIR="/var/log/myapp"
# Functions: lowercase with underscores, verb_noun pattern
get_user_input() { ... }
validate_config() { ... }
process_file() { ... }
# Private/internal functions: prefix with underscore
_helper_function() { ... }
_validate_internal() { ... }
# Boolean variables: use is_, has_, can_ prefixes
is_valid=true
has_error=false
can_write=true
# Arrays: plural names
declare -a users=()
declare -a log_files=()
declare -A config_options=()
user_count over cnt, config_file over cf. The few extra characters are worth the clarity.
Script Organization
Well-organized scripts follow a predictable structure.
#!/usr/bin/env bash
#
# Script: deploy.sh
# Description: Deploy application to production servers
# Author: Your Name
# Date: 2024-01-15
# Version: 1.0.0
#
# ==============================================================================
# CONFIGURATION
# ==============================================================================
set -euo pipefail
IFS=$'\\n\\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Default values
readonly DEFAULT_ENV="staging"
readonly DEFAULT_TIMEOUT=60
# ==============================================================================
# GLOBALS
# ==============================================================================
declare -g verbose=false
declare -g dry_run=false
declare -g environment="$DEFAULT_ENV"
# ==============================================================================
# FUNCTIONS
# ==============================================================================
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Deploy application to specified environment.
Options:
-e, --env ENV Target environment (default: $DEFAULT_ENV)
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done
-h, --help Show this help message
Examples:
$SCRIPT_NAME -e production server1
$SCRIPT_NAME --dry-run -v server2
EOF
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
die() {
echo "ERROR: $*" >&2
exit 1
}
# ==============================================================================
# MAIN
# ==============================================================================
main() {
# Parse arguments
# Validate inputs
# Execute logic
log "Deployment complete"
}
# Only run main if script is executed (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Section Order
- Shebang and header comments
- Configuration: set options, constants, defaults
- Global variables: declared explicitly
- Functions: utility functions first, then main logic
- Main: entry point, argument parsing
- Execution guard: check if sourced vs executed
Defensive Programming
Write scripts that fail safely and handle edge cases.
# Always start with strict mode
set -euo pipefail
# Validate all inputs
validate_input() {
local input="$1"
[[ -z "$input" ]] && die "Input required"
[[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]] || die "Invalid characters in input"
return 0
}
# Check dependencies at startup
check_dependencies() {
local deps=(curl jq awk)
for cmd in "${deps[@]}"; do
command -v "$cmd" >/dev/null 2>&1 || {
die "Required command not found: $cmd"
}
done
}
# Use default values
config_file="${CONFIG_FILE:-/etc/default.conf}"
timeout="${TIMEOUT:-30}"
# Check file existence before operations
[[ -f "$config_file" ]] || die "Config not found: $config_file"
[[ -r "$config_file" ]] || die "Config not readable: $config_file"
# Use temporary files safely
temp_file=$(mktemp) || die "Failed to create temp file"
trap 'rm -f "$temp_file"' EXIT
# Quote all variable expansions
process_file "$input_file" # Good
process_file $input_file # Bad - word splitting!
-e: Exit on any command failure-u: Exit on undefined variable-o pipefail: Pipeline fails if any command fails
Documentation Standards
Good documentation helps future maintainers (including yourself).
# File header - describe purpose, author, usage
#!/usr/bin/env bash
#
# backup.sh - Automated backup script for MySQL databases
#
# Usage: backup.sh [-d database] [-o output_dir] [-r retention_days]
#
# Environment Variables:
# MYSQL_HOST - Database host (default: localhost)
# MYSQL_USER - Database user (required)
# MYSQL_PASSWORD - Database password (required)
#
# Exit Codes:
# 0 - Success
# 1 - General error
# 2 - Missing dependency
# 3 - Configuration error
#
# Function documentation
# @description Validates database connection
# @param $1 hostname
# @param $2 username
# @return 0 on success, 1 on failure
validate_connection() {
local host="$1"
local user="$2"
# Implementation...
}
# Inline comments for complex logic
# Calculate retention date (files older than this will be deleted)
# Using -mtime +N finds files modified MORE than N days ago
retention_date=$(date -d "-${retention_days} days" +%Y%m%d)
Common Mistakes
1. Not quoting variables
# Wrong - word splitting and glob expansion
rm -rf $dir/*
[ -f $file ]
# Correct - always quote
rm -rf "$dir"/*
[[ -f "$file" ]]
2. Using deprecated syntax
# Wrong - old style
result=\`command\`
[ $a -eq $b ]
function myFunc {
# Correct - modern style
result=$(command)
[[ $a -eq $b ]]
myFunc() {
3. Hardcoding paths
# Wrong - fragile
cd /home/user/project
source /home/user/config.sh
# Correct - relative to script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
source "$SCRIPT_DIR/config.sh"
4. Ignoring exit codes
# Wrong - continues even if command fails
cp important.txt backup/
rm important.txt
# Correct - check or use set -e
cp important.txt backup/ || die "Copy failed"
rm important.txt
# Or with set -e at script start
set -e
cp important.txt backup/
rm important.txt
Summary
- Consistency: Use consistent indentation (2-4 spaces), naming, and style
- Naming: snake_case for variables/functions, UPPERCASE for constants
- Organization: Follow standard section order: header β config β functions β main
- Defensive: Always use
set -euo pipefailand quote variables - Documentation: Include header comments, function docs, and inline explanations
- Modern syntax: Use
[[ ]],$(), andfunc() {}style
What's Next?
Now let's dive deeper into Error Handling. You'll learn advanced techniques with set options, trap commands, and building robust error recovery patterns!
Enjoying these tutorials?