bash
Guide for writing production-quality bash scripts following modern idiomatic practices. Enforces set -euo pipefail, [[ ]] conditionals, ${var} syntax, ShellCheck compliance. Triggers on "bash script", "shell script", ".sh file", "write a script", "automation script", "bash function", "shellcheck", "bash template", "pre-commit hook", "deploy script", "build script", "install script", "setup script", "bash error handling", "bash arrays", "bash loop", "bash conditional", "parse arguments", "getopts", "bash logging", "#!/bin/bash", "source script", "dot script", "shell function", "edit script", "update script", "modify script", "change script", "edit .sh", "update .sh", "modify .sh", "statusline.sh", "hook script". PROACTIVE: MUST invoke BEFORE editing/writing ANY .sh file or pre-commit hook.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install mauromedda-agent-toolkit-bash
Repository
Skill path: skills/bash
Guide for writing production-quality bash scripts following modern idiomatic practices. Enforces set -euo pipefail, [[ ]] conditionals, ${var} syntax, ShellCheck compliance. Triggers on "bash script", "shell script", ".sh file", "write a script", "automation script", "bash function", "shellcheck", "bash template", "pre-commit hook", "deploy script", "build script", "install script", "setup script", "bash error handling", "bash arrays", "bash loop", "bash conditional", "parse arguments", "getopts", "bash logging", "#!/bin/bash", "source script", "dot script", "shell function", "edit script", "update script", "modify script", "change script", "edit .sh", "update .sh", "modify .sh", "statusline.sh", "hook script". PROACTIVE: MUST invoke BEFORE editing/writing ANY .sh file or pre-commit hook.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, DevOps, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: mauromedda.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install bash into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/mauromedda/agent-toolkit before adding bash to shared team environments
- Use bash for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: bash
description: >-
Guide for writing production-quality bash scripts following modern idiomatic practices.
Enforces set -euo pipefail, [[ ]] conditionals, ${var} syntax, ShellCheck compliance.
Triggers on "bash script", "shell script", ".sh file", "write a script", "automation script",
"bash function", "shellcheck", "bash template", "pre-commit hook", "deploy script",
"build script", "install script", "setup script", "bash error handling", "bash arrays",
"bash loop", "bash conditional", "parse arguments", "getopts", "bash logging",
"#!/bin/bash", "source script", "dot script", "shell function",
"edit script", "update script", "modify script", "change script",
"edit .sh", "update .sh", "modify .sh", "statusline.sh", "hook script".
PROACTIVE: MUST invoke BEFORE editing/writing ANY .sh file or pre-commit hook.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---
# ABOUTME: Bash scripting skill for production-quality scripts with Bash 4.x+
# ABOUTME: Emphasizes safety, readability, maintainability, and mandatory ShellCheck compliance
# Bash Scripting Skill
**Target**: Bash 4.0+ with mandatory ShellCheck compliance.
**Detailed patterns**: See `references/script-template.md` and `references/advanced-patterns.md`
---
## š FILE OPERATION CHECKPOINT (BLOCKING)
**Before EVERY `Write` or `Edit` tool call on a `.sh` file or shell script:**
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā š STOP - BASH SKILL CHECK ā
ā ā
ā You are about to modify a shell script. ā
ā ā
ā QUESTION: Is /bash skill currently active? ā
ā ā
ā If YES ā Proceed with the edit ā
ā If NO ā STOP! Invoke /bash FIRST, then edit ā
ā ā
ā This check applies to: ā
ā ā Write tool with file_path ending in .sh ā
ā ā Edit tool with file_path ending in .sh ā
ā ā Files named "pre-commit", "post-commit", etc. (git hooks) ā
ā ā ANY shell script, regardless of conversation topic ā
ā ā
ā Examples that REQUIRE this skill: ā
ā - "update the statusline" (edits statusline.sh) ā
ā - "add a feature to the hook" (edits pre-commit) ā
ā - "fix the deploy script" (edits deploy.sh) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
**Why this matters:** Shell scripts without proper safety headers (`set -euo pipefail`)
can fail silently or cause data corruption. The skill ensures ShellCheck compliance.
---
## Quick Reference
| Pattern | Use | Avoid |
|---------|-----|-------|
| Conditionals | `[[ "${var}" == "x" ]]` | `[ $var == "x" ]` |
| Variables | `"${var}"` (quoted) | `$var` (unquoted) |
| Command sub | `$(command)` | `` `command` `` |
| Arithmetic | `(( count++ ))` | `let count++` |
| Error handling | `set -euo pipefail` | No safety flags |
---
## When to Use Bash
**USE for** (< 200 lines):
- Quick automation tasks
- Build/deployment scripts
- System administration
- Glue code between tools
**DON'T USE for**:
- Complex business logic
- Robust error handling with recovery
- Long-running services
- Code requiring unit testing
If script exceeds ~200 lines, consider Python or Go.
---
## š RESUMED SESSION CHECKPOINT
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SESSION RESUMED - BASH SKILL VERIFICATION ā
ā ā
ā Before continuing: ā
ā 1. Does script have set -euo pipefail? ā
ā 2. Are all variables quoted: "${var}"? ā
ā 3. Using [[ ]] conditionals (not [ ])? ā
ā 4. Run: shellcheck <script>.sh ā
ā 5. Re-invoke /bash if skill context was lost ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
---
## Mandatory Header
Every script MUST start with:
```bash
#!/bin/bash
set -euo pipefail
# -e: Exit on error
# -u: Error on unset variables
# -o pipefail: Pipeline fails on first error
```
---
## Core Syntax Rules
### Conditionals (Always `[[ ]]`)
```bash
if [[ "${var}" =~ ^[0-9]+$ ]]; then
echo "Numeric"
fi
if [[ -f "${file}" && -r "${file}" ]]; then
echo "File exists and readable"
fi
```
### Variable Expansion (Always quoted)
```bash
echo "Value: ${config_path}"
echo "Array: ${my_array[@]}"
```
### Command Substitution
```bash
current_date=$(date +"%Y-%m-%d")
file_count=$(find . -type f | wc -l)
```
### Arithmetic
```bash
(( count++ ))
(( total = value1 + value2 ))
if (( count > 10 )); then
echo "Exceeded"
fi
```
---
## Error Handling Patterns
### Pattern 1: Need output AND status
```bash
output=$(complex_command 2>&1)
rc=$?
if [[ ${rc} -ne 0 ]]; then
log_error "Failed: ${output}"
return 1
fi
```
### Pattern 2: Status check only
```bash
if ! simple_command --flag; then
die "Command failed"
fi
# Or short form
simple_command || die "Failed"
```
---
## Essential Functions
```bash
# Logging
log_info() { echo "[INFO] $(date +%H:%M:%S) ${*}" >&2; }
log_error() { echo "[ERROR] $(date +%H:%M:%S) ${*}" >&2; }
die() { log_error "${*}"; exit 1; }
# Validation
validate_file() {
local -r f="${1}"
[[ -f "${f}" ]] || die "Not found: ${f}"
[[ -r "${f}" ]] || die "Not readable: ${f}"
}
# Cleanup
cleanup() {
[[ -d "${TEMP_DIR:-}" ]] && rm -rf "${TEMP_DIR}"
}
trap cleanup EXIT
```
---
## Argument Parsing
```bash
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "${1}" in
-h|--help) help; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-f|--file)
[[ -z "${2:-}" ]] && die "-f requires argument"
FILE="${2}"; shift 2
;;
-*) die "Unknown: ${1}" ;;
*) break ;;
esac
done
ARGS=("${@}")
}
```
---
## ShellCheck Compliance
**All scripts MUST pass ShellCheck with zero warnings.**
### Common Fixes
| Warning | Fix |
|---------|-----|
| SC2086: Quote to prevent globbing | `rm "${file}"` not `rm $file` |
| SC2155: Declare separately | `local x; x=$(cmd)` not `local x=$(cmd)` |
| SC1090: Can't follow source | `# shellcheck source=/dev/null` |
| SC2046: Quote command sub | Use `while read` instead of `for x in $(cmd)` |
### Run ShellCheck
```bash
shellcheck script.sh
shellcheck -s bash script.sh
find . -name "*.sh" -exec shellcheck {} +
```
---
## Arrays Quick Reference
### Indexed Arrays
```bash
declare -a files=()
files+=("item")
for f in "${files[@]}"; do echo "${f}"; done
echo "Count: ${#files[@]}"
```
### Associative Arrays (Bash 4+)
```bash
declare -A config=()
config["key"]="value"
for k in "${!config[@]}"; do echo "${k}=${config[${k}]}"; done
```
---
## Security Rules
1. **Never use `eval`**
2. **Sanitize all inputs**
3. **Use absolute paths** for system commands
4. **Use `readonly`** for constants
5. **Don't store secrets** in scripts
---
## Checklist
Before script is complete:
- [ ] `set -euo pipefail` at top
- [ ] All variables use `"${var}"` syntax
- [ ] Uses `[[ ]]` for conditionals
- [ ] Uses `$(command)` for substitution
- [ ] ShellCheck passes (zero warnings)
- [ ] Has `help()` function
- [ ] Has cleanup trap
- [ ] Variables properly scoped (local/readonly)
- [ ] **If > 100 lines**: Has bats tests
---
## Full Template
See `references/script-template.md` for complete production script template.
## Advanced Patterns
See `references/advanced-patterns.md` for:
- Arrays and associative arrays
- String manipulation
- Parallel processing
- Progress indicators
- Input validation
- bats-core testing
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/script-template.md
```markdown
# ABOUTME: Full production bash script template with all sections
# ABOUTME: Copy and customize for new scripts
# Bash Script Template
## Standard Template
```bash
#!/bin/bash
# set -n # Uncomment for syntax check only (dry run)
# set -x # Uncomment for execution tracing (debugging)
#
# File: script_name.sh
# Author: [Your Name]
# Created: YYYY-MM-DD
# Revision: YYYY-MM-DD
# Purpose: [Brief one-line description]
# Usage: See help() function below
set -euo pipefail
# ============================================================================
# GLOBAL VARIABLES AND CONSTANTS
# ============================================================================
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly VERSION="1.0.0"
# Exit codes
declare -ri EXIT_SUCCESS=0
declare -ri EXIT_FAILURE=1
declare -ri EXIT_USAGE=2
# Default configuration
declare CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/.config}"
declare VERBOSE=false
declare DRY_RUN=false
# ============================================================================
# CONFIGURATION LOADING
# ============================================================================
load_config() {
if [[ -f "${CONFIG_FILE}" ]]; then
log_info "Loading configuration from: ${CONFIG_FILE}"
# shellcheck source=/dev/null
source "${CONFIG_FILE}"
else
log_debug "No configuration file found at: ${CONFIG_FILE}"
fi
}
# ============================================================================
# LOGGING FUNCTIONS
# ============================================================================
log_debug() {
if [[ "${VERBOSE}" == true ]]; then
echo "[DEBUG] $(date +"%Y-%m-%d %H:%M:%S") ${*}" >&2
fi
}
log_info() {
echo "[INFO] $(date +"%Y-%m-%d %H:%M:%S") ${*}" >&2
}
log_warn() {
echo "[WARN] $(date +"%Y-%m-%d %H:%M:%S") ${*}" >&2
}
log_error() {
echo "[ERROR] $(date +"%Y-%m-%d %H:%M:%S") ${*}" >&2
}
die() {
log_error "${*}"
exit "${EXIT_FAILURE}"
}
# ============================================================================
# HELP DOCUMENTATION
# ============================================================================
help() {
cat <<EOF
${SCRIPT_NAME} v${VERSION}
USAGE:
${SCRIPT_NAME} [OPTIONS] ARGUMENTS
DESCRIPTION:
[Detailed description of what the script does]
OPTIONS:
-h, --help Show this help message and exit
-v, --verbose Enable verbose/debug output
-V, --version Show version information
-c, --config FILE Use alternate configuration file
-n, --dry-run Perform a dry run without making changes
ARGUMENTS:
ARG1 Description of first argument
EXAMPLES:
${SCRIPT_NAME} input.txt
${SCRIPT_NAME} --verbose input.txt
EXIT CODES:
0 Success
1 General failure
2 Usage error
EOF
}
version() {
echo "${SCRIPT_NAME} version ${VERSION}"
}
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
check_dependencies() {
local -a required_commands=("jq" "curl")
local missing=false
for cmd in "${required_commands[@]}"; do
if ! command -v "${cmd}" &> /dev/null; then
log_error "Required command not found: ${cmd}"
missing=true
fi
done
if [[ "${missing}" == true ]]; then
die "Missing required dependencies."
fi
}
validate_file() {
local -r filepath="${1}"
[[ -f "${filepath}" ]] || die "File not found: ${filepath}"
[[ -r "${filepath}" ]] || die "File not readable: ${filepath}"
}
# ============================================================================
# CLEANUP AND SIGNAL HANDLING
# ============================================================================
cleanup() {
local -ri exit_code=$?
log_debug "Cleanup called with exit code: ${exit_code}"
if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
fi
exit "${exit_code}"
}
trap cleanup EXIT
trap 'die "Script interrupted"' INT TERM
# ============================================================================
# CORE BUSINESS LOGIC
# ============================================================================
process_input() {
local -r input="${1}"
log_info "Processing: ${input}"
# Implementation here
}
# ============================================================================
# ARGUMENT PARSING
# ============================================================================
parse_arguments() {
if [[ $# -eq 0 ]]; then
help
exit "${EXIT_USAGE}"
fi
while [[ $# -gt 0 ]]; do
case "${1}" in
-h|--help)
help
exit "${EXIT_SUCCESS}"
;;
-V|--version)
version
exit "${EXIT_SUCCESS}"
;;
-v|--verbose)
VERBOSE=true
shift
;;
-c|--config)
[[ -z "${2:-}" ]] && die "Option ${1} requires an argument"
CONFIG_FILE="${2}"
shift 2
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
-*)
die "Unknown option: ${1}"
;;
*)
break
;;
esac
done
export -a POSITIONAL_ARGS=("${@}")
}
# ============================================================================
# MAIN EXECUTION
# ============================================================================
main() {
log_info "Starting ${SCRIPT_NAME} v${VERSION}"
parse_arguments "${@}"
load_config
check_dependencies
if [[ ${#POSITIONAL_ARGS[@]} -lt 1 ]]; then
log_error "Missing required arguments"
help
exit "${EXIT_USAGE}"
fi
validate_file "${POSITIONAL_ARGS[0]}"
process_input "${POSITIONAL_ARGS[0]}"
log_info "Completed successfully"
}
# ============================================================================
# SCRIPT ENTRY POINT
# ============================================================================
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "${@}"
fi
```
```
### references/advanced-patterns.md
```markdown
# ABOUTME: Advanced bash patterns for arrays, strings, error handling, parallelism
# ABOUTME: Reference for complex scripting scenarios
# Advanced Bash Patterns
## Arrays
### Indexed Arrays (Bash 4+)
```bash
declare -a files=()
files+=("file1.txt")
files+=("file2.txt")
for file in "${files[@]}"; do
echo "Processing: ${file}"
done
echo "Count: ${#files[@]}"
echo "First two: ${files[@]:0:2}"
```
### Associative Arrays (Bash 4+)
```bash
declare -A config=()
config["host"]="localhost"
config["port"]="8080"
for key in "${!config[@]}"; do
echo "${key} = ${config[${key}]}"
done
# Check if key exists
if [[ -v config[host] ]]; then
echo "Host is configured"
fi
```
## String Manipulation
```bash
# Case conversion (Bash 4+)
declare -l lowercase_var="HELLO" # Stores as "hello"
declare -u uppercase_var="hello" # Stores as "HELLO"
text="hello world"
echo "${text^}" # Hello world (capitalize first)
echo "${text^^}" # HELLO WORLD (all uppercase)
# Pattern replacement
filename="test.tar.gz"
echo "${filename%.gz}" # test.tar (remove from end)
echo "${filename#test.}" # tar.gz (remove from start)
echo "${filename/test/demo}" # demo.tar.gz (replace first)
echo "${filename//t/T}" # TesT.Tar.gz (replace all)
```
## Advanced Error Handling
### Custom Exit Codes
```bash
declare -ri ERR_FILE_NOT_FOUND=10
declare -ri ERR_PERMISSION_DENIED=11
declare -ri ERR_NETWORK_FAILURE=12
handle_error() {
local -r error_code="${1}"
local -r error_msg="${2}"
log_error "${error_msg}"
case "${error_code}" in
"${ERR_FILE_NOT_FOUND}") log_error "Check path and try again." ;;
"${ERR_PERMISSION_DENIED}") log_error "Run with appropriate privileges." ;;
"${ERR_NETWORK_FAILURE}") log_error "Check connectivity and retry." ;;
esac
exit "${error_code}"
}
```
### Error Stack Traces
```bash
error_exit() {
local -r msg="${1}"
local -r line="${2}"
local -r func="${3}"
log_error "Error in '${func}' at line ${line}: ${msg}"
local -i frame=0
while caller ${frame}; do
(( frame++ ))
done
exit "${EXIT_FAILURE}"
}
trap 'error_exit "Command failed" "${LINENO}" "${FUNCNAME[0]}"' ERR
```
## Parallel Processing
```bash
process_files_parallel() {
local -r -a files=("${@}")
local -r max_jobs=4
local -i job_count=0
for file in "${files[@]}"; do
process_single_file "${file}" &
(( job_count++ ))
if (( job_count >= max_jobs )); then
wait -n
(( job_count-- ))
fi
done
wait
}
```
## Temporary File Management
```bash
setup_temp_directory() {
TEMP_DIR=$(mktemp -d -t "$(basename "${0}").XXXXXXXXXX")
readonly TEMP_DIR
}
create_temp_file() {
local -r prefix="${1:-temp}"
mktemp "${TEMP_DIR}/${prefix}.XXXXXXXXXX"
}
```
## Progress Indicators
```bash
show_progress() {
local -r current="${1}"
local -r total="${2}"
local -r task="${3:-Processing}"
local -i percent=$(( (current * 100) / total ))
local -i filled=$(( (current * 50) / total ))
printf "\r%s: [%-50s] %d%%" "${task}" \
"$(printf '%*s' "${filled}" '' | tr ' ' '=')" "${percent}"
(( current == total )) && echo ""
}
```
## Input Validation
```bash
is_integer() {
[[ "${1}" =~ ^-?[0-9]+$ ]]
}
is_positive_integer() {
[[ "${1}" =~ ^[0-9]+$ ]] && (( ${1} > 0 ))
}
is_valid_email() {
[[ "${1}" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}
is_valid_ip() {
local -a octets
IFS='.' read -r -a octets <<< "${1}"
[[ ${#octets[@]} -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
[[ "${octet}" =~ ^[0-9]+$ ]] || return 1
(( octet >= 0 && octet <= 255 )) || return 1
done
}
```
## Performance Tips
### Avoid Subprocess Spawning
```bash
# Bad: spawns external process
lines=$(cat file.txt | wc -l)
# Good: use mapfile (Bash 4+)
mapfile -t lines < file.txt
echo "Count: ${#lines[@]}"
```
### Efficient String Building
```bash
# Bad: repeated concatenation
result=""
for i in {1..1000}; do
result="${result}${i}\n"
done
# Good: use array then join
results=()
for i in {1..1000}; do
results+=("${i}")
done
result=$(IFS=$'\n'; echo "${results[*]}")
```
## Testing with bats-core
```bash
#!/usr/bin/env bats
setup() {
export TEST_DIR="$(mktemp -d)"
}
teardown() {
rm -rf "${TEST_DIR}"
}
@test "script shows help with --help" {
run script.sh --help
[ "${status}" -eq 0 ]
[[ "${output}" =~ "USAGE:" ]]
}
@test "script fails with invalid argument" {
run script.sh --invalid
[ "${status}" -ne 0 ]
}
```
```