@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,165 @@
|
|
|
1
|
+
# Example: Debugging an Unbound Variable Error
|
|
2
|
+
|
|
3
|
+
## The Failing Script
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
#!/usr/bin/env bash
|
|
7
|
+
# deploy.sh - Deploy application to environment
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
ENV="${1:-}"
|
|
11
|
+
DRY_RUN="${2:-false}"
|
|
12
|
+
|
|
13
|
+
validate_env() {
|
|
14
|
+
local env="$1"
|
|
15
|
+
case "$env" in
|
|
16
|
+
staging|production) ;;
|
|
17
|
+
*)
|
|
18
|
+
echo "Error: invalid environment '$env'" >&2
|
|
19
|
+
return 1
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
build_image() {
|
|
25
|
+
local tag="$1"
|
|
26
|
+
docker build -t "$tag" .
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
push_image() {
|
|
30
|
+
local tag="$1"
|
|
31
|
+
docker push "$tag"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
deploy() {
|
|
35
|
+
local env="$1"
|
|
36
|
+
local tag="myapp:${COMMIT_HASH}"
|
|
37
|
+
build_image "$tag"
|
|
38
|
+
push_image "$tag"
|
|
39
|
+
kubectl set image "deployment/myapp-${env}" "myapp=${tag}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Main
|
|
43
|
+
validate_env "$ENV"
|
|
44
|
+
|
|
45
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
46
|
+
echo "DRY RUN: would deploy to $ENV"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
deploy "$ENV"
|
|
51
|
+
echo "Deployed to $ENV"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Step 1: Observe the Error
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
$ bash deploy.sh staging
|
|
58
|
+
deploy.sh: line 42: COMMIT_HASH: unbound variable
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The script exits with an unbound variable error. The message tells us the variable name (`COMMIT_HASH`) and the line number (42, inside `deploy()`).
|
|
62
|
+
|
|
63
|
+
Exit code:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$ echo $?
|
|
67
|
+
1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Step 2: Syntax Check
|
|
71
|
+
|
|
72
|
+
Before investigating further, confirm the script has no syntax errors:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ bash -n deploy.sh
|
|
76
|
+
# No output -- syntax is valid
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The problem is not a syntax issue. It is a runtime error caused by `set -u` (nounset) combined with a missing variable.
|
|
80
|
+
|
|
81
|
+
## Step 3: Enable Trace Mode
|
|
82
|
+
|
|
83
|
+
Add `set -x` to see the execution flow leading to the failure:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
$ bash -x deploy.sh staging
|
|
87
|
+
+ ENV=staging
|
|
88
|
+
+ DRY_RUN=false
|
|
89
|
+
+ validate_env staging
|
|
90
|
+
+ local env=staging
|
|
91
|
+
+ case staging in
|
|
92
|
+
+ return 0
|
|
93
|
+
+ [[ false == \t\r\u\e ]]
|
|
94
|
+
+ deploy staging
|
|
95
|
+
+ local env=staging
|
|
96
|
+
+ local tag=myapp:
|
|
97
|
+
deploy.sh: line 42: COMMIT_HASH: unbound variable
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The trace shows `tag=myapp:` -- the `COMMIT_HASH` variable is empty at expansion time, and because of `set -u`, bash treats it as an error rather than expanding to an empty string.
|
|
101
|
+
|
|
102
|
+
The root cause is clear: `COMMIT_HASH` is never set in the script. It was presumably meant to be injected from the environment or derived from git.
|
|
103
|
+
|
|
104
|
+
## Step 4: ShellCheck for Additional Issues
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ shellcheck deploy.sh
|
|
108
|
+
|
|
109
|
+
In deploy.sh line 42:
|
|
110
|
+
local tag="myapp:${COMMIT_HASH}"
|
|
111
|
+
^---------^ SC2153: Possible misspelling: COMMIT_HASH (not declared)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
ShellCheck confirms the variable is undeclared. It also catches the missing guard.
|
|
115
|
+
|
|
116
|
+
## Step 5: Apply the Fix
|
|
117
|
+
|
|
118
|
+
The variable should either be derived or have a sensible default:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Option A: Derive from git
|
|
122
|
+
COMMIT_HASH="${COMMIT_HASH:-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}"
|
|
123
|
+
|
|
124
|
+
# Option B: Require it explicitly
|
|
125
|
+
if [[ -z "${COMMIT_HASH:-}" ]]; then
|
|
126
|
+
echo "Error: COMMIT_HASH must be set" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
After applying option A near the top of the script:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
#!/usr/bin/env bash
|
|
135
|
+
# deploy.sh - Deploy application to environment
|
|
136
|
+
set -euo pipefail
|
|
137
|
+
|
|
138
|
+
ENV="${1:-}"
|
|
139
|
+
DRY_RUN="${2:-false}"
|
|
140
|
+
COMMIT_HASH="${COMMIT_HASH:-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Step 6: Verify the Fix
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
$ bash deploy.sh staging true
|
|
147
|
+
DRY RUN: would deploy to staging
|
|
148
|
+
|
|
149
|
+
$ bash -x deploy.sh staging true
|
|
150
|
+
+ ENV=staging
|
|
151
|
+
+ DRY_RUN=true
|
|
152
|
+
++ git rev-parse --short HEAD
|
|
153
|
+
+ COMMIT_HASH=a1b2c3d
|
|
154
|
+
+ validate_env staging
|
|
155
|
+
+ local env=staging
|
|
156
|
+
+ case staging in
|
|
157
|
+
+ return 0
|
|
158
|
+
+ [[ true == \t\r\u\e ]]
|
|
159
|
+
+ echo 'DRY RUN: would deploy to staging'
|
|
160
|
+
DRY RUN: would deploy to staging
|
|
161
|
+
+ exit 0
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The script now resolves `COMMIT_HASH` from git and completes without errors.
|
|
165
|
+
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Shell Script Debugging Guide
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
### Enable Debug Output
|
|
6
|
+
```bash
|
|
7
|
+
# Print each command before execution
|
|
8
|
+
set -x
|
|
9
|
+
|
|
10
|
+
# Print script lines as read
|
|
11
|
+
set -v
|
|
12
|
+
|
|
13
|
+
# Show function calls in trace
|
|
14
|
+
set -o functrace
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Disable Debug Output
|
|
18
|
+
```bash
|
|
19
|
+
set +x
|
|
20
|
+
set +v
|
|
21
|
+
set +o functrace
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Syntax Check
|
|
25
|
+
```bash
|
|
26
|
+
# Parse without executing
|
|
27
|
+
bash -n script.sh
|
|
28
|
+
|
|
29
|
+
# Check all .sh files
|
|
30
|
+
for f in *.sh; do bash -n "$f"; done
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Debugging Checklist
|
|
34
|
+
|
|
35
|
+
Use this checklist when troubleshooting:
|
|
36
|
+
|
|
37
|
+
### Environment Issues
|
|
38
|
+
- [ ] Correct shell? (`echo $SHELL`, `ps -p $$`)
|
|
39
|
+
- [ ] Bash version compatible? (`bash --version`)
|
|
40
|
+
- [ ] Required tools installed? (`command -v toolname`)
|
|
41
|
+
- [ ] PATH correct? (`echo $PATH`)
|
|
42
|
+
- [ ] Environment variables set? (`env | grep VAR`)
|
|
43
|
+
|
|
44
|
+
### Syntax Issues
|
|
45
|
+
- [ ] Run `bash -n script.sh` for syntax errors
|
|
46
|
+
- [ ] Check paired quotes: `'` and `"`
|
|
47
|
+
- [ ] Check paired brackets: `(`, `[`, `{`, `((`, `[[`
|
|
48
|
+
- [ ] Check line continuation: `\` at end of lines
|
|
49
|
+
- [ ] No Windows line endings? (`file script.sh`)
|
|
50
|
+
|
|
51
|
+
### Variable Issues
|
|
52
|
+
- [ ] Variable defined before use?
|
|
53
|
+
- [ ] Variable scope correct? (`local` vs global)
|
|
54
|
+
- [ ] Proper quoting? `"$var"` not `$var`
|
|
55
|
+
- [ ] Default value needed? `${var:-default}`
|
|
56
|
+
- [ ] Indirect reference correct? `${!varname}`
|
|
57
|
+
|
|
58
|
+
### Logic Issues
|
|
59
|
+
- [ ] Correct comparison operator?
|
|
60
|
+
- Strings: `[[ "$a" == "$b" ]]` or `[ "$a" = "$b" ]`
|
|
61
|
+
- Numbers: `(( a == b ))` or `[ "$a" -eq "$b" ]`
|
|
62
|
+
- [ ] Test returns expected value? `echo $?`
|
|
63
|
+
- [ ] Exit codes correct? 0=success, non-zero=failure
|
|
64
|
+
- [ ] Pipeline error handled? `set -o pipefail`
|
|
65
|
+
|
|
66
|
+
### File Issues
|
|
67
|
+
- [ ] File exists? `[[ -f "$file" ]]`
|
|
68
|
+
- [ ] Readable? `[[ -r "$file" ]]`
|
|
69
|
+
- [ ] Correct path? Absolute vs relative
|
|
70
|
+
- [ ] Permission problems? `ls -l "$file"`
|
|
71
|
+
|
|
72
|
+
## Common Patterns
|
|
73
|
+
|
|
74
|
+
### Debug Function
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
debug() {
|
|
78
|
+
if [[ "${DEBUG:-0}" == "1" ]]; then
|
|
79
|
+
echo "[DEBUG] $*" >&2
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Usage
|
|
84
|
+
DEBUG=1
|
|
85
|
+
debug "Variable value: $var"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Debug Section
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Enable debug for a section only
|
|
92
|
+
debug_section() {
|
|
93
|
+
local PS4='+ ${BASH_SOURCE}:${LINENO}: '
|
|
94
|
+
set -x
|
|
95
|
+
# Problematic code here
|
|
96
|
+
"$@"
|
|
97
|
+
set +x
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
debug_section your_function arg1 arg2
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Trace Variable Changes
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Monitor variable assignment
|
|
107
|
+
# Note: eval is used here for diagnostic purposes only.
|
|
108
|
+
# Do not copy this pattern into production scripts.
|
|
109
|
+
trace_var() {
|
|
110
|
+
local var_name="$1"
|
|
111
|
+
local old_value="${!var_name}"
|
|
112
|
+
local new_value="$2"
|
|
113
|
+
|
|
114
|
+
echo "[TRACE] $var_name: '$old_value' -> '$new_value'" >&2
|
|
115
|
+
eval "$var_name='$new_value'"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Usage
|
|
119
|
+
trace_var myvar "new value"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Debugging Specific Issues
|
|
123
|
+
|
|
124
|
+
### "Command not found"
|
|
125
|
+
|
|
126
|
+
**Symptoms**: Script fails with `command: not found`
|
|
127
|
+
|
|
128
|
+
**Checklist**:
|
|
129
|
+
1. Verify command spelling
|
|
130
|
+
2. Check if command exists: `which command` or `command -v command`
|
|
131
|
+
3. Verify PATH: `echo $PATH`
|
|
132
|
+
4. Check if alias/function shadowing: `type command`
|
|
133
|
+
|
|
134
|
+
**Solution**:
|
|
135
|
+
```bash
|
|
136
|
+
# Use absolute path
|
|
137
|
+
/usr/bin/python3 script.py
|
|
138
|
+
|
|
139
|
+
# Or verify command exists first
|
|
140
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
141
|
+
echo "Error: python3 not found" >&2
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### "Unbound variable"
|
|
147
|
+
|
|
148
|
+
**Symptoms**: Script fails with `unbound variable` when using `set -u`
|
|
149
|
+
|
|
150
|
+
**Checklist**:
|
|
151
|
+
1. Variable used before definition?
|
|
152
|
+
2. Empty array causing issue?
|
|
153
|
+
3. Conditional variable not set?
|
|
154
|
+
|
|
155
|
+
**Solution**:
|
|
156
|
+
```bash
|
|
157
|
+
# Provide default value
|
|
158
|
+
echo "${var:-default}"
|
|
159
|
+
|
|
160
|
+
# Or allow empty
|
|
161
|
+
echo "${var:-}"
|
|
162
|
+
|
|
163
|
+
# Or check first
|
|
164
|
+
if [[ -n "${var:-}" ]]; then
|
|
165
|
+
echo "$var"
|
|
166
|
+
fi
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Pipeline fails silently
|
|
170
|
+
|
|
171
|
+
**Symptoms**: Error in middle of pipeline doesn't cause exit
|
|
172
|
+
|
|
173
|
+
**Solution**:
|
|
174
|
+
```bash
|
|
175
|
+
# Enable pipefail
|
|
176
|
+
set -euo pipefail
|
|
177
|
+
|
|
178
|
+
# Or capture pipeline status
|
|
179
|
+
command1 | command2
|
|
180
|
+
pipeline_status=(${PIPESTATUS[@]})
|
|
181
|
+
if [[ ${pipeline_status[0]} -ne 0 ]]; then
|
|
182
|
+
echo "command1 failed" >&2
|
|
183
|
+
fi
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Subshell loses variables
|
|
187
|
+
|
|
188
|
+
**Symptoms**: Variables set in pipeline/subshell not available after
|
|
189
|
+
|
|
190
|
+
**Solution**:
|
|
191
|
+
```bash
|
|
192
|
+
# BAD - var lost
|
|
193
|
+
echo "data" | while read line; do
|
|
194
|
+
var="$line"
|
|
195
|
+
done
|
|
196
|
+
echo "$var" # Empty
|
|
197
|
+
|
|
198
|
+
# GOOD - var preserved
|
|
199
|
+
while IFS= read -r line; do
|
|
200
|
+
var="$line"
|
|
201
|
+
done < <(echo "data")
|
|
202
|
+
echo "$var" # Works
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Whitespace breaking arguments
|
|
206
|
+
|
|
207
|
+
**Symptoms**: Filename with spaces causing errors
|
|
208
|
+
|
|
209
|
+
**Solution**:
|
|
210
|
+
```bash
|
|
211
|
+
# BAD
|
|
212
|
+
for file in *.txt; do
|
|
213
|
+
mv $file /dest/
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
# GOOD
|
|
217
|
+
for file in *.txt; do
|
|
218
|
+
mv "$file" /dest/
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
# GOOD for find
|
|
222
|
+
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
|
|
223
|
+
mv "$file" /dest/
|
|
224
|
+
done
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### "Wrong branch" in && || chain
|
|
228
|
+
|
|
229
|
+
**Symptoms**: The `||` fallback command runs unexpectedly, even when the initial command succeeded
|
|
230
|
+
|
|
231
|
+
**Checklist**:
|
|
232
|
+
1. Is there a `cmd1 && cmd2 || cmd3` pattern?
|
|
233
|
+
2. Did `cmd2` fail even though `cmd1` succeeded?
|
|
234
|
+
3. The `||` triggers on ANY failure in the chain, not just `cmd1`
|
|
235
|
+
|
|
236
|
+
**Solution**:
|
|
237
|
+
```bash
|
|
238
|
+
# BAD - if cmd2 fails, cmd3 runs regardless of cmd1
|
|
239
|
+
deploy && healthcheck || rollback
|
|
240
|
+
|
|
241
|
+
# GOOD - use explicit if/else
|
|
242
|
+
if deploy && healthcheck; then
|
|
243
|
+
echo "OK"
|
|
244
|
+
else
|
|
245
|
+
rollback
|
|
246
|
+
fi
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Numeric comparison gives wrong result
|
|
250
|
+
|
|
251
|
+
**Symptoms**: A numeric comparison behaves unexpectedly (e.g., "9 is less than 7")
|
|
252
|
+
|
|
253
|
+
**Checklist**:
|
|
254
|
+
1. Does the comparison use `>` or `<` inside `[[ ]]`?
|
|
255
|
+
2. `>` inside `[[ ]]` is lexicographic, not numeric: `"9" < "7"` as strings
|
|
256
|
+
3. Does the comparison use `-gt`/`-lt` in `(( ))`? Those are wrong — `(( ))` uses `>`/`<`
|
|
257
|
+
|
|
258
|
+
**Solution**:
|
|
259
|
+
```bash
|
|
260
|
+
# BAD - lexicographic comparison
|
|
261
|
+
[[ $count > 7 ]] # "9" is NOT greater than "7" as strings
|
|
262
|
+
|
|
263
|
+
# GOOD - arithmetic context
|
|
264
|
+
(( count > 7 ))
|
|
265
|
+
|
|
266
|
+
# GOOD - or test operators inside [[ ]]
|
|
267
|
+
[[ $count -gt 7 ]]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Advanced Debugging
|
|
271
|
+
|
|
272
|
+
### Custom PS4 for Better Traces
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Custom trace prompt
|
|
276
|
+
export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:-main}: '
|
|
277
|
+
|
|
278
|
+
set -x
|
|
279
|
+
# Your code here
|
|
280
|
+
set +x
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Logging All Calls
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# Log every function call
|
|
287
|
+
declare -A call_log
|
|
288
|
+
call_count() {
|
|
289
|
+
local func="${FUNCNAME[1]}"
|
|
290
|
+
((call_log[$func]++)) || true
|
|
291
|
+
echo "Call $func: ${call_log[$func]}" >&2
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
trap 'call_count' DEBUG
|
|
295
|
+
|
|
296
|
+
# ... script code ...
|
|
297
|
+
|
|
298
|
+
trap - DEBUG # Disable
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Timing Execution
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Time a section
|
|
305
|
+
start=$(date +%s.%N)
|
|
306
|
+
# ... code ...
|
|
307
|
+
end=$(date +%s.%N)
|
|
308
|
+
duration=$(echo "$end - $start" | bc)
|
|
309
|
+
echo "Took: $duration seconds"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> For comprehensive performance profiling with PS4 timing, BASH_XTRACEFD, strace analysis, and benchmarking workflows, see the `shell-profiling` skill.
|
|
313
|
+
|
|
314
|
+
## ShellCheck Quick Reference
|
|
315
|
+
|
|
316
|
+
Common ShellCheck warnings and fixes:
|
|
317
|
+
|
|
318
|
+
| SC Code | Meaning | Fix |
|
|
319
|
+
|---------|---------|-----|
|
|
320
|
+
| SC2086 | Double quote to prevent globbing | Use `"$var"` |
|
|
321
|
+
| SC2039 | In POSIX sh | Replace bashism |
|
|
322
|
+
| SC2164 | Use `cd ... || exit` | Add error handling |
|
|
323
|
+
| SC2155 | Declare and assign separately (masks return value) | Declare, then assign: `local x; x="$(cmd)"` |
|
|
324
|
+
| SC2002 | Useless use of cat | Pass the file directly: `grep pattern file` |
|
|
325
|
+
| SC2046 | Quote to prevent word splitting | `"$(cmd)"` |
|
|
326
|
+
| SC1091 | File not found | Fix path or disable |
|
|
327
|
+
| SC2206 | Word splitting when filling an array | `read -ra arr <<< "$s"` or `mapfile -t arr` |
|
|
328
|
+
|
|
329
|
+
Run ShellCheck:
|
|
330
|
+
```bash
|
|
331
|
+
shellcheck script.sh
|
|
332
|
+
# Or with specific format
|
|
333
|
+
shellcheck -f gcc script.sh
|
|
334
|
+
# Or ignore specific rules
|
|
335
|
+
shellcheck -e SC2039 script.sh
|
|
336
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shell-profiling
|
|
3
|
+
description: Profile a slow-but-correct bash script: measure a baseline, trace with xtrace timing to find the hotspot, apply shell-specific optimisations, and benchmark the result. Use when a script works correctly but runs too slowly, or to measure/compare execution speed ("profile this script", "why is my script slow", "find the bottleneck"). For runtime errors use shell-debugging; for quality review use shell-review.
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Grep
|
|
7
|
+
- Glob
|
|
8
|
+
- Bash
|
|
9
|
+
argument-hint: [script-path]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Shell Profiling Skill
|
|
13
|
+
|
|
14
|
+
Guides systematic performance profiling of bash scripts to identify bottlenecks and apply targeted optimisations.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
This skill handles **performance problems** -- scripts that work correctly but run too slowly. It covers:
|
|
19
|
+
|
|
20
|
+
- Timing measurement at multiple granularities (whole-script, per-section, per-line)
|
|
21
|
+
- Xtrace-based profiling with microsecond-precision timestamps
|
|
22
|
+
- Syscall and library-call analysis
|
|
23
|
+
- Statistical benchmarking and comparison
|
|
24
|
+
- Shell-specific optimisation patterns (subshells, builtins, I/O, loops, string processing)
|
|
25
|
+
|
|
26
|
+
This skill does **not** cover:
|
|
27
|
+
|
|
28
|
+
- Runtime failures or error diagnosis -- use `shell-debugging`
|
|
29
|
+
- General code quality or style -- use `shell-review`
|
|
30
|
+
- Writing standards or scaffolding -- use `shell-best-practices`
|
|
31
|
+
|
|
32
|
+
## Workflow
|
|
33
|
+
|
|
34
|
+
The profiling cycle is: **measure the original, instrument a temporary copy to diagnose, discard the copy, apply fixes to the original, benchmark the result.** Never leave profiling instrumentation in the target script.
|
|
35
|
+
|
|
36
|
+
### 1. Define Target
|
|
37
|
+
|
|
38
|
+
Before profiling, decide what acceptable performance looks like (e.g., "under 5 seconds" or "twice as fast"). This prevents over-optimisation and gives a clear stopping condition.
|
|
39
|
+
|
|
40
|
+
### 2. Measure baseline
|
|
41
|
+
|
|
42
|
+
Run the original script with `time` to establish a baseline. No modifications to the script:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
time bash script.sh args
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For per-section granularity, use `EPOCHREALTIME` inside the script (add temporarily, remove after measurement). See `references/profiling-tools.md` for all timing methods and their trade-offs.
|
|
49
|
+
|
|
50
|
+
### 3. Trace with Timing
|
|
51
|
+
|
|
52
|
+
Create a temporary copy of the script and add xtrace instrumentation to it. The original script remains untouched:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cp script.sh /tmp/script.profiling.sh
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Insert after the shebang and `set` lines in the copy:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
exec 42>/tmp/trace.log
|
|
62
|
+
BASH_XTRACEFD=42
|
|
63
|
+
PS4='+ ${EPOCHREALTIME} ${BASH_SOURCE}:${LINENO} ${FUNCNAME[0]:-main} '
|
|
64
|
+
set -x
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run the instrumented copy, then discard it. See `references/profiling-tools.md` for BASH_XTRACEFD setup and capture patterns.
|
|
68
|
+
|
|
69
|
+
### 4. Identify Hotspot
|
|
70
|
+
|
|
71
|
+
Process the trace output to find the lines consuming the most time. Use `scripts/trace-aggregate.sh` for deterministic aggregation:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
scripts/trace-aggregate.sh /tmp/trace.log
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Output shows cumulative time per source location with call counts, sorted by descending time.
|
|
78
|
+
|
|
79
|
+
Look for locations with the highest cumulative time and call counts, particularly tight loops, subshell invocations, or external command calls.
|
|
80
|
+
|
|
81
|
+
### 5. Deep-dive
|
|
82
|
+
|
|
83
|
+
When the hotspot involves system calls or I/O, use deeper analysis tools. These run against the original script without modification:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Syscall summary (Linux)
|
|
87
|
+
strace -c -f bash script.sh
|
|
88
|
+
|
|
89
|
+
# Detailed resource usage
|
|
90
|
+
/usr/bin/time -v bash script.sh
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See `references/profiling-tools.md` for platform-specific alternatives (macOS: `dtruss`, `sample`, Instruments).
|
|
94
|
+
|
|
95
|
+
### 6. Apply Optimisation
|
|
96
|
+
|
|
97
|
+
Apply fixes to the **original script** (not the instrumented copy). Consult `references/optimisation-patterns.md` for shell-specific fixes:
|
|
98
|
+
|
|
99
|
+
- Subshell elimination -- replace `$(cmd)` with parameter expansion
|
|
100
|
+
- Builtin selection -- use bash builtins over external commands
|
|
101
|
+
- I/O reduction -- batch reads, redirect once, avoid unnecessary pipes
|
|
102
|
+
- Loop tuning -- process substitution, `lastpipe`, pre-allocated arrays
|
|
103
|
+
- String processing -- single-pass awk vs multi-tool pipelines
|
|
104
|
+
|
|
105
|
+
### 7. Benchmark
|
|
106
|
+
|
|
107
|
+
Measure the improvement statistically against the original baseline:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# With hyperfine (recommended)
|
|
111
|
+
hyperfine 'bash script_before.sh' 'bash script_after.sh'
|
|
112
|
+
|
|
113
|
+
# With bench.sh (no hyperfine required)
|
|
114
|
+
scripts/bench.sh -r 10 -w 1 -- bash script_before.sh
|
|
115
|
+
scripts/bench.sh -r 10 -w 1 -- bash script_after.sh
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Discard the first run (warm-up), compare median not mean.
|
|
119
|
+
|
|
120
|
+
If the result still misses the target set in step 1, return to step 3 with the next hotspot; stop once the target is met or all viable patterns are exhausted.
|
|
121
|
+
|
|
122
|
+
### 8. Report
|
|
123
|
+
|
|
124
|
+
Present a before/after comparison:
|
|
125
|
+
|
|
126
|
+
- Baseline timing vs optimised timing
|
|
127
|
+
- Percentage improvement
|
|
128
|
+
- Which patterns were applied
|
|
129
|
+
- Any trade-offs introduced (readability, portability)
|
|
130
|
+
|
|
131
|
+
Clean up temporary files (`/tmp/trace.log`, `/tmp/script.profiling.sh`).
|
|
132
|
+
|
|
133
|
+
## References
|
|
134
|
+
|
|
135
|
+
- `references/profiling-tools.md` -- Comprehensive catalogue of timing, tracing, syscall, and benchmarking tools with syntax examples, output interpretation, and platform availability
|
|
136
|
+
- `references/optimisation-patterns.md` -- Shell-specific performance patterns with before/after code snippets, covering subshells, builtins, I/O, loops, and string processing
|
|
137
|
+
|
|
138
|
+
Always read all references, scripts, and examples before producing output.
|
|
139
|
+
|
|
140
|
+
## Scripts
|
|
141
|
+
|
|
142
|
+
- `scripts/trace-aggregate.sh` -- Aggregates xtrace timestamps into cumulative per-line timings with call counts. Usage: `trace-aggregate.sh <trace-file> [top-n]`
|
|
143
|
+
- `scripts/bench.sh` -- Manual benchmark harness with warm-up runs, median/min/max/spread statistics. Usage: `bench.sh [-r runs] [-w warmup] [--] <command...>`
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
- `examples/profile-session.md` -- End-to-end walkthrough: profiling a slow log-processing script from baseline measurement through hotspot identification, optimisation, and benchmarking
|
|
148
|
+
|
|
149
|
+
## Integration
|
|
150
|
+
|
|
151
|
+
- **`shell-debugging`** -- For scripts that produce errors or incorrect output (profiling assumes the script works correctly)
|
|
152
|
+
- **`shell-best-practices`** -- General writing standards, quoting, error handling (apply alongside performance optimisations)
|
|
153
|
+
- **`shell-review`** -- Quality assessment after optimisation is complete
|
|
154
|
+
- **`shell-architect`** agent -- Architectural decisions affecting performance (batch vs individual processing, parallelism strategy, data flow)
|