
Bash Script Generator
- 420 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
bash-script-generator is an agent skill that generates production-ready bash scripts with strict mode, argument parsing, logging, ShellCheck validation, and templates for deployment hooks, cron jobs, and CI automation.
About
bash-script-generator is a DevOps agent skill from akin-ozer/cc-devops-skills that walks agents through a mandatory requirements-capture and generation workflow for maintainable shell automation. Its SKILL.md spans roughly 25.9 KB of patterns for strict mode, traps, grep/awk/sed pipelines, API client scripts, and cron-friendly utilities, and it pairs with devops-skills:bash-script-validator for ShellCheck-backed iteration. The parent cc-devops-skills plugin advertises 31 generator and validator skills across Terraform, Docker, Kubernetes, GitHub Actions, and more. Reach for bash-script-generator when converting manual CLI steps into validated .sh files for deployment hooks, scheduled jobs, or CI glue instead of writing brittle shell from scratch.
- Produces runnable bash with argument parsing and error handling
- Fits CI/CD hooks and local developer workflows
- Reduces copy-paste shell mistakes across environments
- Speeds repeatable ops tasks into versioned scripts
Bash Script Generator by the numbers
- 420 all-time installs (skills.sh)
- Ranked #129 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill bash-script-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 420 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you generate reliable bash scripts for CI glue?
Generate reliable bash scripts for deployment hooks, cron jobs, local dev setup, and CI glue without hand-writing brittle shell from scratch.
Who is it for?
DevOps engineers and backend developers who need validated shell scripts for deployment hooks, cron jobs, log processing, or CI utilities.
Skip if: Skip bash-script-generator when you only need to lint an existing script—use bash-script-validator for validation-only requests.
When should I use this skill?
A developer asks to create, generate, or write a bash script for automation, cron, deployment, or text-processing pipelines.
What you get
Executable .sh scripts with shebang, strict mode, logging helpers, argument parsing, and a validation checklist.
- executable shell script
- requirements capture table
- validation report
By the numbers
- SKILL.md documentation is approximately 25.9 KB
- Part of akin-ozer/cc-devops-skills with 31 DevOps generator and validator skills
Files
Bash Script Generator
Overview
Generate production-ready Bash scripts with clear requirements capture, deterministic generation flow, and validation-first iteration.
Trigger Phrases
Use this skill when the user asks to:
- Create, generate, write, or build a Bash/shell script
- Convert manual CLI steps into an automation script
- Build a text-processing script using
grep,awk, orsed - Create an operations helper script, cron script, or CI utility script
Do not use this skill for validating an existing script only. Use devops-skills:bash-script-validator for validation-only requests.
Execution Model
Follow stages in order. Do not skip a stage; use the documented fallback when blocked.
Stage 0: Preflight
1. Confirm scope and target output path. 2. Confirm shell target:
- Default:
bash - If portability is requested: POSIX
sh
3. Check capabilities and pick fallback path:
| Capability | Default Path | Fallback Path |
|---|---|---|
| Requirement clarification | AskUserQuestion tool | Ask same questions in normal chat; mark unresolved items as assumptions |
| Script scaffold | bash scripts/generate_script_template.sh ... | Copy assets/templates/standard-template.sh manually or hand-craft minimal scaffold |
| Validation | devops-skills:bash-script-validator | Local checks: bash -n, shellcheck if available, sh -n for POSIX mode |
If a fallback path is used, state it explicitly in the final summary.
Stage 1: Capture Requirements
Collect only what is needed to generate the script correctly:
- Input source and format
- Output destination and format
- Error handling behavior (fail-fast/retry/continue)
- Security constraints (sensitive data, privilege level)
- Performance constraints (large files, parallelism)
- Portability requirement (Bash-only vs POSIX)
Then create a Captured Requirements table with stable IDs.
## Captured Requirements
| Requirement ID | Description | Source | Implementation Plan |
|---|---|---|---|
| REQ-001 | Parse nginx logs from file input | User | `parse_args()` + `validate_file()` + `awk` parser |
| REQ-002 | Output top 10 errors | User | `analyze_errors()` + `sort | uniq -c | head -10` |
| REQ-003 | Handle large files efficiently | Assumption | Single-pass `awk`; avoid multi-pass loops |Rules:
- Every major design decision maps to at least one
REQ-*. - Keep assumptions explicit and minimal.
Stage 2: Choose Generation Path
Use this deterministic decision tree:
Need multi-command architecture, unusual control flow, or strict non-template conventions?
├─ Yes -> Custom generation
└─ No
Need standard CLI skeleton (usage/logging/arg parsing/cleanup)?
├─ Yes -> Template-first generation
└─ No -> Custom generationTemplate-first is the default for single-purpose CLI utilities.
Stage 3: Load Only Relevant References
Use progressive disclosure. Read only docs needed for the current request.
| Need | Reference |
|---|---|
Tool choice (grep vs awk vs sed) | docs/text-processing-guide.md |
| Script structure and argument patterns | docs/script-patterns.md |
| Strict mode, shell differences, safety | docs/bash-scripting-guide.md |
| Naming, organization, and quality baseline | docs/generation-best-practices.md |
Citation format (required):
[Ref: docs/<file>.md -> <section>]
Stage 4: Generate Script
Path A: Template-first (default)
1. Generate scaffold:
bash scripts/generate_script_template.sh standard output-script.sh2. Replace placeholders and add business logic. 3. Keep logging to stderr and data output to stdout unless requirements say otherwise. 4. Add comments only where logic is non-obvious.
Path B: Custom generation
Build a script with at least:
- Shebang and strict mode
usage()parse_args()- Input validation and dependency checks
- Main workflow function(s)
- Predictable exit codes
Stage 5: Validate and Iterate
Default validation path: 1. Invoke devops-skills:bash-script-validator 2. Apply fixes 3. Re-run validation 4. Repeat until checks pass or blocker is identified
Fallback validation path (when validator skill is unavailable):
# Deterministic local gate for this skill:
bash scripts/run_ci_checks.sh --skip-shellcheck
# CI gate (shellcheck required):
bash scripts/run_ci_checks.sh --require-shellcheckIf any check is skipped, include Skipped check, Reason, and Risk in the output.
Stage 6: Final Response Contract
Always return: 1. Generated script path 2. Requirements traceability (REQ-* -> implementation) 3. Validation results with rerun status 4. Citations in standard format 5. Any assumptions/fallbacks used
Canonical Example Flows
Example A: Full Flow (Template-first)
1. Clarify missing data format and output expectations. 2. Capture REQ-* table. 3. Choose template-first path. 4. Generate scaffold with scripts/generate_script_template.sh. 5. Implement logic and map functions to REQ-*. 6. Validate with devops-skills:bash-script-validator and rerun until clean. 7. Return final summary with citations.
Example B: Constrained Environment Flow
Use this when AskUserQuestion, validator skill, or shellcheck is unavailable: 1. Ask clarifying questions in chat. 2. Mark unresolved items as assumptions in Captured Requirements. 3. Generate from template script or template file copy fallback. 4. Run bash -n (and sh -n if relevant). 5. If shellcheck is missing, report skip with risk and mitigation.
Done Criteria
The task is complete only when all items are true:
- Trigger matched and scope confirmed
Captured Requirementstable exists withREQ-*IDs- Template-first vs custom decision is documented
- Script is generated with deterministic structure
- Validation executed and rerun policy applied
- Any skipped checks include explicit reason and risk
- Final response includes traceability, citations, and assumptions
Helper Scripts and Assets
- Script generator:
scripts/generate_script_template.sh - Deterministic CI gate:
scripts/run_ci_checks.sh - Regression test suite:
scripts/test_generator.sh - Standard scaffold:
assets/templates/standard-template.sh - Example output style:
examples/log-analyzer.sh
Reference Docs
docs/bash-scripting-guide.mddocs/script-patterns.mddocs/generation-best-practices.mddocs/text-processing-guide.md
External References
#!/usr/bin/env bash
#
# Script Name: SCRIPT_NAME
# Description: Brief description of what this script does
# Usage: SCRIPT_NAME [OPTIONS] ARGUMENTS
#
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly VERSION="1.0.0"
VERBOSE=false
DRY_RUN=false
LOG_LEVEL=1 # 0=DEBUG 1=INFO 2=WARN 3=ERROR
usage() {
cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS] [ARGUMENTS]
Description:
Brief description of what the script does
Options:
-h, --help Show this help message and exit
-v, --verbose Enable verbose output
-d, --debug Enable debug mode
-n, --dry-run Perform dry run without making changes
Examples:
${SCRIPT_NAME} -v file.txt
${SCRIPT_NAME} --dry-run input.txt output.txt
EOF
}
log() {
local level="$1"
shift
echo "[${level}] $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}
log_debug() { if [[ ${LOG_LEVEL} -le 0 ]]; then log "DEBUG" "$@"; fi; }
log_info() { if [[ ${LOG_LEVEL} -le 1 ]]; then log "INFO" "$@"; fi; }
log_warn() { if [[ ${LOG_LEVEL} -le 2 ]]; then log "WARN" "$@"; fi; }
log_error() { log "ERROR" "$@"; }
die() {
log_error "$@"
exit 1
}
check_command() {
command -v "$1" &> /dev/null || die "Required command not found: $1"
}
validate_file() {
[[ -f "$1" ]] || die "File not found: $1"
[[ -r "$1" ]] || die "File not readable: $1"
}
cleanup() {
local exit_code=$?
log_debug "Cleaning up..."
exit "${exit_code}"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-d|--debug) LOG_LEVEL=0; VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
ARGS=("$@")
}
main() {
parse_args "$@"
log_info "Starting ${SCRIPT_NAME}..."
# Main logic goes here
log_info "Completed successfully"
}
trap cleanup EXIT ERR INT TERM
main "$@"
Bash Scripting Guide
Table of Contents
1. Introduction 2. Bash vs POSIX sh 3. Strict Mode and Error Handling 4. Variables and Parameter Expansion 5. Functions and Scope 6. Arrays and Associative Arrays 7. Control Structures 8. Process and Command Substitution 9. Best Practices 10. Common Pitfalls
Introduction
Bash (Bourne Again Shell) is a powerful Unix shell and command language. This guide covers modern bash scripting practices and patterns for creating robust, maintainable scripts.
Bash vs POSIX sh
Key Differences
Bash-specific features (not in POSIX sh):
- Arrays:
arr=(one two three) - Associative arrays:
declare -A map=([key]=value) [[conditional expressions$(( ))arithmetic expansion with more operators${var//pattern/replacement}parameter expansion- Process substitution:
<(command) selectkeyword for menus**recursive globbing withshopt -s globstar
POSIX sh compatible:
- Basic variable assignment and substitution
[test command (single brackets)casestatements- Basic parameter expansion
- Command substitution with
$() - Functions (with different syntax)
When to Choose
Use Bash when:
- Script runs on modern Linux/macOS systems
- Need arrays or associative arrays
- Want advanced string manipulation
- Targeting bash-specific environments
Use POSIX sh when:
- Maximum portability required
- Running on minimal systems (embedded, containers)
- Need to run on different Unix variants
- Following strict POSIX compliance requirements
Strict Mode and Error Handling
Essential: set -euo pipefail
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'Explanation:
set -e(errexit): Exit immediately if a command exits with non-zero statusset -u(nounset): Treat unset variables as an errorset -o pipefail: Return value of pipeline is status of last command to exit with non-zero statusIFS=$'\n\t': Set Internal Field Separator to newline and tab only (prevents word splitting issues)
When to Disable Strict Mode Temporarily
# Disable errexit for commands that are expected to fail
set +e
command_that_might_fail
exit_code=$?
set -e
# Or use || true for single commands
command_that_might_fail || true
# Or handle error explicitly
if ! command_that_might_fail; then
echo "Command failed, but continuing..."
fiSignal Handling with trap
# Cleanup function
cleanup() {
local exit_code=$?
echo "Cleaning up..." >&2
rm -f "${temp_file}"
exit "${exit_code}"
}
# Set traps
trap cleanup EXIT # Always run cleanup on exit
trap cleanup ERR # Run cleanup on error
trap cleanup INT TERM # Run cleanup on interrupt or termination
# Create temp file
temp_file=$(mktemp)
# Rest of script...Error Handling Patterns
# Pattern 1: Die function
die() {
echo "ERROR: $*" >&2
exit 1
}
[[ -f "${file}" ]] || die "File not found: ${file}"
# Pattern 2: Check function return values
if ! do_something; then
echo "do_something failed" >&2
return 1
fi
# Pattern 3: Command substitution with error handling
output=$(command 2>&1) || {
echo "Command failed: ${output}" >&2
exit 1
}
# Pattern 4: Validate prerequisites
check_command() {
command -v "$1" &> /dev/null || die "Required command not found: $1"
}
check_command "jq"
check_command "curl"Variables and Parameter Expansion
Variable Naming Conventions
# Constants - uppercase with readonly
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/myapp/config.conf"
# Environment variables - uppercase
export PATH="${HOME}/bin:${PATH}"
export LOG_LEVEL="INFO"
# Local variables - lowercase
local counter=0
local temp_file=""
# Function names - lowercase with underscores
process_data() {
local input="$1"
# ...
}Always Quote Variables
# Good - properly quoted
rm "${file}"
cp "${source}" "${destination}"
echo "Value: ${variable}"
# Bad - unquoted (prone to word splitting and globbing)
rm $file
cp $source $destination
echo "Value: $variable"Parameter Expansion
# Default values
${var:-default} # Use default if var is unset or empty
${var:=default} # Set var to default if unset or empty
${var:?error message} # Exit with error message if var is unset or empty
${var:+alternative} # Use alternative if var is set
# String manipulation
${var#pattern} # Remove shortest match from beginning
${var##pattern} # Remove longest match from beginning
${var%pattern} # Remove shortest match from end
${var%%pattern} # Remove longest match from end
${var/pattern/replacement} # Replace first match
${var//pattern/replacement} # Replace all matches
${var^} # Uppercase first character
${var^^} # Uppercase all characters
${var,} # Lowercase first character
${var,,} # Lowercase all characters
# Length and substring
${#var} # Length of var
${var:offset} # Substring from offset to end
${var:offset:length} # Substring from offset with length
# Examples
file="/path/to/file.txt"
${file##*/} # file.txt (basename)
${file%.*} # /path/to/file (remove extension)
${file##*.} # txt (extension only)
${file%/*} # /path/to (dirname)Functions and Scope
Function Definition
# POSIX style (portable)
function_name() {
# function body
}
# Bash-specific (not portable to sh)
function function_name {
# function body
}
# Recommended: POSIX style with local variables
process_file() {
local input_file="$1"
local output_file="$2"
# Process file
grep "pattern" "${input_file}" > "${output_file}"
}Variable Scope
# Global variable
GLOBAL_VAR="global"
my_function() {
# Local variable - only visible in function
local local_var="local"
# Modifying global variable
GLOBAL_VAR="modified"
# Function parameter access
local param1="$1"
local param2="$2"
echo "Params: ${param1} ${param2}"
}
my_function "arg1" "arg2"Return Values
# Functions return exit status (0-255)
check_file() {
local file="$1"
[[ -f "${file}" ]] && return 0 || return 1
}
# Use function return status
if check_file "data.txt"; then
echo "File exists"
fi
# Return data via stdout
get_value() {
echo "computed value"
}
# Capture output
result=$(get_value)
# Return data via variable (using nameref in bash 4.3+)
get_data() {
local -n result_var=$1
result_var="computed value"
}
get_data my_result
echo "${my_result}"Arrays and Associative Arrays
Indexed Arrays (Bash-specific)
# Array creation
arr=() # Empty array
arr=(one two three) # Initialize with values
arr[0]="first" # Assign to specific index
# Array operations
arr+=("four") # Append
${arr[0]} # Access element
${arr[@]} # All elements (as separate words)
${arr[*]} # All elements (as single word)
${#arr[@]} # Number of elements
${!arr[@]} # Indices
# Iterating over array
for item in "${arr[@]}"; do
echo "${item}"
done
# Iterating with indices
for i in "${!arr[@]}"; do
echo "Index $i: ${arr[i]}"
done
# Array slicing
${arr[@]:offset:length} # Slice array
# Remove element
unset 'arr[1]' # Remove specific elementAssociative Arrays (Bash 4.0+)
# Declaration required
declare -A map
# Assignment
map[key1]="value1"
map[key2]="value2"
# Or initialize
declare -A map=([key1]="value1" [key2]="value2")
# Access
${map[key1]} # Get value
${map[@]} # All values
${!map[@]} # All keys
${#map[@]} # Number of elements
# Check if key exists
if [[ -v map[key1] ]]; then
echo "key1 exists"
fi
# Iterate over keys and values
for key in "${!map[@]}"; do
echo "${key}: ${map[${key}]}"
donePOSIX Alternative to Arrays
# Use positional parameters
set -- one two three
# Access
echo "$1" # one
echo "$2" # two
echo "$#" # count: 3
# Iterate
for item in "$@"; do
echo "${item}"
done
# Add item
set -- "$@" "four"
# Remove first item
shiftControl Structures
Conditional Expressions
# Bash [[ ... ]] (recommended for bash)
if [[ -f "${file}" ]]; then
echo "File exists"
fi
if [[ "${var}" == "value" ]]; then
echo "Match"
fi
if [[ "${var}" =~ ^[0-9]+$ ]]; then
echo "Numeric"
fi
# POSIX [ ... ] (portable)
if [ -f "${file}" ]; then
echo "File exists"
fi
# File tests
[[ -e file ]] # Exists
[[ -f file ]] # Regular file
[[ -d file ]] # Directory
[[ -L file ]] # Symbolic link
[[ -r file ]] # Readable
[[ -w file ]] # Writable
[[ -x file ]] # Executable
[[ -s file ]] # Not empty
# String tests
[[ -z "${var}" ]] # Empty string
[[ -n "${var}" ]] # Non-empty string
[[ "${a}" == "${b}" ]] # Equal
[[ "${a}" != "${b}" ]] # Not equal
[[ "${a}" < "${b}" ]] # Lexicographically less (bash only)
# Numeric tests
[[ "${a}" -eq "${b}" ]] # Equal
[[ "${a}" -ne "${b}" ]] # Not equal
[[ "${a}" -lt "${b}" ]] # Less than
[[ "${a}" -le "${b}" ]] # Less than or equal
[[ "${a}" -gt "${b}" ]] # Greater than
[[ "${a}" -ge "${b}" ]] # Greater than or equal
# Logical operators
[[ condition1 && condition2 ]] # AND
[[ condition1 || condition2 ]] # OR
[[ ! condition ]] # NOTcase Statements
case "${var}" in
pattern1)
# commands
;;
pattern2|pattern3)
# Multiple patterns
;;
*)
# Default case
;;
esac
# Example with patterns
case "${file}" in
*.txt)
echo "Text file"
;;
*.jpg|*.png)
echo "Image file"
;;
*)
echo "Unknown type"
;;
esacLoops
# while loop
while condition; do
# commands
done
# until loop
until condition; do
# commands
done
# for loop (C-style, bash only)
for ((i=0; i<10; i++)); do
echo "${i}"
done
# for loop (iterating over values)
for item in one two three; do
echo "${item}"
done
# for loop (iterating over files)
for file in *.txt; do
echo "${file}"
done
# for loop (iterating over command output)
while IFS= read -r line; do
echo "${line}"
done < file.txt
# Or with command substitution (avoid for large output)
for file in $(find . -name "*.txt"); do
echo "${file}"
doneProcess and Command Substitution
Command Substitution
# Recommended: $( ... )
result=$(command)
result=$(command arg1 arg2)
# Nested command substitution
outer=$(echo "Inner: $(echo "value")")
# Not recommended: backticks (legacy)
result=`command`Process Substitution (Bash-specific)
# <( ... ) creates a named pipe/file descriptor
# Treat command output as a file
# Compare output of two commands
diff <(ls dir1) <(ls dir2)
# Use multiple inputs
paste <(cut -f1 file1) <(cut -f2 file2)
# Output redirection with process substitution
command > >(tee stdout.log) 2> >(tee stderr.log >&2)Best Practices
Script Structure
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# ============================================================================
# Script Name: example.sh
# Description: Brief description
# Author: Your Name
# Created: 2025-01-23
# ============================================================================
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Global variables
VERBOSE=false
DRY_RUN=false
# Functions
usage() {
# ...
}
cleanup() {
# ...
}
main() {
# ...
}
# Signal handlers
trap cleanup EXIT ERR INT TERM
# Execute main
main "$@"Always Use Quotes
# Good
echo "${variable}"
cp "${source}" "${dest}"
[[ -f "${file}" ]]
# Bad (unsafe)
echo $variable
cp $source $dest
[[ -f $file ]]Use readonly for Constants
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/config"Prefer $() Over Backticks
# Good
output=$(command)
result=$(first $(second))
# Bad
output=`command`
result=`first \`second\`` # Hard to readCheck Command Existence
if ! command -v required_cmd &> /dev/null; then
echo "Error: required_cmd not found" >&2
exit 1
fiValidate Inputs
# Check argument count
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <file>" >&2
exit 1
fi
# Validate file exists
[[ -f "${file}" ]] || { echo "File not found: ${file}" >&2; exit 1; }
# Validate numeric input
[[ "${count}" =~ ^[0-9]+$ ]] || { echo "Count must be numeric" >&2; exit 1; }Common Pitfalls
Word Splitting
# Problem: Filename with spaces
file="my file.txt"
rm $file # Tries to remove "my" and "file.txt"
# Solution: Quote variables
rm "${file}" # Correctly removes "my file.txt"Globbing
# Problem: Pattern in variable
pattern="*.txt"
echo $pattern # Expands to list of .txt files
# Solution: Quote to prevent globbing
echo "${pattern}" # Prints "*.txt"Useless Use of Cat (UUOC)
# Bad: Unnecessary cat
cat file.txt | grep "pattern"
# Good: Direct input
grep "pattern" file.txt
# Bad: cat in loop
cat file.txt | while read line; do
echo "${line}"
done
# Good: redirect to while
while read -r line; do
echo "${line}"
done < file.txtNot Handling Spaces in Filenames
# Bad: Will break on filenames with spaces
for file in $(find . -name "*.txt"); do
process "${file}"
done
# Good: Use while read
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
process "${file}"
done
# Or use globbing
for file in ./**/*.txt; do
process "${file}"
doneIgnoring Command Exit Status
# Bad: Ignoring failure
command_that_might_fail
next_command
# Good: Check exit status
if command_that_might_fail; then
next_command
else
echo "Command failed" >&2
exit 1
fi
# Or with errexit
command_that_might_fail || { echo "Failed" >&2; exit 1; }---
References
- GNU Bash Manual
- Google Shell Style Guide
- ShellCheck - Script analysis tool
- Bash Guide for Beginners
Script Generation Best Practices
Guidelines for generating high-quality, maintainable bash scripts.
Core Principles
1. Security First - Validate inputs, quote variables, avoid injection 2. Fail Fast - Use strict mode, check errors immediately 3. Self-Documenting - Clear names, usage text, comments for complex logic 4. Testable - Modular functions, predictable behavior 5. Maintainable - Consistent style, organized structure
Script Structure Template
#!/usr/bin/env bash
#
# Script Name: descriptive-name.sh
# Description: What it does in one line
# Usage: script.sh [OPTIONS] ARGUMENTS
# Author: Name
# Created: Date
#
set -euo pipefail
IFS=$'\n\t'
# Constants (UPPERCASE, readonly)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Global variables (lowercase or Mixed_Case)
verbose=false
dry_run=false
# Functions (lowercase_with_underscores)
usage() { }
cleanup() { }
main() { }
# Signal handlers
trap cleanup EXIT ERR INT TERM
# Execute
main "$@"Naming Conventions
# Constants - UPPERCASE with readonly
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/app.conf"
# Environment variables - UPPERCASE
export PATH="${HOME}/bin:${PATH}"
export LOG_LEVEL="INFO"
# Global variables - lowercase or Mixed_Case
script_version="1.0.0"
temp_directory=""
# Functions - lowercase_with_underscores
process_file() { }
send_notification() { }
# Local variables - lowercase
local count=0
local file_path=""Security Best Practices
# 1. Always quote variables
rm "${file}" # Good
rm $file # Bad
# 2. Validate all inputs
[[ "${input}" =~ ^[a-zA-Z0-9_-]+$ ]] || die "Invalid input"
# 3. Never use eval with user input
eval "${user_command}" # Dangerous!
# 4. Validate file paths
[[ "${file}" =~ /etc ]] && die "Cannot modify /etc"
[[ -f "${file}" ]] || die "File not found"
# 5. Use $() instead of backticks
output=$(command) # Good
output=`command` # Bad
# 6. Set safe IFS
IFS=$'\n\t'Error Handling Patterns
# Pattern 1: Die function
die() {
echo "ERROR: $*" >&2
exit 1
}
# Pattern 2: Check prerequisites
check_command() {
command -v "$1" &> /dev/null || die "Required: $1"
}
# Pattern 3: Validate inputs
[[ $# -ge 1 ]] || die "Usage: $0 FILE"
[[ -f "$1" ]] || die "File not found: $1"
# Pattern 4: Cleanup on exit
cleanup() {
[[ -n "${temp_dir:-}" ]] && rm -rf "${temp_dir}"
}
trap cleanup EXITFunction Design
# Good function design
#######################################
# Process a log file and extract errors
# Globals:
# LOG_LEVEL
# Arguments:
# $1 - Path to log file
# $2 - Output file (optional)
# Outputs:
# Writes errors to stdout or file
# Returns:
# 0 on success, 1 on error
#######################################
process_log_file() {
local log_file="$1"
local output_file="${2:-}"
# Validate
[[ -f "${log_file}" ]] || return 1
# Process
local errors
errors=$(grep "ERROR" "${log_file}")
# Output
if [[ -n "${output_file}" ]]; then
echo "${errors}" > "${output_file}"
else
echo "${errors}"
fi
return 0
}Code Organization
# Recommended order:
1. Shebang and header comments
2. Strict mode settings
3. Constants
4. Global variables
5. Helper functions (general → specific)
6. Main logic functions
7. Main function
8. Signal handlers
9. Main executionGenerated Code Quality Checklist
- [ ] Proper shebang:
#!/usr/bin/env bash - [ ] Strict mode enabled:
set -euo pipefail - [ ] All variables quoted:
"${var}" - [ ] Constants marked readonly
- [ ] Functions documented
- [ ] Error handling implemented
- [ ] Usage/help function included
- [ ] Input validation present
- [ ] Cleanup on exit (trap)
- [ ] No ShellCheck warnings
- [ ] Comments for complex logic
- [ ] Consistent formatting
References
Bash Script Patterns
Common patterns and templates for bash script generation.
Table of Contents
1. Argument Parsing Patterns 2. Configuration File Handling 3. Logging Frameworks 4. Parallel Processing 5. Lock Files 6. Signal Handling 7. Retry Logic
Argument Parsing Patterns
Simple getopts Pattern
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat << EOF
Usage: ${0##*/} [OPTIONS] FILE
Options:
-h Show this help
-v Verbose output
-f FILE Input file
-o FILE Output file
EOF
}
main() {
local verbose=false
local input_file=""
local output_file=""
while getopts ":hvf:o:" opt; do
case ${opt} in
h) usage; exit 0 ;;
v) verbose=true ;;
f) input_file="${OPTARG}" ;;
o) output_file="${OPTARG}" ;;
:) echo "Option -${OPTARG} requires an argument" >&2; exit 1 ;;
\?) echo "Invalid option: -${OPTARG}" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
# Validation
[[ -n "${input_file}" ]] || { echo "Error: -f required" >&2; exit 1; }
# Process
echo "Processing ${input_file}..."
}
main "$@"Long Options Pattern
# Parse both short and long options
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-f|--file)
INPUT_FILE="$2"
shift 2
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
break
;;
esac
done
# Remaining arguments
REMAINING_ARGS=("$@")
}Subcommand Pattern
#!/usr/bin/env bash
set -euo pipefail
cmd_start() {
echo "Starting service..."
}
cmd_stop() {
echo "Stopping service..."
}
cmd_status() {
echo "Checking status..."
}
usage() {
cat << EOF
Usage: ${0##*/} COMMAND [OPTIONS]
Commands:
start Start the service
stop Stop the service
status Check service status
Options:
-h, --help Show this help
EOF
}
main() {
[[ $# -lt 1 ]] && { usage; exit 1; }
local command="$1"
shift
case "${command}" in
start) cmd_start "$@" ;;
stop) cmd_stop "$@" ;;
status) cmd_status "$@" ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown command: ${command}" >&2; usage; exit 1 ;;
esac
}
main "$@"Configuration File Handling
Source-based Configuration
# config.conf file
CONFIG_VALUE="something"
MAX_RETRIES=3
API_URL="https://api.example.com"
# In script
load_config() {
local config_file="${1:-config.conf}"
if [[ -f "${config_file}" ]]; then
# shellcheck source=/dev/null
source "${config_file}"
else
echo "Warning: Config file not found: ${config_file}" >&2
fi
}
load_config "/etc/myapp/config.conf"Key-Value Configuration Parser
# config.conf format:
# key=value
# # comments
load_config() {
local config_file="$1"
while IFS='=' read -r key value; do
# Skip empty lines and comments
[[ -z "${key}" || "${key}" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
key=$(echo "${key}" | tr -d '[:space:]')
value=$(echo "${value}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Export as variable
declare -g "${key}=${value}"
done < "${config_file}"
}INI-style Configuration Parser
# Parse INI format [section] key=value
parse_ini() {
local file="$1"
local section=""
while IFS= read -r line; do
# Skip empty lines and comments
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*[#;] ]] && continue
# Section header
if [[ "${line}" =~ ^\[([^]]+)\] ]]; then
section="${BASH_REMATCH[1]}"
continue
fi
# Key=value
if [[ "${line}" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
key=$(echo "${key}" | tr -d '[:space:]')
value=$(echo "${value}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Store in associative array
config["${section}.${key}"]="${value}"
fi
done < "${file}"
}Logging Frameworks
Simple Logging with Levels
# LOG_LEVEL: 0=DEBUG, 1=INFO (default), 2=WARN, 3=ERROR
LOG_LEVEL=${LOG_LEVEL:-1}
# Use if-form guards — the && short-circuit form returns 1 when the
# level check fails, which triggers set -e at the call site.
log_debug() { if [[ ${LOG_LEVEL} -le 0 ]]; then echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; fi; }
log_info() { if [[ ${LOG_LEVEL} -le 1 ]]; then echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; fi; }
log_warn() { if [[ ${LOG_LEVEL} -le 2 ]]; then echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; fi; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }File-based Logging
readonly LOG_FILE="${LOG_FILE:-/var/log/myscript.log}"
log_to_file() {
local level="$1"
shift
echo "[${level}] $(date '+%Y-%m-%d %H:%M:%S') $*" >> "${LOG_FILE}"
}
log_info() {
local msg="$*"
echo "[INFO] ${msg}" >&2
log_to_file "INFO" "${msg}"
}Structured JSON Logging
log_json() {
local level="$1"
local message="$2"
local timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
cat <<EOF
{"timestamp":"${timestamp}","level":"${level}","message":"${message}","script":"${SCRIPT_NAME}"}
EOF
}
log_info() {
log_json "INFO" "$*" >&2
}Parallel Processing
Using xargs for Parallel Execution
# Process files in parallel
find . -name "*.txt" -print0 | xargs -0 -P 4 -I {} process_file {}
# With function export
process_file() {
echo "Processing $1..."
# ... processing logic
}
export -f process_file
find . -name "*.txt" | xargs -P 4 -I {} bash -c 'process_file "$@"' _ {}Using GNU Parallel
# Requires: apt-get install parallel
# Simple parallel execution
parallel process_file ::: file1.txt file2.txt file3.txt
# From file list
cat files.txt | parallel process_file
# With progress bar
parallel --bar process_file ::: *.txt
# Control number of jobs
parallel -j 4 process_file ::: *.txtBackground Jobs Pattern
# Track background jobs
pids=()
# Start jobs
for file in *.txt; do
process_file "${file}" &
pids+=($!)
done
# Wait for all jobs
for pid in "${pids[@]}"; do
if wait "${pid}"; then
echo "Job ${pid} completed successfully"
else
echo "Job ${pid} failed" >&2
fi
doneLock Files
Simple Lock File
readonly LOCK_FILE="/var/lock/myscript.lock"
acquire_lock() {
if [[ -f "${LOCK_FILE}" ]]; then
echo "Another instance is running (lock file exists)" >&2
exit 1
fi
echo $$ > "${LOCK_FILE}"
trap 'rm -f "${LOCK_FILE}"' EXIT
}
acquire_lockPID-based Lock with Stale Lock Detection
acquire_lock() {
local lock_file="/var/lock/myscript.lock"
if [[ -f "${lock_file}" ]]; then
local old_pid=$(cat "${lock_file}")
# Check if process is still running
if kill -0 "${old_pid}" 2>/dev/null; then
echo "Another instance (PID ${old_pid}) is running" >&2
return 1
else
echo "Removing stale lock file" >&2
rm -f "${lock_file}"
fi
fi
echo $$ > "${lock_file}"
trap 'rm -f "${lock_file}"' EXIT
}Using flock for Atomic Locking
# Requires flock command
exec 200>/var/lock/myscript.lock
flock -n 200 || { echo "Another instance is running" >&2; exit 1; }
# Script runs exclusively
# Lock is released when script exitsSignal Handling
Cleanup on Exit
cleanup() {
local exit_code=$?
echo "Cleaning up..." >&2
# Remove temp files
[[ -n "${temp_dir:-}" ]] && rm -rf "${temp_dir}"
# Release locks
[[ -f "${lock_file:-}" ]] && rm -f "${lock_file}"
exit "${exit_code}"
}
trap cleanup EXITHandling Multiple Signals
handle_sigint() {
echo "Received SIGINT, cleaning up..." >&2
cleanup
exit 130 # Standard exit code for SIGINT
}
handle_sigterm() {
echo "Received SIGTERM, cleaning up..." >&2
cleanup
exit 143 # Standard exit code for SIGTERM
}
trap handle_sigint INT
trap handle_sigterm TERM
trap cleanup EXIT ERRGraceful Shutdown
SHUTDOWN=false
handle_signal() {
echo "Shutdown signal received, finishing current work..." >&2
SHUTDOWN=true
}
trap handle_signal INT TERM
# Main processing loop
while [[ "${SHUTDOWN}" == "false" ]]; do
process_next_item || break
done
echo "Graceful shutdown complete" >&2Retry Logic
Simple Retry with Backoff
retry() {
local max_attempts=3
local delay=1
local attempt=1
while [[ ${attempt} -le ${max_attempts} ]]; do
if "$@"; then
return 0
else
echo "Attempt ${attempt} failed, retrying in ${delay}s..." >&2
sleep "${delay}"
((attempt++))
((delay*=2)) # Exponential backoff
fi
done
echo "All ${max_attempts} attempts failed" >&2
return 1
}
# Usage
retry curl -f https://api.example.com/dataAdvanced Retry with Custom Parameters
retry_with_backoff() {
local max_attempts="${1}"
local delay="${2}"
local max_delay="${3:-60}"
shift 3
local attempt=1
while [[ ${attempt} -le ${max_attempts} ]]; do
if "$@"; then
return 0
fi
if [[ ${attempt} -lt ${max_attempts} ]]; then
echo "Attempt ${attempt}/${max_attempts} failed" >&2
echo "Retrying in ${delay}s..." >&2
sleep "${delay}"
# Exponential backoff with max cap
delay=$((delay * 2))
[[ ${delay} -gt ${max_delay} ]] && delay=${max_delay}
fi
((attempt++))
done
echo "All ${max_attempts} attempts failed" >&2
return 1
}
# Usage: retry_with_backoff MAX_ATTEMPTS INITIAL_DELAY MAX_DELAY command args...
retry_with_backoff 5 1 30 curl -f https://api.example.com/dataRetry with Jitter
retry_with_jitter() {
local max_attempts="$1"
local base_delay="$2"
shift 2
local attempt=1
while [[ ${attempt} -le ${max_attempts} ]]; do
if "$@"; then
return 0
fi
if [[ ${attempt} -lt ${max_attempts} ]]; then
# Add random jitter (0-100% of delay)
local jitter=$((RANDOM % base_delay))
local delay=$((base_delay + jitter))
echo "Attempt ${attempt} failed, retrying in ${delay}s..." >&2
sleep "${delay}"
# Exponential backoff
((base_delay*=2))
fi
((attempt++))
done
return 1
}---
References
Text Processing Guide
Guide for choosing and using grep, awk, sed, and other text processing tools effectively in bash scripts.
Decision Tree: Which Tool to Use?
Is it a simple pattern match/filter?
├─ YES → Use grep
└─ NO
├─ Is it field/column-based data?
│ └─ YES → Use awk
└─ NO
├─ Is it find-and-replace or deletion?
│ └─ YES → Use sed
└─ NO
└─ Complex processing → Use awk or consider Python/Perlgrep: Pattern Matching and Filtering
When to Use grep
- Searching for patterns in files
- Filtering lines by regex
- Simple line extraction
- Counting matches
- Finding files containing patterns
Common grep Patterns
# Basic search
grep "pattern" file.txt
# Case-insensitive
grep -i "error" log.txt
# Invert match (lines NOT containing pattern)
grep -v "DEBUG" log.txt
# Count matches
grep -c "ERROR" log.txt
# Show line numbers
grep -n "TODO" *.sh
# Extended regex (ERE)
grep -E "(error|fail|critical)" log.txt
# Recursive directory search
grep -r "function_name" src/
# Show filename only
grep -l "pattern" *.txt
# Show context (lines before/after)
grep -A 2 -B 2 "ERROR" log.txt # 2 lines after and before
# Multiple patterns
grep -e "error" -e "fail" log.txt
# Read patterns from file
grep -f patterns.txt input.txt
# Whole word match
grep -w "test" file.txt # Matches "test" but not "testing"
# Fixed string (not regex)
grep -F "a.b" file.txt # Matches literal "a.b", not regex
# Binary file handling
grep -a "pattern" binary_file # Treat binary as textgrep for Log Analysis
# Extract error messages from last hour
find /var/log -name "*.log" -mmin -60 -exec grep "ERROR" {} +
# Count errors by type
grep "ERROR" app.log | cut -d':' -f3 | sort | uniq -c | sort -rn
# Find errors excluding known issues
grep "ERROR" app.log | grep -v -f known_errors.txtawk: Field-Based Text Processing
When to Use awk
- Processing structured data (CSV, logs, tables)
- Extracting specific fields
- Performing calculations
- Generating reports
- Complex conditional logic on fields
awk Basics
# Print specific fields (space-delimited by default)
awk '{print $1, $3}' file.txt
# Custom delimiter
awk -F',' '{print $1, $3}' data.csv
awk -F':' '{print $1}' /etc/passwd
# Multiple delimiters
awk -F'[,:]' '{print $1}' file.txt
# Print last field
awk '{print $NF}' file.txt
# Print all but first field
awk '{$1=""; print $0}' file.txtawk Conditionals
# Print lines where field 3 > 100
awk '$3 > 100' data.txt
# Print lines where field matches pattern
awk '$2 ~ /error/' log.txt
# Print lines where field does NOT match
awk '$2 !~ /DEBUG/' log.txt
# Multiple conditions
awk '$3 > 100 && $4 < 500' data.txt
# If-else logic
awk '{if ($3 > 100) print "High:", $0; else print "Low:", $0}' data.txtawk Calculations
# Sum values in column 3
awk '{sum += $3} END {print sum}' numbers.txt
# Average
awk '{sum += $3; count++} END {print sum/count}' numbers.txt
# Find max value
awk 'BEGIN {max=0} {if ($1 > max) max=$1} END {print max}' numbers.txt
# Count lines matching condition
awk '$3 > 100 {count++} END {print count}' data.txtawk Formatted Output
# Printf-style formatting
awk '{printf "Name: %-20s Age: %3d\n", $1, $2}' people.txt
# Tab-separated output
awk 'BEGIN {OFS="\t"} {print $1, $2, $3}' file.txt
# Custom output formatting
awk '{printf "%s: %10.2f\n", $1, $2}' data.txtawk Built-in Variables
NF # Number of fields in current line
NR # Current line number
FNR # Line number in current file
FS # Input field separator
OFS # Output field separator
RS # Input record separator
ORS # Output record separator
FILENAME # Current filename
# Examples
awk '{print NR, NF, $0}' file.txt # Line number, field count, full line
awk 'NR==10' file.txt # Print line 10
awk 'NF > 5' file.txt # Lines with more than 5 fieldsawk for Log Analysis
# Apache/Nginx access log analysis
# Extract status codes and count
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# Summarize traffic by IP
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn
# Calculate average response time (field 11)
awk '{sum += $11; count++} END {print sum/count}' access.log
# Extract requests by hour
awk '{print substr($4, 2, 14)}' access.log | uniq -csed: Stream Editing
When to Use sed
- Find and replace operations
- Deleting specific lines
- In-place file editing
- Simple transformations
sed Substitution
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt
# Global substitution (all occurrences)
sed 's/old/new/g' file.txt
# Case-insensitive substitution
sed 's/old/new/gi' file.txt
# In-place editing
sed -i 's/old/new/g' file.txt
# Backup before in-place edit
sed -i.bak 's/old/new/g' file.txt
# Replace only on specific line
sed '5s/old/new/' file.txt
# Replace on lines matching pattern
sed '/ERROR/s/old/new/g' file.txt
# Use different delimiter
sed 's|/usr/local|/opt|g' file.txt
# Backreferences
sed 's/\([0-9]*\)-\([0-9]*\)/\2-\1/' file.txt
# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txtsed Deletion
# Delete specific line
sed '5d' file.txt
# Delete range of lines
sed '5,10d' file.txt
# Delete lines matching pattern
sed '/pattern/d' file.txt
# Delete empty lines
sed '/^$/d' file.txt
# Delete lines NOT matching pattern
sed '/pattern/!d' file.txtsed Line Operations
# Print specific line
sed -n '10p' file.txt
# Print range
sed -n '10,20p' file.txt
# Print lines matching pattern
sed -n '/ERROR/p' file.txt
# Insert line before match
sed '/pattern/i\New line before' file.txt
# Append line after match
sed '/pattern/a\New line after' file.txt
# Change entire line
sed '/pattern/c\Replacement line' file.txtsed Advanced Patterns
# Remove comments
sed 's/#.*//' file.txt
# Remove leading whitespace
sed 's/^[ \t]*//' file.txt
# Remove trailing whitespace
sed 's/[ \t]*$//' file.txt
# Remove HTML tags
sed 's/<[^>]*>//g' file.html
# Extract text between delimiters
sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' file.htmlCombining Tools: Pipeline Patterns
grep + awk
# Filter then extract fields
grep "ERROR" log.txt | awk '{print $1, $5}'
# Filter multiple patterns, process
grep -E "ERROR|WARN" log.txt | awk '{count[$2]++} END {for (i in count) print i, count[i]}'sed + awk
# Clean then process
sed 's/[^[:print:]]//g' data.txt | awk '{sum += $2} END {print sum}'
# Remove comments, extract fields
sed 's/#.*//' config.txt | awk -F'=' '{print $1}'Complete Pipeline Example
# Analyze web server logs
cat access.log \
| grep "GET" \
| grep -v "robot" \
| sed 's/.*HTTP\/[0-9.]*" //' \
| awk '$1 >= 200 && $1 < 300 {success++} $1 >= 400 {fail++} END {print "Success:", success, "Fail:", fail}'Performance Tips
grep Performance
# Use -F for fixed strings (faster than regex)
grep -F "literal.string" large_file.txt
# Use -m to stop after N matches
grep -m 10 "pattern" large_file.txt
# Parallel grep for large files
parallel -j4 grep "pattern" ::: chunk1 chunk2 chunk3 chunk4awk Performance
# Exit early if possible
awk '{if (condition) {print; exit}}' large_file.txt
# Process only needed lines
awk 'NR > 1000 {exit} {process}' large_file.txt
# Use built-in functions efficiently
awk '{count[$1]++} END {for (i in count) print i, count[i]}' file.txtsed Performance
# Minimize patterns
sed -e 's/a/b/g' -e 's/c/d/g' file.txt # Better than multiple sed calls
# Use in-place editing for large files
sed -i 's/old/new/g' large_file.txt # Avoids loading entire fileAvoid Useless Use of cat
# Bad
cat file.txt | grep "pattern"
cat file.txt | awk '{print $1}'
cat file.txt | sed 's/old/new/g'
# Good
grep "pattern" file.txt
awk '{print $1}' file.txt
sed 's/old/new/g' file.txtReal-World Examples
Example 1: CSV Processing
# Extract specific columns from CSV
awk -F',' '{print $1, $3, $5}' data.csv
# Filter rows by value
awk -F',' '$3 > 1000 {print $0}' data.csv
# Calculate sum per category
awk -F',' '{sum[$1] += $3} END {for (cat in sum) print cat, sum[cat]}' sales.csvExample 2: Log Analysis
# Error rate over time
grep "ERROR" app.log \
| awk '{print $1}' \
| uniq -c \
| awk '{print $2, $1}'
# Top 10 error messages
grep "ERROR" app.log \
| sed 's/.*ERROR: //' \
| sort \
| uniq -c \
| sort -rn \
| head -10Example 3: Configuration File Processing
# Extract non-comment, non-empty lines
sed -e 's/#.*//' -e '/^$/d' config.txt
# Convert KEY=VALUE to JSON
awk -F'=' 'BEGIN {print "{"} {printf " \"%s\": \"%s\",\n", $1, $2} END {print "}"}' config.txt---
References
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat << EOF
Usage: ${0##*/} [OPTIONS] LOG_FILE
Analyze log files and generate summary reports
Options:
-h Show this help
-t TYPE Report type: errors|summary (default: summary)
-o FILE Output file (default: stdout)
Examples:
${0##*/} application.log
${0##*/} -t errors -o errors.txt application.log
EOF
}
analyze_errors() {
local log_file="$1"
echo "Error Summary:"
echo "=============="
grep "ERROR" "${log_file}" \
| sed 's/.*ERROR: //' \
| sed 's/ -.*//' \
| sort \
| uniq -c \
| sort -rn \
| awk '{count=$1; $1=""; sub(/^ /,""); printf " %-40s %6d\n", $0, count}'
echo ""
echo "Total errors: $(grep -c "ERROR" "${log_file}" 2>/dev/null || true)"
}
generate_summary() {
local log_file="$1"
echo "Log File Analysis Summary"
echo "========================="
echo ""
echo "File: ${log_file}"
echo "Total lines: $(wc -l < "${log_file}")"
echo ""
echo "Log Levels:"
for level in DEBUG INFO WARN ERROR FATAL; do
local count
count=$(grep -c "${level}" "${log_file}" 2>/dev/null || true)
printf " %-10s %6d\n" "${level}:" "${count}"
done
}
main() {
local report_type="summary"
local output_file=""
local log_file=""
while getopts ":ht:o:" opt; do
case ${opt} in
h) usage; exit 0 ;;
t) report_type="${OPTARG}" ;;
o) output_file="${OPTARG}" ;;
:) echo "Option -${OPTARG} requires an argument" >&2; exit 1 ;;
\?) echo "Invalid option: -${OPTARG}" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
log_file="${1:-}"
[[ -n "${log_file}" ]] || { echo "Error: LOG_FILE required" >&2; usage; exit 1; }
[[ -f "${log_file}" ]] || { echo "Error: File not found: ${log_file}" >&2; exit 1; }
local output
case "${report_type}" in
errors) output=$(analyze_errors "${log_file}") ;;
summary) output=$(generate_summary "${log_file}") ;;
*) echo "Invalid report type: ${report_type}" >&2; exit 1 ;;
esac
if [[ -n "${output_file}" ]]; then
echo "${output}" > "${output_file}"
echo "Report saved to: ${output_file}"
else
echo "${output}"
fi
}
main "$@"
#!/usr/bin/env bash
#
# Generate bash script templates
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly TEMPLATES_DIR="${SCRIPT_DIR}/../assets/templates"
list_templates() {
local found=false
for template_path in "${TEMPLATES_DIR}"/*-template.sh; do
if [[ -f "${template_path}" ]]; then
found=true
basename "${template_path}" | sed 's/-template.sh$//'
fi
done
if [[ "${found}" == "false" ]]; then
echo "(none)"
fi
}
default_output_file() {
local template_type="$1"
echo "./${template_type}-script.sh"
}
validate_template_type() {
local value="$1"
# Block traversal payloads and path separators explicitly.
if [[ "$value" == *"/"* || "$value" == *".."* ]]; then
echo "Error: Invalid TEMPLATE_TYPE: ${value}" >&2
echo "Allowed characters: letters, digits, '_' and '-'" >&2
return 1
fi
if [[ ! "$value" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Invalid TEMPLATE_TYPE: ${value}" >&2
echo "Allowed characters: letters, digits, '_' and '-'" >&2
return 1
fi
}
usage() {
cat << EOF
Usage: ${0##*/} TEMPLATE_TYPE [OUTPUT_FILE]
Generate a bash script from a template.
Templates include:
- Proper shebang and strict mode (set -euo pipefail)
- Logging functions (debug, info, warn, error)
- Error handling (die, check_command, validate_file)
- Argument parsing with getopts
- Cleanup trap handlers
- Usage documentation
Examples:
${0##*/} standard
${0##*/} standard myscript.sh
${0##*/} standard /usr/local/bin/deploy.sh
Available templates:
$(list_templates)
EOF
}
main() {
local template_type
local output_file
local template_file
local output_dir
if [[ $# -eq 0 ]]; then
echo "Error: TEMPLATE_TYPE is required" >&2
usage
exit 1
fi
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -gt 2 ]]; then
echo "Error: Too many arguments" >&2
usage
exit 1
fi
template_type="$1"
validate_template_type "$template_type" || exit 1
output_file="${2:-$(default_output_file "${template_type}")}"
template_file="${TEMPLATES_DIR}/${template_type}-template.sh"
if [[ ! -f "${template_file}" ]]; then
echo "Error: Template not found: ${template_type}" >&2
echo "Available templates:" >&2
while IFS= read -r template_name; do
echo " ${template_name}" >&2
done < <(list_templates)
exit 1
fi
output_dir="$(dirname "${output_file}")"
if [[ "${output_dir}" != "." ]]; then
mkdir -p "${output_dir}"
fi
cp "${template_file}" "${output_file}"
chmod u+x "${output_file}"
echo "Created script: ${output_file}"
echo "Template: ${template_type}"
echo "Source: ${template_file}"
}
main "$@"
#!/usr/bin/env bash
#
# Deterministic CI validation entrypoint for bash-script-generator.
# Runs:
# 1) bash -n syntax checks
# 2) shellcheck -x checks (optional or required)
# 3) regression tests (scripts/test_generator.sh)
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
readonly TEST_RUNNER="${SCRIPT_DIR}/test_generator.sh"
usage() {
cat <<EOF
Usage: ${0##*/} [OPTIONS]
Run deterministic checks for bash-script-generator.
Options:
--require-shellcheck Fail if shellcheck is unavailable.
--skip-shellcheck Skip shellcheck stage.
--skip-regression-tests Skip scripts/test_generator.sh.
-h, --help Show this help message.
Environment:
SHELLCHECK_BIN Override shellcheck binary/path (default: shellcheck)
CI=true|1 Defaults to --require-shellcheck unless explicitly overridden
EOF
}
is_true() {
local value="$1"
[[ "$value" == "true" || "$value" == "1" ]]
}
command_is_available() {
local command_name="$1"
if [[ "$command_name" == */* ]]; then
[[ -x "$command_name" ]]
else
command -v "$command_name" >/dev/null 2>&1
fi
}
collect_shell_files() {
SHELL_FILES=()
while IFS= read -r file; do
SHELL_FILES+=("$file")
done < <(
{
find "${SCRIPT_DIR}" -maxdepth 1 -type f -name "*.sh"
find "${SKILL_DIR}/examples" -maxdepth 1 -type f -name "*.sh"
find "${SKILL_DIR}/assets/templates" -maxdepth 1 -type f -name "*.sh"
} | LC_ALL=C sort
)
if [[ "${#SHELL_FILES[@]}" -eq 0 ]]; then
echo "Error: No shell files found to validate." >&2
return 1
fi
}
run_syntax_checks() {
echo "[1/3] bash -n syntax checks"
local shell_file
for shell_file in "${SHELL_FILES[@]}"; do
bash -n "${shell_file}"
done
echo " PASS: bash -n succeeded for ${#SHELL_FILES[@]} file(s)"
}
run_shellcheck_checks() {
local shellcheck_bin="$1"
local require_shellcheck="$2"
local skip_shellcheck="$3"
if [[ "$skip_shellcheck" -eq 1 ]]; then
echo "[2/3] shellcheck -x checks"
echo " SKIP: shellcheck stage disabled via --skip-shellcheck"
return 0
fi
echo "[2/3] shellcheck -x checks"
if ! command_is_available "$shellcheck_bin"; then
if [[ "$require_shellcheck" -eq 1 ]]; then
echo "Error: shellcheck is required but not available (${shellcheck_bin})." >&2
return 1
fi
echo " SKIP: shellcheck unavailable (${shellcheck_bin}); continuing without shellcheck"
return 0
fi
local shell_file
for shell_file in "${SHELL_FILES[@]}"; do
"$shellcheck_bin" -x "${shell_file}"
done
echo " PASS: shellcheck succeeded for ${#SHELL_FILES[@]} file(s)"
}
run_regression_tests() {
local skip_regression_tests="$1"
echo "[3/3] regression tests"
if [[ "$skip_regression_tests" -eq 1 ]]; then
echo " SKIP: regression suite disabled via --skip-regression-tests"
return 0
fi
bash "${TEST_RUNNER}"
}
main() {
local require_shellcheck=0
local skip_shellcheck=0
local skip_regression_tests=0
local shellcheck_mode_overridden=0
local shellcheck_bin="${SHELLCHECK_BIN:-shellcheck}"
while [[ $# -gt 0 ]]; do
case "$1" in
--require-shellcheck)
require_shellcheck=1
skip_shellcheck=0
shellcheck_mode_overridden=1
;;
--skip-shellcheck)
skip_shellcheck=1
require_shellcheck=0
shellcheck_mode_overridden=1
;;
--skip-regression-tests)
skip_regression_tests=1
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
usage
exit 1
;;
esac
shift
done
if [[ "$shellcheck_mode_overridden" -eq 0 ]] && is_true "${CI:-}"; then
require_shellcheck=1
fi
if [[ "$skip_shellcheck" -eq 1 && "$require_shellcheck" -eq 1 ]]; then
echo "Error: --skip-shellcheck and --require-shellcheck cannot be used together." >&2
exit 1
fi
export LC_ALL=C
export LANG=C
export TZ=UTC
collect_shell_files
run_syntax_checks
run_shellcheck_checks "$shellcheck_bin" "$require_shellcheck" "$skip_shellcheck"
run_regression_tests "$skip_regression_tests"
echo ""
echo "All configured CI checks passed."
}
main "$@"
#!/usr/bin/env bash
#
# Regression test suite for bash-script-generator
#
# Tests:
# 1. generate_script_template.sh — argument handling and file generation
# 2. log-analyzer.sh — functional behaviour
# 3. run_ci_checks.sh — deterministic validation wiring
#
# Exit 0 when all assertions pass; non-zero on any failure.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
GENERATOR="$SCRIPT_DIR/generate_script_template.sh"
CI_RUNNER="$SCRIPT_DIR/run_ci_checks.sh"
LOG_ANALYZER="$SCRIPT_DIR/../examples/log-analyzer.sh"
PASS=0
FAIL=0
# ─── helpers ────────────────────────────────────────────────────────────────
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Run a command silently and return its exit code without aborting this script.
run_exit_code() {
local exit_code=0
"$@" >/dev/null 2>&1 || exit_code=$?
echo "$exit_code"
}
assert_exit_code() {
local label="$1"
local expected="$2"
shift 2
local actual
actual=$(run_exit_code "$@")
if [[ "$actual" -eq "$expected" ]]; then
pass "$label (exit $actual)"
else
fail "$label — expected exit $expected, got $actual"
echo " --- command output ---"
"$@" 2>&1 | sed 's/^/ /' || true
echo " --- end output ---"
fi
}
# Assert that a pattern IS present in the combined stdout+stderr of a command.
assert_output_contains() {
local label="$1"
local pattern="$2"
shift 2
local output
output=$("$@" 2>&1 || true)
if echo "$output" | grep -qE "$pattern"; then
pass "$label"
else
fail "$label — pattern not found: $pattern"
echo " --- command output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
fi
}
# Assert that a pattern is NOT present in the combined stdout+stderr of a command.
assert_output_not_contains() {
local label="$1"
local pattern="$2"
shift 2
local output
output=$("$@" 2>&1 || true)
if echo "$output" | grep -qE "$pattern"; then
fail "$label — unexpected pattern found: $pattern"
echo " --- command output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
else
pass "$label"
fi
}
# ─── setup ──────────────────────────────────────────────────────────────────
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "${TEMP_DIR}"' EXIT
# Fixture log file used by log-analyzer tests
LOG_FILE="${TEMP_DIR}/app.log"
cat > "$LOG_FILE" << 'EOF'
2024-01-15 10:00:01 INFO: App started
2024-01-15 10:00:02 DEBUG: Loading config
2024-01-15 10:00:03 ERROR: Connection refused to database
2024-01-15 10:00:04 ERROR: Connection refused to database
2024-01-15 10:00:05 WARN: Retry attempt
2024-01-15 10:00:06 ERROR: Timeout on upstream request
2024-01-15 10:00:07 INFO: Request complete
2024-01-15 10:00:08 FATAL: Critical failure
EOF
echo "Running bash-script-generator tests..."
# ─── generate_script_template.sh: argument handling ─────────────────────────
echo ""
echo "[generate_script_template.sh — argument handling]"
assert_exit_code \
"no args exits non-zero" \
1 \
bash "$GENERATOR"
assert_exit_code \
"--help exits 0" \
0 \
bash "$GENERATOR" --help
assert_exit_code \
"-h exits 0" \
0 \
bash "$GENERATOR" -h
assert_exit_code \
"unknown template exits non-zero" \
1 \
bash "$GENERATOR" nonexistent-template
assert_exit_code \
"too many args exits non-zero" \
1 \
bash "$GENERATOR" standard out.sh extra-arg
assert_exit_code \
"traversal template_type payload is rejected" \
1 \
bash "$GENERATOR" ../templates/standard
assert_output_contains \
"traversal payload shows invalid template_type error" \
"Invalid TEMPLATE_TYPE" \
bash "$GENERATOR" ../templates/standard
assert_exit_code \
"slash and dot traversal payload is rejected" \
1 \
bash "$GENERATOR" standard/../../evil
assert_output_contains \
"slash and dot traversal payload shows invalid template_type error" \
"Invalid TEMPLATE_TYPE" \
bash "$GENERATOR" standard/../../evil
assert_output_contains \
"no-args error mentions TEMPLATE_TYPE" \
"TEMPLATE_TYPE" \
bash "$GENERATOR"
assert_output_contains \
"unknown template error lists available templates" \
"Available templates" \
bash "$GENERATOR" nonexistent-template
# ─── generate_script_template.sh: file generation ───────────────────────────
echo ""
echo "[generate_script_template.sh — file generation]"
OUTPUT="${TEMP_DIR}/generated.sh"
assert_exit_code \
"standard template with explicit output exits 0" \
0 \
bash "$GENERATOR" standard "$OUTPUT"
if [[ -f "$OUTPUT" ]]; then
pass "output file was created"
else
fail "output file was not created"
fi
if [[ -x "$OUTPUT" ]]; then
pass "output file is executable (chmod u+x applied)"
else
fail "output file is not executable"
fi
assert_output_contains \
"generated file has bash shebang" \
"#!/usr/bin/env bash" \
cat "$OUTPUT"
assert_output_contains \
"generated file has strict mode" \
"set -euo pipefail" \
cat "$OUTPUT"
assert_output_contains \
"generated file has numeric LOG_LEVEL" \
"LOG_LEVEL=1" \
cat "$OUTPUT"
# Syntax check: the generated script must be parseable by bash
if bash -n "$OUTPUT" 2>/dev/null; then
pass "generated file passes bash -n syntax check"
else
fail "generated file has syntax errors"
fi
# Overwrite: generating into an existing path must succeed silently
assert_exit_code \
"overwriting existing output file exits 0" \
0 \
bash "$GENERATOR" standard "$OUTPUT"
# Nested directories must be created automatically
NESTED="${TEMP_DIR}/a/b/c/nested.sh"
assert_exit_code \
"output into non-existent nested directory exits 0" \
0 \
bash "$GENERATOR" standard "$NESTED"
if [[ -f "$NESTED" ]]; then
pass "nested output file was created"
else
fail "nested output file was not created"
fi
# Default output path: when no OUTPUT_FILE is given the file lands in CWD
ORIG_DIR="$(pwd)"
WORK_DIR="${TEMP_DIR}/workspace"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
assert_exit_code \
"default output path (no output arg) exits 0" \
0 \
bash "$GENERATOR" standard
if [[ -f "${WORK_DIR}/standard-script.sh" ]]; then
pass "default output file ./standard-script.sh was created"
else
fail "default output file ./standard-script.sh was not created"
fi
TRAVERSAL_TARGET="${TEMP_DIR}/templates/standard-script.sh"
rm -rf "${TEMP_DIR}/templates"
assert_exit_code \
"traversal payload with default output exits non-zero" \
1 \
bash "$GENERATOR" ../templates/standard
if [[ ! -e "$TRAVERSAL_TARGET" ]]; then
pass "traversal payload did not create file outside current directory"
else
fail "traversal payload created unexpected file: $TRAVERSAL_TARGET"
fi
cd "$ORIG_DIR"
# ─── log-analyzer.sh: argument handling ─────────────────────────────────────
echo ""
echo "[log-analyzer.sh — argument handling]"
assert_exit_code \
"no file arg exits non-zero" \
1 \
bash "$LOG_ANALYZER"
assert_exit_code \
"-h exits 0" \
0 \
bash "$LOG_ANALYZER" -h
assert_exit_code \
"nonexistent file exits non-zero" \
1 \
bash "$LOG_ANALYZER" /nonexistent/file.log
assert_exit_code \
"invalid report type exits non-zero" \
1 \
bash "$LOG_ANALYZER" -t badtype "$LOG_FILE"
# ─── log-analyzer.sh: functional behaviour ──────────────────────────────────
echo ""
echo "[log-analyzer.sh — functional behaviour]"
assert_exit_code \
"summary report exits 0" \
0 \
bash "$LOG_ANALYZER" "$LOG_FILE"
assert_output_contains \
"summary report shows total lines" \
"Total lines" \
bash "$LOG_ANALYZER" "$LOG_FILE"
assert_output_contains \
"summary shows ERROR count" \
"ERROR" \
bash "$LOG_ANALYZER" "$LOG_FILE"
assert_output_contains \
"summary shows FATAL count" \
"FATAL" \
bash "$LOG_ANALYZER" "$LOG_FILE"
assert_exit_code \
"errors report exits 0" \
0 \
bash "$LOG_ANALYZER" -t errors "$LOG_FILE"
assert_output_contains \
"errors report shows total errors" \
"Total errors" \
bash "$LOG_ANALYZER" -t errors "$LOG_FILE"
# Multi-word error messages must not be truncated
assert_output_contains \
"errors report preserves multi-word message (Connection refused to database)" \
"Connection refused to database" \
bash "$LOG_ANALYZER" -t errors "$LOG_FILE"
assert_output_contains \
"errors report preserves multi-word message (Timeout on upstream request)" \
"Timeout on upstream request" \
bash "$LOG_ANALYZER" -t errors "$LOG_FILE"
# Repeated errors should show correct count (2 for "Connection refused to database")
assert_output_contains \
"errors report shows count 2 for repeated error" \
"Connection refused to database.*2" \
bash "$LOG_ANALYZER" -t errors "$LOG_FILE"
# Output to file
REPORT="${TEMP_DIR}/report.txt"
assert_exit_code \
"output-to-file exits 0" \
0 \
bash "$LOG_ANALYZER" -o "$REPORT" "$LOG_FILE"
if [[ -f "$REPORT" ]]; then
pass "report file was created by -o flag"
else
fail "report file was not created by -o flag"
fi
assert_output_contains \
"-o flag prints confirmation message to stdout" \
"Report saved to" \
bash "$LOG_ANALYZER" -o "${TEMP_DIR}/report2.txt" "$LOG_FILE"
assert_output_not_contains \
"report file content excludes confirmation message" \
"Report saved to" \
cat "$REPORT"
# ─── run_ci_checks.sh: determinism and shellcheck gating ───────────────────
echo ""
echo "[run_ci_checks.sh — deterministic validation]"
SHELLCHECK_STUB="${TEMP_DIR}/shellcheck-stub.sh"
cat > "$SHELLCHECK_STUB" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
chmod +x "$SHELLCHECK_STUB"
assert_exit_code \
"ci runner --help exits 0" \
0 \
bash "$CI_RUNNER" --help
assert_exit_code \
"ci runner succeeds when shellcheck is required and stubbed" \
0 \
env SHELLCHECK_BIN="$SHELLCHECK_STUB" \
bash "$CI_RUNNER" --require-shellcheck --skip-regression-tests
assert_exit_code \
"ci runner fails when required shellcheck is unavailable" \
1 \
env SHELLCHECK_BIN="${TEMP_DIR}/missing-shellcheck" \
bash "$CI_RUNNER" --require-shellcheck --skip-regression-tests
assert_output_contains \
"required-shellcheck failure message is explicit" \
"shellcheck is required but not available" \
env SHELLCHECK_BIN="${TEMP_DIR}/missing-shellcheck" \
bash "$CI_RUNNER" --require-shellcheck --skip-regression-tests
assert_exit_code \
"CI=true defaults to requiring shellcheck" \
1 \
env CI=true SHELLCHECK_BIN="${TEMP_DIR}/missing-shellcheck" \
bash "$CI_RUNNER" --skip-regression-tests
# ─── summary ────────────────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
echo ""
if [[ $FAIL -gt 0 ]]; then
exit 1
fi
Related skills
How it compares
Use bash-script-generator to author new shell automation; pair with bash-script-validator when reviewing scripts you did not generate here.
FAQ
Does bash-script-generator validate its output?
bash-script-generator defaults to invoking devops-skills:bash-script-validator, running bash -n and ShellCheck when available, then iterating until checks pass. Validation is a mandatory stage in the documented six-step generation workflow.
What scripts can bash-script-generator create?
bash-script-generator covers deployment hooks, cron jobs, CI utilities, log analysis pipelines, and API client shell tools. It enforces requirement clarification tables before choosing templates for text processing or systems administration tasks.