Back to skills
SkillHub ClubWrite Technical DocsFull StackDevOpsTech Writer

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.

Stars
9
Hot score
84
Updated
March 20, 2026
Overall rating
C1.9
Composite score
1.9
Best-practice grade
B70.4

Install command

npx @skill-hub/cli install mauromedda-agent-toolkit-bash
bashshellcheckautomationscriptingbest-practices

Repository

mauromedda/agent-toolkit

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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 ]
}
```

```

bash | SkillHub