askill
writing-shell-scripts

writing-shell-scriptsSafety 90Repository

Shell script development best practices. Use when writing, modifying, or reviewing bash/sh/zsh scripts, .sh files, .bash files, .bashrc, .bash_profile, or shell script templates (.sh.tftpl).

0 stars
1.2k downloads
Updated 2/23/2026

Package Files

Loading files...
SKILL.md

Shell Script Development Best Practices

Shebang

  • #!/usr/bin/env bash for portability. #!/bin/sh for POSIX-only scripts.
  • Executables: no extension or .sh. Libraries: always .sh, not executable.

Error Handling

Always use set -Eeuo pipefail after the shebang:

  • -E: ERR trap inherited in functions
  • -e: exit on failure
  • -u: exit on undefined variables
  • -o pipefail: exit if any pipeline command fails

Use || true only when explicitly ignoring failures.

Formatting

  • 2-space indent, never tabs. Max 80 characters per line.
  • Place ; then and ; do on the same line as if/for/while.
  • Split long pipelines across lines with | at the start of continuation lines.

Variables

  • Always quote: "$variable", "${prefix}_${suffix}".
  • readonly or declare -r for constants. UPPER_CASE for constants/environment variables.
  • local for function variables. lower_case for local/function-scoped variables.
  • Avoid globals; prefer function parameters and return values.

Functions

  • Define before use. local for all variables. Return meaningful exit codes.
  • lower_case with underscores. Use :: for library namespacing (mylib::helper).
  • Document purpose, globals, arguments, outputs, and return values:
    # Downloads and verifies a file
    # Globals: DOWNLOAD_TIMEOUT
    # Args: $1=URL, $2=destination
    # Outputs: writes to stdout on success
    # Returns: 0 on success, 1 on failure
    download_file() {
        local url="${1:?URL required}"
        local dest="${2:?destination required}"
    }
    
  • Use a main function in scripts with multiple functions; call it last:
    main() {
        parse_args "$@"
        run
    }
    main "$@"
    

Safe file iteration

while IFS= read -r -d '' file; do
    process "$file"
done < <(find . -name "*.txt" -print0)

Cleanup with trap

cleanup() {
    local exit_code=$?
    # Kill background jobs before cleaning up
    jobs -p | xargs -r kill 2>/dev/null || true
    wait 2>/dev/null || true
    rm -rf "$TEMP_DIR"
    exit "$exit_code"
}
trap cleanup EXIT
trap 'trap - EXIT; cleanup; kill -INT $$' INT
trap 'trap - EXIT; cleanup; kill -TERM $$' TERM
  • trap cleanup EXIT runs on normal exit, errors (set -e), and signals.
  • Separate INT/TERM traps re-raise the signal so the parent process sees the correct exit status.
  • Kill child processes explicitly — by default, backgrounded jobs survive the parent's exit.

Logging

log_info()  { printf '[INFO]  %s\n' "$*"; }
log_warn()  { printf '[WARN]  %s\n' "$*" >&2; }
log_error() { printf '[ERROR] %s\n' "$*" >&2; }
die()       { log_error "$@"; exit 1; }

Input/Output

  • printf '%s\n' "$msg" over echo for formatted output.
  • Errors to stderr: >&2.
  • Use heredocs for multi-line strings.

Arithmetic

  • Use (( )) or $(( )) for math. Never let, $[ ], or expr.

String Manipulation

Prefer parameter expansion over sed/awk for simple transformations (${var#pattern}, ${var%pattern}, ${var/old/new}).

Defaults and guards:

"${1:-default}"    # use default if $1 is unset or empty
"${1:?error msg}"  # exit with error if $1 is unset or empty
"${var:+alt}"      # use alt if var is set and non-empty, else empty

Working with JSON (jq)

Use jq for JSON processing — never parse JSON with grep/sed/awk.

# Extract a field
name=$(jq -r '.user.name' response.json)

# Iterate over array elements
jq -r '.items[] | .id' data.json | while IFS= read -r id; do
    process "$id"
done

# Build JSON safely with --arg (handles escaping)
jq -n --arg name "$USER" --arg host "$HOSTNAME" \
    '{"name": $name, "host": $host}'

# Modify JSON in-place
jq '.config.timeout = 30 | .config.retries = 3' config.json > tmp.json \
    && mv tmp.json config.json

# Select and filter
jq '[.[] | select(.status == "active")]' users.json
  • Always use -r (raw output) when piping jq output to other commands — without it, strings include quotes.
  • Use --arg and --argjson to pass shell variables into jq expressions safely.
  • Use --exit-status (or -e) to make jq return non-zero when the filter produces false or null.

Arrays

# Indexed arrays
files=("one.txt" "two.txt" "three.txt")
files+=("four.txt")             # append
echo "${files[0]}"              # first element
echo "${files[@]}"              # all elements (preserves quoting)
echo "${#files[@]}"             # length

# Iterate safely (handles spaces in elements)
for f in "${files[@]}"; do
    process "$f"
done

# Associative arrays (bash 4+)
declare -A config
config[host]="localhost"
config[port]="5432"
for key in "${!config[@]}"; do
    echo "$key=${config[$key]}"
done

Security

  • Validate all user inputs. Use -- to separate options from arguments.
  • Avoid eval. Secure permissions (chmod 600) for sensitive files.
  • SUID/SGID forbidden on shell scripts — use sudo instead.
  • Use mktemp for temp files — never hardcode /tmp/myfile:
    TEMP_DIR="$(mktemp -d)" || die "failed to create temp dir"
    

ShellCheck

Run shellcheck on every script. Fix all warnings — don't suppress without justification.

# Suppress a specific rule with an explanation
# shellcheck disable=SC2086  # word splitting intended for $flags
docker run $flags "$image"

# Suppress for an entire file (top of file, after shebang)
# shellcheck disable=SC2034  # variables sourced by another script

# Source directive for files shellcheck can't find
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"

Rule ranges: SC1xxx = syntax/parsing, SC2xxx = best practices, SC3xxx = POSIX compatibility.

Portability

  • Check required tools: command -v git >/dev/null || die "git required".
  • Avoid GNU-specific options when possible (see portability-matrix.md).
  • POSIX sh vs bash: POSIX sh has no arrays, no [[ ]], no <() process substitution, no {a,b} brace expansion, no += append. Use #!/bin/sh only when you avoid all bash-isms.
  • macOS ships bash 3.2 (2007, GPLv2). Associative arrays (declare -A) require bash 4+. If targeting macOS, either use #!/bin/sh (POSIX) or require users to install modern bash via Homebrew.

New script workflow

- [ ] Start with shebang and set -Eeuo pipefail
- [ ] Add argument parsing with usage/help
- [ ] Add prerequisite checks (required tools)
- [ ] Implement main logic with cleanup trap
- [ ] Add logging and error handling
- [ ] Run validation loop (below)

Validation loop

  1. shellcheck script.sh — fix all warnings (SC1xxx=syntax, SC2xxx=best practices, SC3xxx=POSIX)
  2. bash -n script.sh — fix syntax errors
  3. Test with valid input — verify expected behavior
  4. Test with invalid input — verify error messages and exit codes
  5. Test on macOS if targeting both platforms
  6. Repeat until shellcheck is clean and all cases pass

Deep-dive references

CLI tool template: See patterns/cli-tool-template.md for a production-ready script template Deployment scripts: See patterns/deployment-scripts.md for deploy, rollback, health check patterns Testing: See patterns/testing-patterns.md for bats framework, mocking, CI integration GNU vs BSD: See portability-matrix.md for cross-platform command differences

Official references

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

92/100Analyzed 2/22/2026

Excellent, comprehensive shell script development guide with high actionability. Covers shebang, error handling (set -Eeuo pipefail), formatting, variables, functions, trap cleanup, logging, JSON/jq, arrays, security, ShellCheck integration, and portability. Includes practical validation loop and new script workflow checklist. References Google Shell Style Guide and ShellCheck Wiki. Slight deduction for internal file references but overall highly reusable.

90
90
88
92
95

Metadata

Licenseunknown
Version-
Updated2/23/2026
PublisherChogos

Tags

ci-cdgithubgithub-actionsobservabilitysecuritytesting