APMSign in

>Agent Skill

@b33eep/standards-shell

skilldevelopment

This skill provides Shell/Bash coding standards and is automatically loaded for shell projects. It includes defensive scripting patterns, best practices, and recommended tooling.

apm::install
$apm install @b33eep/standards-shell
apm::skill.md
---
name: standards-shell
description: This skill provides Shell/Bash coding standards and is automatically loaded for shell projects. It includes defensive scripting patterns, best practices, and recommended tooling.
type: context
applies_to: [bash, sh, shell, zsh, shellcheck, bats]
file_extensions: [".sh", ".bash"]
---

# Shell/Bash Coding Standards

## Core Principles

1. **Simplicity**: Simple, understandable scripts
2. **Readability**: Readability over cleverness
3. **Maintainability**: Scripts that are easy to maintain
4. **Testability**: Scripts that are easy to test
5. **DRY**: Don't Repeat Yourself - but don't overdo it
6. **Defensiveness**: Fail early, fail loudly

## General Rules

- **Defensive Header**: Always use `set -euo pipefail`
- **Quote Variables**: Always quote variables `"$var"`
- **Descriptive Names**: Meaningful names for variables and functions
- **Minimal Changes**: Only change relevant code parts
- **No Over-Engineering**: No unnecessary complexity
- **ShellCheck Clean**: All scripts must pass ShellCheck

## Naming Conventions

| Element | Convention | Example |
|---------|------------|---------|
| Variables | snake_case | `user_name`, `file_count` |
| Functions | snake_case | `get_user_by_id`, `validate_input` |
| Constants | UPPER_SNAKE_CASE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
| Files | kebab-case or snake_case | `deploy-app.sh`, `run_tests.sh` |
| Environment Vars | UPPER_SNAKE_CASE | `API_URL`, `DATABASE_HOST` |

## Script Template

```bash
#!/bin/bash
set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

# Cleanup on exit
cleanup() {
    rm -f "$SCRIPT_DIR"/*.tmp 2>/dev/null || true
}
trap cleanup EXIT

# Error handler
error_handler() {
    echo "Error on line $1" >&2
    exit 1
}
trap 'error_handler $LINENO' ERR

main() {
    # Script logic here
    echo "Running $SCRIPT_NAME"
}

main "$@"
```

## Defensive Scripting

```bash
# REQUIRED: Always start with this
set -euo pipefail
# -e: Exit on error
# -u: Error on undefined variables
# -o pipefail: Pipe fails if any command fails

# RECOMMENDED: Safer IFS
IFS=$'\n\t'

# REQUIRED: Quote all variables
echo "$var"                     # Good
echo $var                       # Bad - word splitting

# REQUIRED: Use [[ ]] for conditionals (Bash)
if [[ -f "$file" ]]; then       # Good
if [ -f "$file" ]; then         # POSIX only
```

## Parameter Expansion

```bash
# Defaults and validation
${var:-default}             # Use default if unset
${var:=default}             # Assign default if unset
${var:?error message}       # Error if unset

# String manipulation
${var#pattern}              # Remove prefix (shortest)
${var##pattern}             # Remove prefix (longest)
${var%pattern}              # Remove suffix (shortest)
${var%%pattern}             # Remove suffix (longest)
${var/old/new}              # Replace first
${var//old/new}             # Replace all
${#var}                     # Length

# Examples
file="document.txt"
echo "${file%%.*}"          # "document" (remove extension)
echo "${file##*.}"          # "txt" (get extension)
```

## Functions

```bash
# REQUIRED: Use local variables
get_user_name() {
    local user_id=$1
    local name
    name=$(grep "^${user_id}:" /etc/passwd | cut -d: -f5)
    echo "$name"
}

# Return values via stdout
result=$(get_user_name "1000")

# Return status codes
validate_file() {
    local file=$1
    if [[ ! -f "$file" ]]; then
        echo "Error: File not found: $file" >&2
        return 1
    fi
    return 0
}

if validate_file "$input_file"; then
    process_file "$input_file"
fi
```

## Arrays

```bash
# Indexed arrays
files=("file1.txt" "file2.txt" "file3.txt")
echo "${files[0]}"          # First element
echo "${files[@]}"          # All elements
echo "${#files[@]}"         # Array length

# Iterate safely
for file in "${files[@]}"; do
    echo "Processing: $file"
done

# Associative arrays (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}:${config[port]}"
```

## File Operations

```bash
# Read file line by line
while IFS= read -r line; do
    echo "Line: $line"
done < "input.txt"

# Read into array
mapfile -t lines < "input.txt"

# Write to file (heredoc)
cat > output.txt <<EOF
Line 1
Line 2
EOF

# Temp files with cleanup
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT
```

## Error Handling

```bash
# Trap for cleanup
cleanup() {
    echo "Cleaning up..."
    rm -f "$temp_file"
}
trap cleanup EXIT

# Trap for errors
error_handler() {
    local line=$1
    echo "Error occurred on line $line" >&2
}
trap 'error_handler $LINENO' ERR

# Check command exists
if ! command -v python3 &>/dev/null; then
    echo "Error: python3 not found" >&2
    exit 1
fi

# Conditional execution
command1 && command2        # Run command2 only if command1 succeeds
command1 || command2        # Run command2 only if command1 fails
```

## Argument Parsing with getopts

```bash
usage() {
    echo "Usage: $0 [-v] [-o output] [-h]"
    echo "  -v        Verbose mode"
    echo "  -o FILE   Output file"
    echo "  -h        Show help"
    exit 1
}

verbose=false
output_file=""

while getopts "vo:h" opt; do
    case $opt in
        v) verbose=true ;;
        o) output_file="$OPTARG" ;;
        h) usage ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))

# Remaining args in $@
```

## Logging

```bash
log() {
    local level=$1
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}

log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }

# Usage
log_info "Starting process"
log_error "Failed to connect"
```

## Debugging

```bash
# Enable debugging
set -x                      # Print commands
PS4='+ ${BASH_SOURCE}:${LINENO}: '  # Better debug output

# Debug specific section
set -x
# code to debug
set +x

# Run script with debug
bash -x script.sh
bash -n script.sh           # Syntax check only
```

## Common Patterns

```bash
# Check if root
if [[ $EUID -ne 0 ]]; then
    echo "This script must be run as root" >&2
    exit 1
fi

# Safe directory change
cd "$target_dir" || exit 1

# Process files safely (handles spaces)
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    echo "Processing: $file"
done

# Retry pattern
retry() {
    local max_attempts=$1
    local delay=$2
    shift 2
    local attempt=1

    while [[ $attempt -le $max_attempts ]]; do
        if "$@"; then
            return 0
        fi
        log_warn "Attempt $attempt failed, retrying in ${delay}s..."
        sleep "$delay"
        ((attempt++))
    done
    return 1
}

retry 3 5 curl -f "https://api.example.com/health"
```

## Recommended Tooling

| Tool | Purpose |
|------|---------|
| `shellcheck` | Static analysis (required) |
| `shfmt` | Code formatting |
| `bats-core` | Testing framework |
| `bash 5.x` | Modern features (avoid macOS default 3.2) |

## ShellCheck Usage

```bash
# Run ShellCheck
shellcheck script.sh

# Disable specific warning (sparingly)
# shellcheck disable=SC2086
echo $UNQUOTED_VAR

# Follow sourced files
shellcheck -x script.sh
```

## Testing with bats-core

```bash
#!/usr/bin/env bats
# File: test_script.bats

source ./script.sh

@test "add function returns correct sum" {
    result=$(add 5 3)
    [ "$result" = "8" ]
}

@test "validate_file fails on missing file" {
    run validate_file "nonexistent.txt"
    [ "$status" -eq 1 ]
}
```

Run tests:
```bash
bats tests/
```

## POSIX Compatibility

For maximum portability (sh, dash, ash):

```sh
#!/bin/sh
# Use [ ] instead of [[ ]]
if [ -f "file.txt" ]; then
    echo "File exists"
fi

# No arrays, use positional parameters
set -- "apple" "banana" "cherry"
echo "First: $1"

# No $() in older shells, use backticks
current_date=`date +%Y-%m-%d`
```

## Production Best Practices

1. **Defensive header** - Always `set -euo pipefail`
2. **Quote everything** - Prevent word splitting and glob expansion
3. **Local variables** - Use `local` in functions
4. **ShellCheck clean** - No warnings before commit
5. **Cleanup traps** - Always clean up temp files
6. **Meaningful exit codes** - 0 for success, non-zero for errors
7. **Logging to stderr** - Keep stdout for data, stderr for logs
8. **Check dependencies** - Verify required commands exist
9. **Handle signals** - Trap SIGTERM for graceful shutdown
10. **Document usage** - Include `--help` option

---

## References

- Based on [moai-lang-shell](https://github.com/AJBcoding/claude-skill-eval/tree/main/skills/moai-lang-shell) by AJBcoding