Web Analytics

Best Practices

Advanced ~30 min read

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.

Output
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 { }
Shebang Best Practice: Always use #!/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=()
Descriptive Names: Prefer 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

  1. Shebang and header comments
  2. Configuration: set options, constants, defaults
  3. Global variables: declared explicitly
  4. Functions: utility functions first, then main logic
  5. Main: entry point, argument parsing
  6. 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!
The set -euo pipefail Trio:
  • -e: Exit on any command failure
  • -u: Exit on undefined variable
  • -o pipefail: Pipeline fails if any command fails
These catch most common scripting errors automatically.

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 pipefail and quote variables
  • Documentation: Include header comments, function docs, and inline explanations
  • Modern syntax: Use [[ ]], $(), and func() {} 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!