| name | bash |
| description | Apply comprehensive bash scripting standards including main function pattern, usage documentation, argument parsing, dependency checking, and error handling. Triggers when creating/editing .sh files, bash scripts, or discussing shell scripting, deployment scripts, automation tasks, or bash conventions. |
| metadata | {"version":"2.0.0"} |
Bash Best Practices Skill
This skill ensures all bash scripts follow enterprise-grade best practices for maintainability, reliability, and user-friendliness.
When This Skill Applies
This skill should be automatically invoked when:
- Creating new bash scripts or shell scripts
- Editing existing .sh or .bash files
- User requests scripts for automation, deployment, backup, or system tasks
- Reviewing or refactoring bash code
- User mentions: bash, shell script, main function, argument parsing, script structure
Script Complexity Guidelines
This skill recognizes two categories of scripts with different structural requirements:
Simple Scripts
Scripts that do one simple thing well without unnecessary ceremony.
Characteristics:
- Less than ~30 lines of actual logic (excluding comments and whitespace)
- No command-line arguments required
- Single, focused purpose that's self-evident
- Linear flow or minimal branching
- Output is often consumed by other programs or scripts
Examples: Version detection, environment checks, data format conversion, simple calculations
Structural Requirements:
- Main function and guard clause: Optional
- Usage function: Not needed (no arguments to document)
- Argument parsing: Not applicable
- Color output: Optional (often unnecessary)
- Can use direct execution without function wrapping
Ordinary Scripts
Scripts with broader scope requiring full structure for maintainability.
Characteristics:
- More than ~30 lines of logic OR
- Takes command-line arguments OR
- Multiple functions or complex branching OR
- Interactive use by humans OR
- Requires detailed documentation
Examples: Deployment tools, backup utilities, system maintenance scripts, development tools
Structural Requirements:
- Main function and guard clause: Required
- Usage function: Required
- Argument parsing: Required (if taking arguments)
- Full error handling: Required
- User-friendly output: Required (for interactive scripts)
Core Principles to Apply
For Ordinary Scripts
When writing ordinary scripts, ensure:
- Main function pattern with guard clause - Every script must have a main() function
- Usage function - Clear help documentation
- Argument parsing - Structured option handling in main
- Dependency checking - Explicit validation of required tools
- Explicit error handling - Never use
set -e, handle errors explicitly
- Function organization - Correct ordering and single responsibility
- User-friendly output - Colored, structured output with status indicators
For Simple Scripts
When writing simple scripts, ensure:
- Clear purpose comment - Explain what the script does at the top
- Explicit error handling - Never use
set -e, handle errors explicitly
- Proper exit codes - 0 for success, non-zero for failure
- Error output to stderr - Use
>&2 for error messages
- Variable safety - Quote variables, handle undefined cases
Script Structure Templates
Template for Ordinary Scripts
Every new bash script should follow this structure:
#!/usr/bin/env bash
DEPENDENCIES=(jq curl git)
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
if [[ -n "${NO_COLOR:-}" ]] || [[ "${TERM:-}" == "dumb" ]]; then
RED=""
GREEN=""
YELLOW=""
BLUE=""
NC=""
fi
function usage() {
cat <<EOM
Brief description of what this script does.
usage: ${SCRIPT_NAME} [options]
options:
-i|--input <file> Input file to process (required)
-o|--output <file> Output file (optional, defaults to stdout)
-v|--verbose Enable verbose output
-h|--help Show this help message
--version Show version information
dependencies: ${DEPENDENCIES[@]}
examples:
${SCRIPT_NAME} -i data.txt -o report.json
${SCRIPT_NAME} --input data.txt --verbose
EOM
exit 1
}
function main() {
local input_file=""
local output_file=""
local verbose=false
while [ "$1" != "" ]; do
case $1 in
-i | --input)
shift
input_file="$1"
;;
-o | --output)
shift
output_file="$1"
;;
-v | --verbose)
verbose=true
;;
--version)
echo "${SCRIPT_NAME} version ${VERSION}"
exit 0
;;
-h | --help)
usage
;;
*)
echo "Error: Unknown option '$1'"
usage
;;
esac
shift
done
if [ -z "$input_file" ]; then
echo "Error: Input file is required" >&2
usage
fi
exit_on_missing_tools "${DEPENDENCIES[@]}"
process_file "$input_file" "$output_file" "$verbose"
}
function process_file() {
local input="$1"
local output="$2"
local verbose="$3"
echo "Processing ${input}..."
}
function exit_on_missing_tools() {
for cmd in "$@"; do
if command -v "$cmd" &>/dev/null; then
continue
fi
printf "Error: Required tool '%s' is not installed or not in PATH\n" "$cmd"
exit 1
done
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
exit 0
fi
Template for Simple Scripts
For scripts that do one simple thing without arguments:
#!/usr/bin/env bash
TAG=$(git describe --exact-match --tags HEAD 2>/dev/null)
if [ -n "$TAG" ]; then
echo "${TAG#v}"
else
if [ -n "$GITHUB_HEAD_REF" ]; then
BRANCH="$GITHUB_HEAD_REF"
elif [ -n "$GITHUB_REF_NAME" ]; then
BRANCH="$GITHUB_REF_NAME"
else
BRANCH=$(git symbolic-ref -q --short HEAD 2>/dev/null)
if [ -z "$BRANCH" ]; then
echo "Error: Unable to determine branch" >&2
exit 1
fi
fi
SANITIZED=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/_/g')
echo "${SANITIZED}-SNAPSHOT"
fi
Key points for simple scripts:
- Clear purpose comment at the top
- Direct execution without main() wrapper
- Errors still go to stderr with >&2
- Meaningful exit codes (0 for success, non-zero for failure)
- Variables are still properly quoted
- No need for usage() function when there are no arguments
- No need for color output when output is data
Detailed Best Practices
1. Script Bootstrapping
Main Function Pattern
- ALWAYS use a
main function as the entry point
- This enables safe sourcing by other scripts
- Provides clear structure and testability
The Guard Clause
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
exit 0
fi
- Must be at the very end of the script
- Enables the script to be sourced without execution
- Always include explicit
exit 0 after main
2. Usage Function and Argument Parsing
Usage Function Requirements
- Must be defined before main() function
- Use heredoc for clean formatting
- Include: description, options, dependencies, examples
- Exit with status 1
Argument Parsing Best Practices
function main() {
local input_file=""
local verbose=false
while [ "$1" != "" ]; do
case $1 in
-i | --input)
shift
input_file="$1"
;;
-v | --verbose)
verbose=true
;;
-h | --help)
usage
;;
*)
echo "Error: Unknown option '$1'"
usage
;;
esac
shift
done
if [ -z "$input_file" ]; then
echo "Error: Input file is required" >&2
usage
fi
}
3. Dependency Declaration and Checking
Always Declare Dependencies
DEPENDENCIES=(jq curl git docker)
Check Before Use
function exit_on_missing_tools() {
for cmd in "$@"; do
if command -v "$cmd" &>/dev/null; then
continue
fi
printf "Error: Required tool '%s' is not installed or not in PATH\n" "$cmd"
exit 1
done
}
Include in Usage
Show dependencies in the usage function so users know requirements upfront.
4. Function Organization
Correct Function Order (MANDATORY)
usage() - First after global declarations
main() - Immediately after usage
- Business logic functions - Core functionality
- Utility functions - Generic helpers
NEVER add section comment headers like "# UTILITY FUNCTIONS". The code structure speaks for itself.
Function Design Principles
- Single Responsibility: Each function does one thing
- Clear Inputs/Outputs: Use local variables, return meaningful status
- Descriptive Names: Use verb-noun combinations (create_backup, validate_input)
- Keep Small: Break down functions longer than 50 lines
- Use Local Variables: Always declare with
local
5. Error Handling
AVOID set -e
Never use set -e. Instead, use explicit error handling:
if ! command; then
echo "Error: Command failed" >&2
exit 1
fi
cd /some/directory || {
echo "Error: Cannot change directory" >&2
exit 1
}
output=$(command 2>&1)
if [ $? -ne 0 ]; then
echo "Error: Command failed: $output" >&2
exit 1
fi
Error Handling Rules
- Always redirect errors to stderr with
>&2
- Provide context about what failed
- Use meaningful exit codes
- Clean up temporary files on failure
6. User-Friendly Output
Color Output (When Appropriate)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
if [[ -n "${NO_COLOR:-}" ]] || [[ "${TERM:-}" == "dumb" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" NC=""
fi
Output Functions
function print_header() {
echo -e "${BLUE}=======================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}=======================${NC}"
}
function print_success() {
echo -e "${GREEN}✅ $1${NC}"
}
function print_error() {
echo -e "${RED}❌ Error: $1${NC}" >&2
}
function print_warning() {
echo -e "${YELLOW}⚠️ Warning: $1${NC}"
}
function print_step() {
local current="$1"
local total="$2"
local message="$3"
echo -e "${YELLOW}[${current}/${total}] ${message}${NC}"
}
7. Keep Main Function Lean
Main should only:
- Parse arguments
- Validate inputs
- Check dependencies
- Call business functions
- Handle high-level errors
Business logic belongs in separate functions.
8. Code Comments
Avoid Excessive Comments
- Well-named functions are self-documenting
- Only comment the "why", not the "what"
- Use comments for:
- Complex algorithms
- Non-obvious workarounds
- Business logic rules
- External API quirks
counter=$((counter + 1))
delay=$((2 ** attempt * 1000 + RANDOM % 1000))
Script Generation Checklist
When generating a new script, verify:
Common Patterns
Interactive Prompts
read -p "Continue? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Operation cancelled"
exit 0
fi
echo "Select an option:"
echo "1) Full backup"
echo "2) Incremental backup"
read -p "Enter choice (1-2): " choice
read -s -p "Enter password: " password
echo
Temporary Files
temp_file=$(mktemp)
trap "rm -f '$temp_file'" EXIT
temp_dir=$(mktemp -d)
trap "rm -rf '$temp_dir'" EXIT
Pipeline Error Handling
command1 | command2 | command3
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "Error: command1 failed" >&2
exit 1
fi
Examples to Follow
When users request scripts for common tasks, apply these patterns:
Backup Script
- Include timestamp in backup names
- Verify source exists before backup
- Check available disk space
- Provide progress feedback
- Clean up old backups based on retention
Deployment Script
- Validate environment first
- Show clear step progression
- Roll back on failure
- Log all operations
- Provide summary at end
Data Processing Script
- Validate input format
- Handle large files efficiently
- Provide progress indicators
- Support resume on failure
- Generate detailed reports
Choosing Between Simple and Ordinary Scripts
Decision Tree
Ask these questions to determine which pattern to use:
-
Does the script take command-line arguments?
- YES → Use ordinary script pattern (needs usage() and parsing)
- NO → Continue to question 2
-
Is the logic more than ~30 lines (excluding comments)?
- YES → Use ordinary script pattern (needs structure)
- NO → Continue to question 3
-
Does it have multiple functions or complex branching?
- YES → Use ordinary script pattern (needs organization)
- NO → Continue to question 4
-
Will it be run interactively by humans who need help?
- YES → Use ordinary script pattern (needs user-friendly features)
- NO → Continue to question 5
-
Is it doing one simple, self-evident task?
- YES → Use simple script pattern
- NO → Use ordinary script pattern
When in doubt, use the ordinary pattern. It's better to have structure you don't need than to need structure you don't have.
Examples of Simple Scripts
determine-version.sh - Outputs version string based on git state
check-env.sh - Verifies environment variables are set
format-json.sh - Reformats JSON from stdin to stdout
count-lines.sh - Counts lines in specified file types
is-production.sh - Returns 0 if on production branch
Examples of Ordinary Scripts
deploy.sh - Multi-step deployment with rollback
backup.sh - Database backup with retention management
setup-dev.sh - Developer environment initialization
test-runner.sh - Configurable test execution
migrate-data.sh - Data migration with validation
Testing Scripts
Always test generated scripts for:
- Syntax:
bash -n script.sh
- Undefined variables: Run with
set -u temporarily
- Dependencies: Run without required tools installed
- Error paths: Test with invalid inputs
- Sourcing: Verify guard clause works (for ordinary scripts)
Remember
Every script you generate or modify should be production-ready:
- Maintainable by others
- Self-documenting through good structure
- Gracefully handles errors
- Provides helpful output
- Follows consistent patterns
These practices ensure scripts are professional, reliable, and user-friendly.