Skip to content

Script Development

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

Script Development

← User Guide | Home | Team Workflows →


Table of Contents

📚 Quick Navigation
New to DotRun? Start with the User Guide and Quick Start Tutorial
Need examples? Check Script Examples for ready-to-use templates
Working with a team? See Team Workflows for collaboration

Script Structure

Basic Template

Every DotRun script should follow this structure:

#!/usr/bin/env bash
### DOC
# Script Name
#
# Brief description (one line)
#
# Detailed description explaining what the script does,
# when to use it, and any important considerations.
#
# Usage: dr scriptname [options] [arguments]
#
# Options:
#   -h, --help      Show this help message
#   -v, --verbose   Enable verbose output
#   -d, --dry-run   Show what would be done without doing it
#
# Arguments:
#   arg1    Description of first argument
#   arg2    Description of second argument (optional)
#
# Examples:
#   dr scriptname arg1
#   dr scriptname -v arg1 arg2
#   dr scriptname --dry-run arg1
#
# Dependencies:
#   - tool1: Required for X functionality
#   - tool2: Optional, enables Y feature
#
# Environment Variables:
#   VAR_NAME    Description of what this variable does
#
# Exit Codes:
#   0   Success
#   1   General error
#   2   Invalid arguments
#   3   Missing dependencies
### DOC

# Strict mode
set -euo pipefail

# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Default values
VERBOSE=false
DRY_RUN=false

# Functions
usage() {
  grep '^#' "$0" | grep -v '^#!/' | grep -v '^### DOC' | sed 's/^# *//'
}

error() {
  echo "Error: $1" >&2
  exit "${2:-1}"
}

info() {
  if [[ "$VERBOSE" == true ]]; then
    echo "Info: $1"
  fi
}

# Argument parsing
while [[ $# -gt 0 ]]; do
  case $1 in
    -h | --help)
      usage
      exit 0
      ;;
    -v | --verbose)
      VERBOSE=true
      shift
      ;;
    -d | --dry-run)
      DRY_RUN=true
      shift
      ;;
    -*)
      error "Unknown option: $1" 2
      ;;
    *)
      break
      ;;
  esac
done

# Main logic
main() {
  # Validate arguments
  if [[ $# -eq 0 ]]; then
    error "Missing required argument" 2
  fi

  local arg1="$1"
  local arg2="${2:-default}"

  info "Processing $arg1 with $arg2"

  if [[ "$DRY_RUN" == true ]]; then
    echo "Would execute: command $arg1 $arg2"
  else
    # Actual implementation
    echo "Executing: command $arg1 $arg2"
  fi
}

# Run main function with remaining arguments
main "$@"

Advanced Features

Progress Indicators

# Simple progress
echo -n "Processing files..."
for file in *.txt; do
  process_file "$file"
  echo -n "."
done
echo " done!"

# Percentage progress
total=$(find . -name "*.log" | wc -l)
current=0
while IFS= read -r file; do
  current=$((current + 1))
  percent=$((current * 100 / total))
  printf "\rProcessing: %3d%%" "$percent"
  process_file "$file"
done < <(find . -name "*.log")
printf "\rProcessing: 100%% - Complete!\n"

Color Output

# Color functions
red() { echo -e "\033[31m$*\033[0m"; }
green() { echo -e "\033[32m$*\033[0m"; }
yellow() { echo -e "\033[33m$*\033[0m"; }
blue() { echo -e "\033[34m$*\033[0m"; }

# Usage
green "✓ Success: Operation completed"
red "✗ Error: Operation failed"
yellow "⚠ Warning: Check configuration"
blue "ℹ Info: Processing started"

Documentation Standards

Inline Documentation Rules

  1. Always include ### DOC section
  2. Start with script name and brief description
  3. Provide clear usage examples
  4. Document all options and arguments
  5. List dependencies and requirements
  6. Include exit codes for debugging

Documentation Sections

Required Sections

  • Script name and description
  • Usage syntax
  • At least one example

Recommended Sections

  • Options (if any)
  • Arguments (if any)
  • Dependencies
  • Environment variables
  • Exit codes
  • Notes/warnings

Optional Sections

  • Author information
  • Version history
  • Related scripts
  • External references

Error Handling

Best Practices

# 1. Use strict mode
set -euo pipefail

# 2. Trap errors
trap 'echo "Error on line $LINENO"' ERR

# 3. Cleanup function
cleanup() {
  # Remove temporary files
  rm -f "$TEMP_FILE"
  # Restore state
  cd "$ORIGINAL_DIR"
}
trap cleanup EXIT

# 4. Check dependencies
check_dependency() {
  if ! command -v "$1" &>/dev/null; then
    error "$1 is required but not installed" 3
  fi
}

check_dependency git
check_dependency jq

Common Patterns

# File operations
if [[ ! -f "$file" ]]; then
  error "File not found: $file"
fi

# Directory operations
if [[ ! -d "$dir" ]]; then
  error "Directory not found: $dir"
fi

# Command success
if ! command_that_might_fail; then
  error "Command failed"
fi

# Pipeline errors
set -o pipefail
if ! curl -s "$url" | jq '.data' >output.json; then
  error "Failed to fetch and parse data"
fi

Input Validation

Argument Validation

# Required arguments
if [[ $# -lt 1 ]]; then
  usage
  error "Missing required argument" 2
fi

# Numeric validation
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
  error "Port must be a number: $port" 2
fi

# Range validation
if ((port < 1 || port > 65535)); then
  error "Port must be between 1 and 65535: $port" 2
fi

# File path validation
if [[ "$path" =~ \.\. ]]; then
  error "Path cannot contain '..': $path" 2
fi

# Email validation
if ! [[ "$email" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
  error "Invalid email format: $email" 2
fi

Interactive Confirmation

confirm() {
  local prompt="${1:-Continue?}"
  local default="${2:-n}"

  if [[ "$default" == "y" ]]; then
    prompt="$prompt [Y/n] "
  else
    prompt="$prompt [y/N] "
  fi

  read -r -p "$prompt" response
  response=${response:-$default}

  [[ "$response" =~ ^[Yy]$ ]]
}

# Usage
if confirm "Delete all files?" "n"; then
  rm -rf ./*
else
  echo "Cancelled"
fi

Testing Scripts

Unit Testing

Create test files for your scripts:

# In test/test_myscript.sh
#!/usr/bin/env bash

source "$(dirname "$0")/../bin/myscript"

# Test function
test_validation() {
  # Test valid input
  if validate_email "user@example.com"; then
    echo "✓ Valid email test passed"
  else
    echo "✗ Valid email test failed"
    return 1
  fi

  # Test invalid input
  if ! validate_email "invalid-email"; then
    echo "✓ Invalid email test passed"
  else
    echo "✗ Invalid email test failed"
    return 1
  fi
}

# Run tests
test_validation

Integration Testing

# Test script execution
test_script_execution() {
  local output
  local exit_code

  # Capture output and exit code
  output=$(dr myscript arg1 2>&1) || exit_code=$?

  # Verify output
  if [[ "$output" =~ "expected string" ]]; then
    echo "✓ Output test passed"
  else
    echo "✗ Output test failed"
    echo "Got: $output"
  fi

  # Verify exit code
  if [[ "${exit_code:-0}" -eq 0 ]]; then
    echo "✓ Exit code test passed"
  else
    echo "✗ Exit code test failed: $exit_code"
  fi
}

Performance Optimization

Best Practices

# 1. Avoid useless cat
# Bad
cat file.txt | grep pattern
# Good
grep pattern file.txt

# 2. Use built-in string manipulation
# Bad
echo "$string" | cut -d: -f1
# Good
echo "${string%%:*}"

# 3. Avoid repeated command calls
# Bad
for file in *.txt; do
  $(dirname "$file")/process.sh "$file"
done
# Good
dir=$(dirname "$file")
for file in *.txt; do
  "$dir/process.sh" "$file"
done

# 4. Use process substitution
# Bad
temp_file=$(mktemp)
command1 >"$temp_file"
command2 <"$temp_file"
rm "$temp_file"
# Good
command2 < <(command1)

Bulk Operations

# Process files in batches
find . -name "*.log" -print0 \
  | xargs -0 -P 4 -n 100 process_batch

# Parallel execution
export -f process_file
find . -name "*.txt" \
  | parallel -j 4 process_file

Security Considerations

Input Sanitization

# Escape special characters
sanitize() {
  printf '%q' "$1"
}

# Use sanitized input
user_input=$(sanitize "$1")
eval "command $user_input"

# Better: avoid eval entirely
command "$1"

Safe File Operations

# Create temp files securely
TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"' EXIT

# Set restrictive permissions
touch sensitive_file
chmod 600 sensitive_file

# Validate file paths
if [[ "$file_path" =~ ^/ ]]; then
  error "Absolute paths not allowed"
fi

# Prevent directory traversal
real_path=$(realpath "$file_path")
if [[ ! "$real_path" =~ ^"$SAFE_DIR" ]]; then
  error "Access denied: $file_path"
fi

Environment Security

# Clear sensitive variables
unset PASSWORD
unset API_KEY

# Use secure defaults
umask 077 # Files created with 600 permissions

# Validate environment
if [[ -z "${HOME:-}" ]]; then
  error "HOME environment variable not set"
fi

Next Steps

With solid script development practices:

  1. Share with Team: Learn about Team Workflows
  2. Manage Configuration: Set up Configuration Management
  3. Create Documentation: Master the Documentation System
  4. Browse Examples: Study Example Scripts
  5. Optimize Workflow: Explore Developer Experience

← User Guide | Team Workflows → | Top ↑

Clone this wiki locally