@objctp/opencode-shell-routines 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/agents/shell-architect.md +88 -0
  4. package/agents/shell-expert.md +60 -0
  5. package/commands/shell-audit.md +47 -0
  6. package/commands/shell-batch-exec.md +48 -0
  7. package/commands/shell-new.md +57 -0
  8. package/commands/shell-routines-setup.md +66 -0
  9. package/commands/shell-test-run.md +46 -0
  10. package/opencode.json +19 -0
  11. package/package.json +34 -0
  12. package/plugins/shell-hooks.ts +150 -0
  13. package/scripts/lib-batch.sh +297 -0
  14. package/scripts/lib-common.sh +332 -0
  15. package/skills/shell-batch-operations/SKILL.md +97 -0
  16. package/skills/shell-batch-operations/assets/batch-template.sh +124 -0
  17. package/skills/shell-batch-operations/examples/data-pipeline.sh +157 -0
  18. package/skills/shell-batch-operations/examples/file-batch.sh +140 -0
  19. package/skills/shell-batch-operations/references/decision-tree.md +53 -0
  20. package/skills/shell-best-practices/SKILL.md +313 -0
  21. package/skills/shell-best-practices/assets/library.sh +142 -0
  22. package/skills/shell-best-practices/assets/minimal.sh +54 -0
  23. package/skills/shell-best-practices/assets/posix.sh +180 -0
  24. package/skills/shell-best-practices/assets/standard.sh +203 -0
  25. package/skills/shell-best-practices/references/patterns.md +386 -0
  26. package/skills/shell-best-practices/references/security.md +195 -0
  27. package/skills/shell-debugging/SKILL.md +115 -0
  28. package/skills/shell-debugging/examples/debug-session.md +165 -0
  29. package/skills/shell-debugging/references/debugging-guide.md +336 -0
  30. package/skills/shell-profiling/SKILL.md +154 -0
  31. package/skills/shell-profiling/examples/profile-session.md +225 -0
  32. package/skills/shell-profiling/references/optimisation-patterns.md +373 -0
  33. package/skills/shell-profiling/references/profiling-tools.md +318 -0
  34. package/skills/shell-profiling/scripts/bench.sh +82 -0
  35. package/skills/shell-profiling/scripts/trace-aggregate.sh +34 -0
  36. package/skills/shell-review/SKILL.md +61 -0
  37. package/skills/shell-review/examples/sample-review.md +42 -0
  38. package/skills/shell-review/references/guidelines.md +48 -0
  39. package/skills/shell-review/references/review-template.md +56 -0
  40. package/skills/shell-security/SKILL.md +128 -0
  41. package/skills/shell-security/examples/dangerous-command-review.md +231 -0
  42. package/skills/shell-security/examples/secure-script-example.sh +317 -0
  43. package/skills/shell-security/references/dangerous-commands.md +561 -0
  44. package/skills/shell-security/references/security-patterns.md +30 -0
  45. package/skills/shell-security/references/sensitive-files.md +525 -0
  46. package/skills/shell-security/scripts/security-audit.sh +208 -0
  47. package/skills/shell-test/SKILL.md +237 -0
  48. package/skills/shell-test/examples/test-example.md +74 -0
  49. package/skills/shell-test/references/advanced-patterns.md +52 -0
  50. package/skills/shell-test/references/assertions.md +184 -0
  51. package/skills/shell-test/references/test-template.md +60 -0
  52. package/skills/shell-test/scripts/public-coverage.sh +93 -0
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2178
3
+ #
4
+ # Common library of reusable bash functions
5
+ # Source this file in your scripts: source /path/to/lib-common.sh
6
+ #
7
+ # Functions:
8
+ # shroutines::log_info, shroutines::log_warn, shroutines::log_error, shroutines::log_debug - Logging functions
9
+ # shroutines::require_command - Check if a command is available
10
+ # shroutines::validate_input - Validate input against a pattern
11
+ # shroutines::ensure_dir - Create directory if it doesn't exist
12
+ # shroutines::temp_file, shroutines::temp_dir - Create temporary resources with cleanup
13
+ # shroutines::prompt_yes_no - Interactive yes/no prompt
14
+ # shroutines::get_timestamp - Get formatted timestamp
15
+ # shroutines::truncate_string - Truncate string to max length
16
+ #
17
+
18
+ # Guard against direct execution
19
+ [[ "${BASH_SOURCE[0]}" == "${0}" ]] && {
20
+ echo "Error: This file should be sourced, not executed" >&2
21
+ exit 2
22
+ }
23
+
24
+ # Version tracking
25
+ # shellcheck disable=SC2034 # available for consumers to check library version
26
+ readonly LIB_COMMON_VERSION="1.0.0"
27
+
28
+ ###
29
+ ### :::: Logging Functions :::: #######
30
+ ###
31
+
32
+ # Internal: print formatted log line (no subprocess — uses printf %()T builtin)
33
+ # Usage: _log "LEVEL" "message"
34
+ function _log() {
35
+ printf '[%(%Y-%m-%d %H:%M:%S)T] [%s] %s\n' -1 "$1" "${*:2}" >&2
36
+ }
37
+
38
+ # Log informational message
39
+ # Usage: shroutines::log_info "message"
40
+ function shroutines::log_info() { _log "INFO" "$@"; }
41
+
42
+ # Log warning message
43
+ # Usage: shroutines::log_warn "message"
44
+ function shroutines::log_warn() { _log "WARN" "$@"; }
45
+
46
+ # Log error message
47
+ # Usage: shroutines::log_error "message"
48
+ function shroutines::log_error() { _log "ERROR" "$@"; }
49
+
50
+ # Log debug message (only when DEBUG=1)
51
+ # Usage: shroutines::log_debug "message"
52
+ function shroutines::log_debug() { [[ "${DEBUG:-0}" == "1" ]] && _log "DEBUG" "$@"; }
53
+
54
+ ###
55
+ ### :::: Command Validation :::: #######
56
+ ###
57
+
58
+ # Check if a command is available
59
+ # Usage: shroutines::require_command "cmdname"
60
+ # Returns: 0 if command exists, 1 otherwise
61
+ function shroutines::require_command() {
62
+ local cmd="$1"
63
+
64
+ if ! command -v "$cmd" >/dev/null 2>&1; then
65
+ shroutines::log_error "Required command not found: $cmd"
66
+ return 1
67
+ fi
68
+
69
+ shroutines::log_debug "Command found: $cmd"
70
+ return 0
71
+ }
72
+
73
+ # Require multiple commands
74
+ # Usage: shroutines::require_commands "cmd1" "cmd2" "cmd3"
75
+ # Returns: 0 if all commands exist, 1 otherwise
76
+ function shroutines::require_commands() {
77
+ local missing=()
78
+
79
+ for cmd in "$@"; do
80
+ if ! command -v "$cmd" >/dev/null 2>&1; then
81
+ missing+=("$cmd")
82
+ fi
83
+ done
84
+
85
+ if [[ ${#missing[@]} -gt 0 ]]; then
86
+ shroutines::log_error "Missing required commands: ${missing[*]}"
87
+ return 1
88
+ fi
89
+
90
+ return 0
91
+ }
92
+
93
+ ###
94
+ ### :::: Input Validation :::: #######
95
+ ###
96
+
97
+ # Validate input against a regex pattern
98
+ # Usage: shroutines::validate_input "input" "pattern"
99
+ # Returns: 0 if matches, 1 otherwise
100
+ function shroutines::validate_input() {
101
+ local input="$1"
102
+ local pattern="${2:-^[a-zA-Z0-9_-]+$}"
103
+
104
+ [[ "$input" =~ $pattern ]]
105
+ }
106
+
107
+ # Validate a number
108
+ # Usage: shroutines::is_number "value"
109
+ function shroutines::is_number() {
110
+ local value="$1"
111
+ [[ "$value" =~ ^-?[0-9]+$ ]]
112
+ }
113
+
114
+ # Validate a port number (1-65535)
115
+ # Usage: shroutines::is_port "value"
116
+ function shroutines::is_port() {
117
+ local value="$1"
118
+ shroutines::is_number "$value" && ((value >= 1 && value <= 65535))
119
+ }
120
+
121
+ ###
122
+ ### :::: File System Operations :::: #######
123
+ ###
124
+
125
+ # Ensure a directory exists, create if missing
126
+ # Usage: shroutines::ensure_dir "path" [mode]
127
+ # Returns: 0 on success, 1 on failure
128
+ function shroutines::ensure_dir() {
129
+ local path="$1"
130
+ local mode="${2:-0755}"
131
+
132
+ if [[ ! -d "$path" ]]; then
133
+ shroutines::log_debug "Creating directory: $path"
134
+ mkdir -p "$path" && chmod "$mode" "$path" || return 1
135
+ fi
136
+
137
+ return 0
138
+ }
139
+
140
+ # Create temporary file with automatic cleanup
141
+ # Usage: shroutines::temp_file var_name
142
+ # Tracks all temp files in _LIB_TEMP_FILES to avoid overwriting previous traps
143
+ function shroutines::temp_file() {
144
+ local -n var_ref="$1"
145
+ local tmp
146
+
147
+ tmp=$(mktemp) || {
148
+ shroutines::log_error "Failed to create temporary file"
149
+ return 1
150
+ }
151
+
152
+ # shellcheck disable=SC2034 # nameref: assignment is the intended use
153
+ var_ref="$tmp"
154
+ shroutines::log_debug "Created temp file: $tmp"
155
+
156
+ _LIB_TEMP_FILES+=("$tmp")
157
+ }
158
+
159
+ # Create temporary directory with automatic cleanup
160
+ # Usage: shroutines::temp_dir var_name
161
+ # Tracks all temp dirs in _LIB_TEMP_DIRS to avoid overwriting previous traps
162
+ function shroutines::temp_dir() {
163
+ local -n var_ref="$1"
164
+ local tmp
165
+
166
+ tmp=$(mktemp -d) || {
167
+ shroutines::log_error "Failed to create temporary directory"
168
+ return 1
169
+ }
170
+
171
+ # shellcheck disable=SC2034 # nameref: assignment is the intended use
172
+ var_ref="$tmp"
173
+ shroutines::log_debug "Created temp dir: $tmp"
174
+
175
+ _LIB_TEMP_DIRS+=("$tmp")
176
+ }
177
+
178
+ # Initialise cleanup tracking arrays and register a single trap
179
+ # shellcheck disable=SC2034 # arrays populated by shroutines::temp_file/shroutines::temp_dir
180
+ _LIB_TEMP_FILES=()
181
+ _LIB_TEMP_DIRS=()
182
+ function _lib_cleanup() {
183
+ local f
184
+ for f in "${_LIB_TEMP_FILES[@]+"${_LIB_TEMP_FILES[@]}"}"; do
185
+ rm -f "$f"
186
+ done
187
+ local d
188
+ for d in "${_LIB_TEMP_DIRS[@]+"${_LIB_TEMP_DIRS[@]}"}"; do
189
+ rm -rf "$d"
190
+ done
191
+ }
192
+ trap _lib_cleanup EXIT
193
+
194
+ ###
195
+ ### :::: User Interaction :::: ########
196
+ ###
197
+
198
+ # Prompt user for yes/no confirmation
199
+ # Usage: shroutines::prompt_yes_no "question" [default]
200
+ # Returns: 0 for yes, 1 for no
201
+ function shroutines::prompt_yes_no() {
202
+ local question="$1"
203
+ local default="${2:-n}"
204
+ local prompt
205
+ local response
206
+
207
+ # Build prompt string
208
+ if [[ "$default" == "y" ]]; then
209
+ prompt="$question [Y/n] "
210
+ else
211
+ prompt="$question [y/N] "
212
+ fi
213
+
214
+ # Read response
215
+ read -r -p "$prompt" response </dev/tty
216
+ response=${response:-$default}
217
+
218
+ # Check response
219
+ case "$response" in
220
+ [Yy] | [Yy][Ee][Ss]) return 0 ;;
221
+ *) return 1 ;;
222
+ esac
223
+ }
224
+
225
+ ###
226
+ ### :::: String Utilities :::: #######
227
+ ###
228
+
229
+ # Get formatted timestamp
230
+ # Usage: shroutines::get_timestamp [format]
231
+ # Default format: YYYY-MM-DD HH:MM:SS
232
+ function shroutines::get_timestamp() {
233
+ local format="${1:-+%Y-%m-%d %H:%M:%S}"
234
+ date "$format"
235
+ }
236
+
237
+ # Truncate string to max length
238
+ # Usage: shroutines::truncate_string "string" max_length [suffix]
239
+ function shroutines::truncate_string() {
240
+ local string="$1"
241
+ local max_length="$2"
242
+ local suffix="${3:-...}"
243
+
244
+ if [[ ${#string} -le $max_length ]]; then
245
+ echo "$string"
246
+ else
247
+ printf '%.*s%s' "$((max_length - ${#suffix}))" "$string" "$suffix"
248
+ fi
249
+ }
250
+
251
+ # Repeat a character n times
252
+ # Usage: shroutines::repeat_char "*" 40
253
+ function shroutines::repeat_char() {
254
+ local char="$1"
255
+ local count="$2"
256
+
257
+ printf '%*s' "$count" '' | tr ' ' "$char"
258
+ }
259
+
260
+ ###
261
+ ### :::: Array Utilities :::: #########
262
+ ###
263
+
264
+ # Check if array contains a value
265
+ # Usage: shroutines::array_contains "value" "${array[@]}"
266
+ # Returns: 0 if found, 1 otherwise
267
+ function shroutines::array_contains() {
268
+ local seek="$1"
269
+ shift
270
+
271
+ local item
272
+ for item in "$@"; do
273
+ [[ "$item" == "$seek" ]] && return 0
274
+ done
275
+
276
+ return 1
277
+ }
278
+
279
+ # Join array elements with delimiter
280
+ # Usage: shroutines::array_join "," "${array[@]}"
281
+ function shroutines::array_join() {
282
+ local delimiter="$1"
283
+ shift
284
+
285
+ (($# == 0)) && return 0
286
+
287
+ local first="$1"
288
+ shift
289
+
290
+ printf '%s' "$first"
291
+ local item
292
+ for item in "$@"; do
293
+ printf '%s%s' "$delimiter" "$item"
294
+ done
295
+ }
296
+
297
+ ###
298
+ ### :::: Exit Handling :::: ###########
299
+ ###
300
+
301
+ # Exit with error message
302
+ # Usage: shroutines::die "error message" [exit_code]
303
+ function shroutines::die() {
304
+ local message="$1"
305
+ local exit_code="${2:-1}"
306
+
307
+ shroutines::log_error "$message"
308
+ exit "$exit_code"
309
+ }
310
+
311
+ # Show usage and exit
312
+ # Usage: shroutines::show_usage "usage_string"
313
+ function shroutines::show_usage() {
314
+ local usage="$1"
315
+
316
+ echo "$usage" >&2
317
+ exit 2
318
+ }
319
+
320
+ ###
321
+ ### :::: Export Functions :::: ########
322
+ ###
323
+
324
+ # Export all public functions for use in subshells
325
+ export -f _log shroutines::log_info shroutines::log_warn shroutines::log_error shroutines::log_debug
326
+ export -f shroutines::require_command shroutines::require_commands
327
+ export -f shroutines::validate_input shroutines::is_number shroutines::is_port
328
+ export -f shroutines::ensure_dir shroutines::temp_file shroutines::temp_dir
329
+ export -f shroutines::prompt_yes_no
330
+ export -f shroutines::get_timestamp shroutines::truncate_string shroutines::repeat_char
331
+ export -f shroutines::array_contains shroutines::array_join
332
+ export -f shroutines::die shroutines::show_usage
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: shell-batch-operations
3
+ description: Write batch shell scripts that consolidate many operations into one run emitting a single structured JSON result. Use when one action runs across 3+ similar inputs ("process all files", "rename many files") or a multi-stage shell pipeline is needed (extract → transform → aggregate), and the user need not see results between steps. Prefer this over repeated individual tool calls.
4
+ allowed-tools: Read, Write, Edit, Bash
5
+ ---
6
+
7
+ # Batch Operations Skill
8
+
9
+ ## When to Use Batch
10
+
11
+ Use batch when the user need not see results between steps — the script consolidates many operations into one JSON result. When intermediate visibility matters (debugging, per-step diagnosis), use individual tool calls instead.
12
+
13
+ For the full decision matrix and borderline cases, read `references/decision-tree.md`.
14
+
15
+ ## The Batch Pattern
16
+
17
+ All batch scripts follow this structure:
18
+
19
+ ```bash
20
+ #!/usr/bin/env bash
21
+ set -euo pipefail
22
+
23
+ source "${CLAUDE_PLUGIN_ROOT}/scripts/lib-batch.sh"
24
+
25
+ declare -A RESULTS
26
+ declare -a METADATA
27
+ declare -a ERRORS
28
+
29
+ batch_add_metadata METADATA "script" "$(basename "$0")"
30
+ batch_add_metadata METADATA "started" "$(date -Iseconds)"
31
+
32
+ # — your processing logic here —
33
+
34
+ batch_add_metadata METADATA "completed" "$(date -Iseconds)"
35
+ batch_output RESULTS METADATA ERRORS
36
+ ```
37
+
38
+ Progress goes to stderr; only the final JSON result reaches stdout.
39
+
40
+ ## lib-batch.sh API
41
+
42
+ Sourced from `${CLAUDE_PLUGIN_ROOT}/scripts/lib-batch.sh`.
43
+
44
+ | Function | Purpose |
45
+ | ------------------------------------------------- | --------------------------------------- |
46
+ | `batch_add_result RESULTS "key" "value"` | Store a named result |
47
+ | `batch_add_result_item RESULTS "item"` | Append to a list |
48
+ | `batch_add_metadata METADATA "key" "value"` | Add run metadata |
49
+ | `batch_add_error ERRORS "message"` | Record a non-fatal error |
50
+ | `batch_progress "message"` | Log to stderr (safe during JSON output) |
51
+ | `batch_step "label" current total` | Log progress with percentage |
52
+ | `batch_output RESULTS METADATA [ERRORS]` | Emit final JSON to stdout |
53
+ | `batch_process_files RESULTS METADATA ERRORS "pattern" callback` | Process files matching a glob pattern |
54
+ | `batch_run_command RESULTS "key" command [args]` | Run command, store exit code and output |
55
+
56
+ ## Output Format
57
+
58
+ ```json
59
+ {
60
+ "results": {
61
+ "file_count": 42,
62
+ "total_lines": 12345
63
+ },
64
+ "metadata": {
65
+ "script": "process-txt-files",
66
+ "started": "2026-03-03T10:30:00+00:00",
67
+ "completed": "2026-03-03T10:30:05+00:00"
68
+ },
69
+ "errors": ["File too large, skipping: ./huge.txt (50000000 bytes)"]
70
+ }
71
+ ```
72
+
73
+ The `errors` key appears only when at least one error was recorded; with none, it is omitted entirely.
74
+
75
+ ## Common Pitfalls
76
+
77
+ | Pitfall | Fix |
78
+ | ------------------------------------- | ----------------------------------------------- |
79
+ | Logging to stdout | Use `batch_progress` or `echo >&2` |
80
+ | Forgetting `batch_output` | Always call it last with all three arrays |
81
+ | Non-JSON-safe values in results | The output builder escapes them; avoid raw newlines in values |
82
+ | Storing full file contents in results | Store summaries only |
83
+
84
+ ## Reference Files
85
+
86
+ - `references/decision-tree.md` — Decision flowchart, matrix of common scenarios, and key factors (operation count, similarity, interactivity, error handling)
87
+ - `assets/batch-template.sh` — Starting point for new batch scripts: argument parsing, lib-batch.sh sourcing with fallback, error collection scaffolding, and standard output formatting
88
+ - `examples/file-batch.sh` — File iteration, per-file processing, and summary results
89
+ - `examples/data-pipeline.sh` — Multi-stage pipelines (extract, transform, analyse): temp-file-based stage handoff, intermediate error handling, and percentage calculations
90
+
91
+ Always read all references and examples before producing output.
92
+
93
+ ## Integration
94
+
95
+ - **`shell-best-practices`** — Coding standards apply inside batch scripts
96
+ - **`shell-architect`** agent — Architecture advice for complex multi-script pipelines
97
+ - **`/shell-batch-exec`** command — Runs a batch script and parses the JSON result
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC1091 # dynamic source paths resolved at runtime
3
+ #
4
+ # Batch script template for shell-routines plugin
5
+ # Description: [Brief description of what this batch script does]
6
+ # Usage: ./script-name.sh [arguments]
7
+ # Output: JSON with results, metadata, and optional errors
8
+ #
9
+
10
+ set -euo pipefail
11
+
12
+ ###
13
+ ### :::: CONFIGURATION :::: ###########
14
+ ###
15
+
16
+ SCRIPT_NAME="${0##*/}"
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+
19
+ # Source batch utilities
20
+ # Note: CLAUDE_PLUGIN_ROOT is set by Claude Code plugin
21
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
22
+ source "${CLAUDE_PLUGIN_ROOT}/scripts/lib-batch.sh"
23
+ elif [[ -f "${SCRIPT_DIR}/lib-batch.sh" ]]; then
24
+ source "${SCRIPT_DIR}/lib-batch.sh"
25
+ else
26
+ echo "Error: Cannot find lib-batch.sh. Install shell-routines plugin or copy lib-batch.sh to ${SCRIPT_DIR}/" >&2
27
+ exit 2
28
+ fi
29
+
30
+ ###
31
+ ### :::: ARGUMENT PARSING :::: ########
32
+ ###
33
+
34
+ # Default values
35
+ VERBOSE=0
36
+
37
+ # shellcheck disable=SC2034 # VERBOSE is a template placeholder, used after scaffolding
38
+ while getopts ":vh" opt; do
39
+ case "$opt" in
40
+ v) VERBOSE=1 ;;
41
+ h)
42
+ echo "Usage: $SCRIPT_NAME [options]"
43
+ echo "Options:"
44
+ echo " -v Verbose output"
45
+ echo " -h Show this help"
46
+ exit 0
47
+ ;;
48
+ \?)
49
+ echo "Invalid option: -$OPTARG" >&2
50
+ exit 2
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ shift $((OPTIND - 1))
56
+
57
+ ###
58
+ ### :::: FUNCTIONS :::: ###############
59
+ ###
60
+
61
+ # Process a single item
62
+ # Usage: process_item "item"
63
+ # Returns: 0 on success, 1 on failure
64
+ process_item() {
65
+ local item="$1"
66
+
67
+ # Your processing logic here
68
+ batch_progress "Processing: ${item}"
69
+
70
+ return 0
71
+ }
72
+
73
+ # Main processing function
74
+ # Usage: main
75
+ main() {
76
+ # Results storage
77
+ # shellcheck disable=SC2034 # passed by nameref to lib-batch.sh functions
78
+ declare -A RESULTS
79
+ # shellcheck disable=SC2034
80
+ declare -a METADATA
81
+ # shellcheck disable=SC2034
82
+ declare -a ERRORS
83
+
84
+ # Add metadata
85
+ batch_add_metadata METADATA "script" "$SCRIPT_NAME"
86
+ batch_add_metadata METADATA "started" "$(date -Iseconds)"
87
+
88
+ ###
89
+ ### :::: YOUR LOGIC HERE :::: ########
90
+ ###
91
+
92
+ batch_progress "Starting batch processing"
93
+
94
+ # Capture total before processing loop
95
+ local total=$#
96
+
97
+ # Example: Process items
98
+ local count=0
99
+ for item in "$@"; do
100
+ if process_item "$item"; then
101
+ count=$((count + 1))
102
+ else
103
+ batch_add_error ERRORS "Failed to process: ${item}"
104
+ fi
105
+ done
106
+
107
+ # Add summary results
108
+ batch_add_result RESULTS "total" "$total"
109
+ batch_add_result RESULTS "processed" "$count"
110
+ batch_add_result RESULTS "failed" "$(($# - count))"
111
+
112
+ ###
113
+ ### :::: OUTPUT RESULTS :::: #########
114
+ ###
115
+
116
+ batch_add_metadata METADATA "completed" "$(date -Iseconds)"
117
+ batch_output RESULTS METADATA ERRORS
118
+ }
119
+
120
+ ###
121
+ ### :::: ENTRY POINT :::: #############
122
+ ###
123
+
124
+ main "$@"
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # Example: Multi-stage data pipeline
3
+ # Description: Extract log entries, transform them, aggregate by status code
4
+ # Usage: ./data-pipeline.sh [log_file]
5
+ #
6
+ # shellcheck disable=SC1091 # dynamic source paths resolved at runtime
7
+ # shellcheck disable=SC2034
8
+
9
+ set -euo pipefail
10
+
11
+ SCRIPT_NAME="${0##*/}"
12
+
13
+ # Source batch utilities
14
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
15
+ source "${CLAUDE_PLUGIN_ROOT}/scripts/lib-batch.sh"
16
+ elif [[ -f "$(dirname "$0")/../../scripts/lib-batch.sh" ]]; then
17
+ source "$(dirname "$0")/../../scripts/lib-batch.sh"
18
+ else
19
+ echo "Error: Cannot find lib-batch.sh" >&2
20
+ exit 2
21
+ fi
22
+
23
+ # Configuration
24
+ LOG_FILE="${1:-access.log}"
25
+ STATUS_PATTERN="${STATUS_PATTERN:-'^[0-9]{3}$'}"
26
+
27
+ # Stage 1: Extract status codes from log file
28
+ # Assumes Combined Log Format or similar
29
+ function stage1_extract() {
30
+ local log_file="$1"
31
+
32
+ batch_progress "Stage 1: Extracting status codes from ${log_file}"
33
+
34
+ if [[ ! -r "$log_file" ]]; then
35
+ batch_add_error ERRORS "Cannot read log file: ${log_file}"
36
+ return 1
37
+ fi
38
+
39
+ # Extract status codes (assumes standard log format with status at position 9)
40
+ awk '{print $9}' "$log_file" | grep -E "$STATUS_PATTERN" || true
41
+ }
42
+
43
+ # Stage 2: Transform and count status codes
44
+ function stage2_transform() {
45
+ batch_progress "Stage 2: Aggregating status codes"
46
+
47
+ local -A status_counts
48
+ local count=0
49
+
50
+ while IFS= read -r status; do
51
+ if [[ -n "$status" ]]; then
52
+ status_counts["$status"]=$((${status_counts["$status"]:-0} + 1))
53
+ count=$((count + 1))
54
+ fi
55
+ done
56
+
57
+ # Output results
58
+ for status in "${!status_counts[@]}"; do
59
+ printf '%s|%s\n' "$status" "${status_counts[$status]}"
60
+ done | sort -t '|' -k2 -nr
61
+ }
62
+
63
+ # Stage 3: Calculate statistics
64
+ function stage3_analyze() {
65
+ batch_progress "Stage 3: Calculating statistics"
66
+
67
+ local total_requests=0
68
+ local success_requests=0
69
+ local redirect_requests=0
70
+ local error_requests=0
71
+
72
+ while IFS='|' read -r status count; do
73
+ total_requests=$((total_requests + count))
74
+
75
+ case "$status" in
76
+ 2*)
77
+ success_requests=$((success_requests + count))
78
+ ;;
79
+ 3*)
80
+ redirect_requests=$((redirect_requests + count))
81
+ ;;
82
+ 4* | 5*)
83
+ error_requests=$((error_requests + count))
84
+ ;;
85
+ esac
86
+ done
87
+
88
+ printf '%s|%s|%s|%s\n' "$total_requests" "$success_requests" "$redirect_requests" "$error_requests"
89
+ }
90
+
91
+ # Main processing pipeline
92
+ function main() {
93
+ declare -A RESULTS
94
+ declare -a METADATA
95
+ declare -a ERRORS
96
+
97
+ # Metadata
98
+ batch_add_metadata METADATA "script" "$SCRIPT_NAME"
99
+ batch_add_metadata METADATA "log_file" "$LOG_FILE"
100
+ batch_add_metadata METADATA "started" "$(date -Iseconds)"
101
+
102
+ local temp_extract
103
+ local temp_transform
104
+ temp_extract=$(mktemp)
105
+ temp_transform=$(mktemp)
106
+ trap 'rm -f "$temp_extract" "$temp_transform"' EXIT
107
+
108
+ # STAGE 1: Extract
109
+ if ! stage1_extract "$LOG_FILE" >"$temp_extract"; then
110
+ batch_add_metadata METADATA "completed" "$(date -Iseconds)"
111
+ batch_add_result RESULTS "success" "false"
112
+ batch_output RESULTS METADATA ERRORS
113
+ return 1
114
+ fi
115
+
116
+ local extracted_count
117
+ extracted_count=$(wc -l <"$temp_extract")
118
+ batch_add_result RESULTS "extracted_entries" "$extracted_count"
119
+
120
+ # STAGE 2: Transform
121
+ stage2_transform <"$temp_extract" >"$temp_transform"
122
+
123
+ # Store top status codes
124
+ local index=0
125
+ while IFS='|' read -r status count && ((index < 10)); do
126
+ batch_add_result RESULTS "status_${status}" "$count"
127
+ index=$((index + 1))
128
+ done <"$temp_transform"
129
+
130
+ # STAGE 3: Analyze
131
+ while IFS='|' read -r total success redirect error; do
132
+ batch_add_result RESULTS "total_requests" "$total"
133
+ batch_add_result RESULTS "success_requests" "$success"
134
+ batch_add_result RESULTS "redirect_requests" "$redirect"
135
+ batch_add_result RESULTS "error_requests" "$error"
136
+
137
+ # Calculate percentages
138
+ if ((total > 0)); then
139
+ local success_pct=$((success * 100 / total))
140
+ local redirect_pct=$((redirect * 100 / total))
141
+ local error_pct=$((error * 100 / total))
142
+
143
+ batch_add_result RESULTS "success_percent" "$success_pct"
144
+ batch_add_result RESULTS "redirect_percent" "$redirect_pct"
145
+ batch_add_result RESULTS "error_percent" "$error_pct"
146
+ fi
147
+ done < <(stage3_analyze <"$temp_transform")
148
+
149
+ # Complete metadata
150
+ batch_add_metadata METADATA "completed" "$(date -Iseconds)"
151
+ batch_add_result RESULTS "success" "true"
152
+
153
+ # Output JSON
154
+ batch_output RESULTS METADATA ERRORS
155
+ }
156
+
157
+ main "$@"