@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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/agents/shell-architect.md +88 -0
- package/agents/shell-expert.md +60 -0
- package/commands/shell-audit.md +47 -0
- package/commands/shell-batch-exec.md +48 -0
- package/commands/shell-new.md +57 -0
- package/commands/shell-routines-setup.md +66 -0
- package/commands/shell-test-run.md +46 -0
- package/opencode.json +19 -0
- package/package.json +34 -0
- package/plugins/shell-hooks.ts +150 -0
- package/scripts/lib-batch.sh +297 -0
- package/scripts/lib-common.sh +332 -0
- package/skills/shell-batch-operations/SKILL.md +97 -0
- package/skills/shell-batch-operations/assets/batch-template.sh +124 -0
- package/skills/shell-batch-operations/examples/data-pipeline.sh +157 -0
- package/skills/shell-batch-operations/examples/file-batch.sh +140 -0
- package/skills/shell-batch-operations/references/decision-tree.md +53 -0
- package/skills/shell-best-practices/SKILL.md +313 -0
- package/skills/shell-best-practices/assets/library.sh +142 -0
- package/skills/shell-best-practices/assets/minimal.sh +54 -0
- package/skills/shell-best-practices/assets/posix.sh +180 -0
- package/skills/shell-best-practices/assets/standard.sh +203 -0
- package/skills/shell-best-practices/references/patterns.md +386 -0
- package/skills/shell-best-practices/references/security.md +195 -0
- package/skills/shell-debugging/SKILL.md +115 -0
- package/skills/shell-debugging/examples/debug-session.md +165 -0
- package/skills/shell-debugging/references/debugging-guide.md +336 -0
- package/skills/shell-profiling/SKILL.md +154 -0
- package/skills/shell-profiling/examples/profile-session.md +225 -0
- package/skills/shell-profiling/references/optimisation-patterns.md +373 -0
- package/skills/shell-profiling/references/profiling-tools.md +318 -0
- package/skills/shell-profiling/scripts/bench.sh +82 -0
- package/skills/shell-profiling/scripts/trace-aggregate.sh +34 -0
- package/skills/shell-review/SKILL.md +61 -0
- package/skills/shell-review/examples/sample-review.md +42 -0
- package/skills/shell-review/references/guidelines.md +48 -0
- package/skills/shell-review/references/review-template.md +56 -0
- package/skills/shell-security/SKILL.md +128 -0
- package/skills/shell-security/examples/dangerous-command-review.md +231 -0
- package/skills/shell-security/examples/secure-script-example.sh +317 -0
- package/skills/shell-security/references/dangerous-commands.md +561 -0
- package/skills/shell-security/references/security-patterns.md +30 -0
- package/skills/shell-security/references/sensitive-files.md +525 -0
- package/skills/shell-security/scripts/security-audit.sh +208 -0
- package/skills/shell-test/SKILL.md +237 -0
- package/skills/shell-test/examples/test-example.md +74 -0
- package/skills/shell-test/references/advanced-patterns.md +52 -0
- package/skills/shell-test/references/assertions.md +184 -0
- package/skills/shell-test/references/test-template.md +60 -0
- 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 "$@"
|