script-template.sh 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env bash
  2. # Starter scaffold for an agent-facing skill script. <one-line, ends with a period.>
  3. #
  4. # Usage: script-template.sh [OPTIONS] <input>...
  5. # Input: one or more inputs as positionals; flags select behaviour
  6. # Output: stdout = data product only (TSV, or JSON under --json)
  7. # Stderr: headers, progress, warnings, errors
  8. # Exit: 0 ok, 2 usage, 3 not-found, 4 validation, 5 missing-dep,
  9. # 7 unavailable, 10 <domain signal — document it here>
  10. #
  11. # Examples:
  12. # script-template.sh input.txt
  13. # script-template.sh --json a.txt b.txt | jq '.data[]'
  14. # script-template.sh --out result.tsv --quiet input.txt
  15. #
  16. # Requires: bash 4+ (uses mapfile-style idioms); shellcheck-clean.
  17. set -Eeuo pipefail
  18. IFS=$'\n\t'
  19. # --- semantic exit codes (SKILL-RESOURCE-PROTOCOL §5) ---
  20. readonly EXIT_OK=0 EXIT_USAGE=2 EXIT_NOTFOUND=3 EXIT_VALIDATION=4
  21. readonly EXIT_MISSING_DEP=5 EXIT_UNAVAILABLE=7 EXIT_FINDING=10
  22. readonly SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
  23. # --- help (stdout, exit 0, EXAMPLES mandatory) ---
  24. usage() {
  25. cat <<EOF
  26. Usage: ${SCRIPT_NAME} [OPTIONS] <input>...
  27. Options:
  28. --json emit a JSON envelope to stdout (needs jq)
  29. --out FILE write output to FILE atomically (default: stdout)
  30. -q, --quiet suppress progress framing on stderr
  31. -h, --help show this help and exit
  32. EXAMPLES:
  33. ${SCRIPT_NAME} input.txt
  34. ${SCRIPT_NAME} --json a.txt b.txt | jq '.data[]'
  35. ${SCRIPT_NAME} --out result.tsv -q input.txt
  36. EOF
  37. }
  38. # --- framing helpers: human text ALWAYS to stderr, never stdout ---
  39. log() { [[ "$QUIET" -eq 1 ]] && return 0; printf '%s\n' "$*" >&2; }
  40. die() { printf 'ERROR: %s\n' "$*" >&2; exit "${2:-1}"; }
  41. # --- cleanup trap + atomic write scaffolding ---
  42. TMPFILE=""
  43. cleanup() {
  44. local rc=$?
  45. [[ -n "$TMPFILE" && -e "$TMPFILE" ]] && rm -f -- "$TMPFILE"
  46. exit "$rc"
  47. }
  48. trap cleanup EXIT
  49. trap 'die "interrupted" 130' INT TERM
  50. # --- argument parsing: case loop, long flags, hard usage errors ---
  51. JSON=0; QUIET=0; OUT=""; ARGS=()
  52. while [[ $# -gt 0 ]]; do
  53. case "$1" in
  54. --json) JSON=1 ;;
  55. -q|--quiet) QUIET=1 ;;
  56. --out) OUT="${2:?--out needs a value}"; shift ;;
  57. --out=*) OUT="${1#*=}" ;;
  58. -h|--help) usage; exit "$EXIT_OK" ;;
  59. --) shift; ARGS+=("$@"); break ;;
  60. -*) die "unknown flag: $1 (try --help)" "$EXIT_USAGE" ;;
  61. *) ARGS+=("$1") ;;
  62. esac
  63. shift
  64. done
  65. # --- validation (per §5/§6) ---
  66. [[ ${#ARGS[@]} -ge 1 ]] || die "need at least one input (try --help)" "$EXIT_USAGE"
  67. if [[ "$JSON" -eq 1 ]]; then
  68. command -v jq >/dev/null 2>&1 || die "jq required for --json" "$EXIT_MISSING_DEP"
  69. fi
  70. for in_f in "${ARGS[@]}"; do
  71. [[ -e "$in_f" ]] || die "input not found: $in_f" "$EXIT_NOTFOUND"
  72. done
  73. # --- work: write the DATA PRODUCT to a buffer, framing to stderr ---
  74. log "=== ${SCRIPT_NAME}: processing ${#ARGS[@]} input(s) ==="
  75. emit_records() {
  76. # Replace this with real logic. Data → stdout, one TSV record per line.
  77. local f
  78. for f in "${ARGS[@]}"; do
  79. local lines
  80. lines=$(wc -l < "$f" 2>/dev/null || echo 0)
  81. printf '%s\t%s\n' "$f" "$lines" # DATA → stdout
  82. log " [ok] ${f} (${lines} lines)" # framing → stderr
  83. done
  84. }
  85. if [[ "$JSON" -eq 1 ]]; then
  86. # Build the §4 success envelope. Booleans true/false, empty lists [], ISO-8601 Z.
  87. records="$(emit_records | jq -R -s -c 'split("\n")
  88. | map(select(length>0) | split("\t") | {file: .[0], lines: (.[1]|tonumber)})')"
  89. payload="$(jq -cn --argjson d "$records" \
  90. '{data: $d, meta: {count: ($d|length), schema: "claude-mods.bash-ops.script-template/v1"}}')"
  91. else
  92. payload="$(emit_records)"
  93. fi
  94. # --- output: atomic write when --out, else stdout ---
  95. if [[ -n "$OUT" ]]; then
  96. TMPFILE="$(mktemp -- "${OUT}.XXXXXX")" || die "mktemp failed" "$EXIT_UNAVAILABLE"
  97. printf '%s\n' "$payload" > "$TMPFILE"
  98. mv -- "$TMPFILE" "$OUT" # atomic rename — reader never sees a partial file
  99. TMPFILE="" # disarm cleanup; the file is now $OUT
  100. log "wrote ${OUT}"
  101. else
  102. printf '%s\n' "$payload" # DATA → stdout
  103. fi
  104. exit "$EXIT_OK"