Skip to content

Script Development Best Practices

Joao Palma edited this page Oct 27, 2025 · 3 revisions

Script Development Best Practices

Skill Level: Beginner to Intermediate

Essential practices for creating maintainable, secure, and effective DotRun scripts.

Naming Conventions

Script Names

  • Use descriptive, kebab-case names: branch-cleanup, deploy-staging
  • Include action and context: git-merge-tool, docker-build-image
  • Avoid generic names: Use database-backup instead of backup
  • Keep names concise but clear: db-migrate instead of database-migration-runner

Examples:

# Good naming
dr set git/branch-cleanup
dr set docker/build-image
dr set deploy/staging-environment

# Avoid
dr set script1
dr set backup
dr set thing

Category Organization

  • Use clear category names: git/, docker/, deployment/
  • Group by technology or workflow: frontend/, testing/, monitoring/
  • Keep hierarchy shallow: Prefer git/branch-cleanup over git/branch/cleanup
  • Use consistent naming across teams

Organization Patterns:

# By Technology Stack
git/
docker/
kubernetes/
terraform/

# By Workflow Stage
development/
testing/
deployment/
monitoring/

# By Team Function
frontend/
backend/
devops/
qa/

Script Structure

Required Elements

Every script should include:

  1. Proper Shebang: #!/usr/bin/env bash
  2. Error Handling: set -euo pipefail
  3. Documentation: ### DOC sections
  4. Main Function: Entry point with "$@"

Template:

#!/usr/bin/env bash
### DOC
# Brief description of what this script does
# Usage: dr script-name [options] [arguments]
#
# Examples:
#   dr script-name
#   dr script-name --verbose
### DOC
set -euo pipefail

main() {
  # Script logic here
  echo "✅ Script completed successfully!"
}

main "$@"

Documentation Standards

Inline Documentation (### DOC)

  • Start with one-line description
  • Include usage examples with actual commands
  • Document important options and flags
  • Note dependencies and requirements
  • Keep it concise but informative
### DOC
# Git branch cleanup tool with smart merge detection
#
# This script safely removes merged branches after confirming
# they have been integrated into the main branch.
#
# Usage:
#   dr git/branch-cleanup          # Interactive mode
#   dr git/branch-cleanup --auto   # Auto-remove merged branches
#   dr git/branch-cleanup --dry-run # Show what would be removed
#
# Requirements: git
# Safety: Never removes current branch or main/master
### DOC

Markdown Documentation

  • Follow consistent structure (Usage, Options, Examples, Notes)
  • Include practical, working examples
  • Document edge cases and limitations
  • Keep documentation updated when script changes
  • Use clear headings and formatting

Error Handling

Use Proper Bash Options

# Always include at the top of scripts
set -euo pipefail # Exit on error, undefined vars, pipe failures

# For scripts that need to continue on errors
set -uo pipefail # Allow errors but catch undefined vars and pipe failures

Validate Inputs

# Check required parameters
if [[ -z "${1:-}" ]]; then
  echo "❌ Usage: dr script-name <required-param>"
  exit 1
fi

# Validate file existence
if [[ ! -f "$config_file" ]]; then
  echo "❌ Configuration file not found: $config_file"
  exit 1
fi

# Validate directory existence
if [[ ! -d "$target_dir" ]]; then
  echo "❌ Target directory does not exist: $target_dir"
  exit 1
fi

Check Dependencies

# Using DotRun helpers
source "$DR_CONFIG/helpers/pkg.sh"
validatePkg git docker kubectl

# Manual checking
check_dependencies() {
  local deps=("git" "docker" "kubectl")
  for dep in "${deps[@]}"; do
    if ! command -v "$dep" >/dev/null 2>&1; then
      echo "❌ Required dependency not found: $dep"
      echo "💡 Install with: brew install $dep" # or appropriate command
      exit 1
    fi
  done
}

Provide Clear Error Messages

# Include context and suggestions
error() {
  echo "❌ Error: $1" >&2
  if [[ -n "${2:-}" ]]; then
    echo "💡 Suggestion: $2" >&2
  fi
  exit 1
}

# Usage examples
[[ -f "$file" ]] || error "File not found: $file" "Check the file path and permissions"
command || error "Command failed" "Ensure prerequisites are installed"

Security Considerations

Never Hardcode Secrets

# ❌ Bad - secrets in code
PASSWORD="secret123"
API_KEY="abc123def456"

# ✅ Good - use environment variables
PASSWORD="${PASSWORD:-$(read -s -p 'Enter password: ' pwd && echo "$pwd")}"
API_KEY="${API_KEY:-}"
if [[ -z "$API_KEY" ]]; then
  echo "❌ API_KEY environment variable required"
  exit 1
fi

Validate File Paths

# ❌ Bad - dangerous user input
rm -rf "$user_input"

# ✅ Good - validate input
if [[ "$user_input" =~ ^[a-zA-Z0-9/_.-]+$ ]] && [[ -d "$user_input" ]]; then
  rm -rf "$user_input"
else
  echo "❌ Invalid or unsafe path: $user_input"
  exit 1
fi

Handle Temporary Files Safely

# Create secure temporary files
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT

# Use secure temporary directories
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT

Performance Best Practices

Avoid Redundant Operations

# ❌ Slow - repeated calls
for file in *.txt; do
  if git status --porcelain | grep -q "$file"; then
    echo "$file is modified"
  fi
done

# ✅ Fast - cache the result
git_status=$(git status --porcelain)
for file in *.txt; do
  if echo "$git_status" | grep -q "$file"; then
    echo "$file is modified"
  fi
done

Use Efficient Tools

# ❌ Slower
grep -r "pattern" .

# ✅ Faster (if available)
if command -v rg >/dev/null; then
  rg "pattern"
else
  grep -r "pattern" .
fi

Parallel Execution When Appropriate

# ❌ Slow - sequential operations
for repo in repo1 repo2 repo3; do
  git clone "$repo"
done

# ✅ Fast - parallel operations
for repo in repo1 repo2 repo3; do
  git clone "$repo" &
done
wait

Platform Compatibility

Handle OS Differences

# macOS vs Linux differences
if [[ "$OSTYPE" == "darwin"* ]]; then
  # macOS-specific code
  if command -v greadlink >/dev/null; then
    readlink_cmd="greadlink"
  else
    readlink_cmd="readlink"
  fi
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
  # Linux-specific code
  readlink_cmd="readlink"
fi

Package Manager Detection

# Handle different package managers
if command -v apt >/dev/null; then
  # Ubuntu/Debian
  install_cmd="sudo apt install"
elif command -v yum >/dev/null; then
  # CentOS/RHEL
  install_cmd="sudo yum install"
elif command -v pacman >/dev/null; then
  # Arch Linux
  install_cmd="sudo pacman -S"
elif command -v brew >/dev/null; then
  # macOS
  install_cmd="brew install"
fi

WSL Considerations

# Handle WSL-specific issues
if grep -q microsoft /proc/version 2>/dev/null; then
  # Running in WSL
  # Handle line ending differences
  # Use WSL paths, not Windows paths
  # Be aware of file permission limitations
  echo "🐧 Running in WSL environment"
fi

Testing and Validation

Include Self-Tests

#!/usr/bin/env bash
### DOC
# Script with built-in testing capability
### DOC
set -euo pipefail

run_tests() {
  echo "🧪 Running self-tests..."

  # Test 1: Dependency check
  if ! command -v git >/dev/null; then
    echo "❌ Test failed: git not available"
    return 1
  fi
  echo "✅ Dependencies available"

  # Test 2: Function test
  if [[ "$(test_function "hello")" == "Hello, hello!" ]]; then
    echo "✅ Function test passed"
  else
    echo "❌ Function test failed"
    return 1
  fi

  echo "✅ All tests passed"
}

test_function() {
  local input="$1"
  echo "Hello, $input!"
}

main() {
  if [[ "${1:-}" == "--test" ]]; then
    run_tests
    exit $?
  fi

  # Regular script logic
  test_function "World"
}

main "$@"

Validate with ShellCheck

# Include ShellCheck validation in your workflow
#!/usr/bin/env bash

# Validate all scripts
find ~/.config/dotrun/bin -name "*.sh" -exec shellcheck {} \;

# Or integrate into your script
if command -v shellcheck >/dev/null; then
  shellcheck "$0"
fi

Code Style Guidelines

Consistent Formatting

# ✅ Good formatting
if [[ -f "$file" ]]; then
  echo "File exists"
elif [[ -d "$file" ]]; then
  echo "Directory exists"
else
  echo "Not found"
fi

# Use consistent indentation (4 spaces)
function long_function() {
  local variable="value"

  if [[ condition ]]; then
    do_something
  fi
}

# Quote variables to prevent word splitting
echo "Hello, $name!"
rm -f "$temp_file"

Variable Naming

# Use descriptive variable names
config_file="/path/to/config"
user_name="$1"
max_retries=3

# Use readonly for constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="1.0.0"

# Use local in functions
function process_data() {
  local input_file="$1"
  local output_file="$2"

  # Process files
}

Next Steps


Following these practices will help you create professional, maintainable scripts that work reliably across different environments.

Clone this wiki locally