@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,386 @@
|
|
|
1
|
+
# Common Bash Patterns
|
|
2
|
+
|
|
3
|
+
## Argument Parsing
|
|
4
|
+
|
|
5
|
+
### Basic positional arguments
|
|
6
|
+
```bash
|
|
7
|
+
#!/usr/bin/env bash
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
echo "Usage: $0 <input> <output>" >&2
|
|
12
|
+
exit 2
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[[ $# -eq 2 ]] || usage
|
|
16
|
+
|
|
17
|
+
input="$1"
|
|
18
|
+
output="$2"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### getopts (flag-based)
|
|
22
|
+
```bash
|
|
23
|
+
#!/usr/bin/env bash
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
verbose=0
|
|
27
|
+
output_file=""
|
|
28
|
+
|
|
29
|
+
while getopts "vo:" opt; do
|
|
30
|
+
case "$opt" in
|
|
31
|
+
v) verbose=1 ;;
|
|
32
|
+
o) output_file="$OPTARG" ;;
|
|
33
|
+
\?) echo "Invalid option: -$OPTARG" >&2; exit 2 ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
shift $((OPTIND - 1))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Temporary Files
|
|
41
|
+
|
|
42
|
+
### Safe temp file with cleanup
|
|
43
|
+
```bash
|
|
44
|
+
tmp_file=$(mktemp)
|
|
45
|
+
trap 'rm -f "$tmp_file"' EXIT
|
|
46
|
+
|
|
47
|
+
# Process with temp file
|
|
48
|
+
process_data > "$tmp_file"
|
|
49
|
+
result=$(cat "$tmp_file")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Safe temp directory
|
|
53
|
+
```bash
|
|
54
|
+
tmp_dir=$(mktemp -d)
|
|
55
|
+
trap 'rm -rf "$tmp_dir"' EXIT
|
|
56
|
+
|
|
57
|
+
# Create files in temp directory
|
|
58
|
+
output="$tmp_dir/output.txt"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Arrays
|
|
62
|
+
|
|
63
|
+
### Iteration
|
|
64
|
+
```bash
|
|
65
|
+
items=("apple" "banana" "cherry")
|
|
66
|
+
for item in "${items[@]}"; do
|
|
67
|
+
echo "$item"
|
|
68
|
+
done
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Building array from command output
|
|
72
|
+
```bash
|
|
73
|
+
readarray -t files < <(find . -name "*.txt" -type f)
|
|
74
|
+
for file in "${files[@]}"; do
|
|
75
|
+
process "$file"
|
|
76
|
+
done
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Associative arrays (bash 4+)
|
|
80
|
+
```bash
|
|
81
|
+
declare -A config
|
|
82
|
+
config[host]="localhost"
|
|
83
|
+
config[port]="8080"
|
|
84
|
+
|
|
85
|
+
for key in "${!config[@]}"; do
|
|
86
|
+
echo "$key = ${config[$key]}"
|
|
87
|
+
done
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## String Manipulation
|
|
91
|
+
|
|
92
|
+
### Parameter expansion (bash 4+)
|
|
93
|
+
```bash
|
|
94
|
+
string="Hello, World!"
|
|
95
|
+
|
|
96
|
+
# Uppercase
|
|
97
|
+
echo "${string^^}" # HELLO, WORLD!
|
|
98
|
+
|
|
99
|
+
# Lowercase
|
|
100
|
+
echo "${string,,}" # hello, world!
|
|
101
|
+
|
|
102
|
+
# Remove suffix
|
|
103
|
+
path="/path/to/file.txt"
|
|
104
|
+
echo "${path%.txt}" # /path/to/file
|
|
105
|
+
|
|
106
|
+
# Remove prefix
|
|
107
|
+
echo "${path##*/}" # file.txt
|
|
108
|
+
|
|
109
|
+
# Replace first match
|
|
110
|
+
echo "${string/World/Bash}" # Hello, Bash!
|
|
111
|
+
|
|
112
|
+
# Replace all matches
|
|
113
|
+
echo "${string//l/L}" # HeLLo, WorLd!
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Use braces for variable expansion
|
|
117
|
+
```bash
|
|
118
|
+
# BAD - ambiguous without braces
|
|
119
|
+
echo "$var_name" # Is this ${var_name} or ${var}_name?
|
|
120
|
+
|
|
121
|
+
# GOOD - braces make intent explicit
|
|
122
|
+
echo "${var}_name"
|
|
123
|
+
echo "${var_name}"
|
|
124
|
+
|
|
125
|
+
# GOOD - braces required for array, length, and manipulation
|
|
126
|
+
echo "${items[@]}"
|
|
127
|
+
echo "${#items[@]}"
|
|
128
|
+
echo "${var:-default}"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Indirect expansion
|
|
132
|
+
```bash
|
|
133
|
+
# Access variable whose name is stored in another variable
|
|
134
|
+
var_name="colour"
|
|
135
|
+
colour="blue"
|
|
136
|
+
|
|
137
|
+
echo "${!var_name}" # blue
|
|
138
|
+
|
|
139
|
+
# Use case: dynamic config lookup
|
|
140
|
+
declare -A config
|
|
141
|
+
config[host]="localhost"
|
|
142
|
+
config[port]="8080"
|
|
143
|
+
|
|
144
|
+
for key in host port; do
|
|
145
|
+
echo "${config[$key]}"
|
|
146
|
+
done
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Numeric vs string comparison
|
|
150
|
+
```bash
|
|
151
|
+
# BAD - '>' inside [[ ]] is lexicographic: "9" < "7"
|
|
152
|
+
if [[ $count > 7 ]]; then ...
|
|
153
|
+
|
|
154
|
+
# GOOD - use (( )) for arithmetic
|
|
155
|
+
if (( count > 7 )); then ...
|
|
156
|
+
|
|
157
|
+
# GOOD - or -gt inside [[ ]]
|
|
158
|
+
if [[ $count -gt 7 ]]; then ...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Exact equality vs pattern matching in [[ ]]
|
|
162
|
+
```bash
|
|
163
|
+
bar="*.txt"
|
|
164
|
+
|
|
165
|
+
# BAD - unquoted RHS enables glob pattern matching
|
|
166
|
+
[[ "file.txt" = $bar ]] && echo yes # matches!
|
|
167
|
+
|
|
168
|
+
# GOOD - quote RHS for exact string equality
|
|
169
|
+
[[ "file.txt" = "$bar" ]] && echo yes # no match
|
|
170
|
+
|
|
171
|
+
# For intentional glob matching, leave RHS unquoted
|
|
172
|
+
[[ "file.txt" == *.txt ]] # matches
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### IFS splitting drops trailing empty fields
|
|
176
|
+
```bash
|
|
177
|
+
# BAD - trailing empty field silently lost
|
|
178
|
+
IFS=, read -ra arr <<< "a,b,"
|
|
179
|
+
echo "${#arr[@]}" # 2, not 3
|
|
180
|
+
|
|
181
|
+
# GOOD - use -d '' to preserve trailing empties
|
|
182
|
+
IFS=, read -d '' -ra arr <<< "a,b,"
|
|
183
|
+
echo "${#arr[@]}" # 3
|
|
184
|
+
|
|
185
|
+
# GOOD - for CSV rows, handle trailing empties explicitly
|
|
186
|
+
line="a,b,"
|
|
187
|
+
IFS=, read -ra arr <<< "$line"
|
|
188
|
+
expected=3
|
|
189
|
+
while (( ${#arr[@]} < expected )); do
|
|
190
|
+
arr+=("")
|
|
191
|
+
done
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## File Operations
|
|
195
|
+
|
|
196
|
+
### Check file existence and type
|
|
197
|
+
```bash
|
|
198
|
+
if [[ -f "$file" ]]; then
|
|
199
|
+
echo "Regular file exists"
|
|
200
|
+
elif [[ -d "$file" ]]; then
|
|
201
|
+
echo "Directory exists"
|
|
202
|
+
elif [[ -e "$file" ]]; then
|
|
203
|
+
echo "Something exists (not regular file or directory)"
|
|
204
|
+
else
|
|
205
|
+
echo "Does not exist"
|
|
206
|
+
fi
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Note**: `[[ -e ]]` returns false for broken symlinks. Use `[[ -L ]]` to test symlink existence regardless of target:
|
|
210
|
+
```bash
|
|
211
|
+
# BAD - broken symlinks report as non-existent
|
|
212
|
+
if [[ -e "$link" ]]; then ... fi # false for broken symlink
|
|
213
|
+
|
|
214
|
+
# GOOD - test the link itself
|
|
215
|
+
if [[ -L "$link" ]]; then ... fi # true for any symlink
|
|
216
|
+
|
|
217
|
+
# GOOD - check both link existence AND target
|
|
218
|
+
if [[ -L "$link" && -e "$link" ]]; then
|
|
219
|
+
echo "Symlink with valid target"
|
|
220
|
+
elif [[ -L "$link" ]]; then
|
|
221
|
+
echo "Broken symlink"
|
|
222
|
+
fi
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Read file line by line
|
|
226
|
+
```bash
|
|
227
|
+
while IFS= read -r line; do
|
|
228
|
+
process_line "$line"
|
|
229
|
+
done < "$input_file"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Process files in directory
|
|
233
|
+
```bash
|
|
234
|
+
for file in *.txt; do
|
|
235
|
+
[[ -f "$file" ]] || continue
|
|
236
|
+
process "$file"
|
|
237
|
+
done
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Use `--` before variable file arguments
|
|
241
|
+
```bash
|
|
242
|
+
# BAD - filename starting with '-' parsed as option
|
|
243
|
+
rm "$file"
|
|
244
|
+
cp "$src" "$dst"
|
|
245
|
+
|
|
246
|
+
# GOOD - '--' ends option parsing
|
|
247
|
+
rm -- "$file"
|
|
248
|
+
cp -- "$src" "$dst"
|
|
249
|
+
|
|
250
|
+
# GOOD - './' prefix prevents dash interpretation
|
|
251
|
+
mv "./$file" "$dest"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Safely rewrite a file in-place
|
|
255
|
+
```bash
|
|
256
|
+
# BAD - file truncated before cat reads it
|
|
257
|
+
cat file | sort > file # file is now empty
|
|
258
|
+
grep pattern file > file # file is now empty
|
|
259
|
+
|
|
260
|
+
# GOOD - write to temp file, then replace
|
|
261
|
+
sort file > tmp && mv tmp file
|
|
262
|
+
|
|
263
|
+
# GOOD - use sponge (moreutils) for in-place
|
|
264
|
+
sort file | sponge file
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Redirect stderr to /dev/null, never close it
|
|
268
|
+
```bash
|
|
269
|
+
# BAD - closing stderr can cause programs to crash or misbehave
|
|
270
|
+
cmd 2>&-
|
|
271
|
+
|
|
272
|
+
# GOOD - redirect to /dev/null instead
|
|
273
|
+
cmd 2>/dev/null
|
|
274
|
+
|
|
275
|
+
# GOOD - suppress both stdout and stderr
|
|
276
|
+
cmd >/dev/null 2>&1
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Preserve variables set inside a read loop
|
|
280
|
+
```bash
|
|
281
|
+
# BAD - pipe creates subshell; counter lost
|
|
282
|
+
count=0
|
|
283
|
+
cat file | while IFS= read -r line; do
|
|
284
|
+
((count++))
|
|
285
|
+
done
|
|
286
|
+
echo "$count" # always 0
|
|
287
|
+
|
|
288
|
+
# GOOD - process substitution keeps same shell
|
|
289
|
+
count=0
|
|
290
|
+
while IFS= read -r line; do
|
|
291
|
+
((count++))
|
|
292
|
+
done < <(cat file)
|
|
293
|
+
echo "$count" # correct count
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Parallel Processing
|
|
297
|
+
|
|
298
|
+
### Simple parallel with xargs
|
|
299
|
+
```bash
|
|
300
|
+
find . -name "*.log" -print0 | \
|
|
301
|
+
xargs -0 -P 4 -I {} process_log "{}"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Background jobs with wait
|
|
305
|
+
```bash
|
|
306
|
+
pids=()
|
|
307
|
+
for item in "${items[@]}"; do
|
|
308
|
+
process "$item" &
|
|
309
|
+
pids+=($!)
|
|
310
|
+
done
|
|
311
|
+
|
|
312
|
+
# Wait for all background jobs
|
|
313
|
+
for pid in "${pids[@]}"; do
|
|
314
|
+
wait "$pid"
|
|
315
|
+
done
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Progress Indication
|
|
319
|
+
|
|
320
|
+
### Simple counter
|
|
321
|
+
```bash
|
|
322
|
+
total=$(wc -l < "$file")
|
|
323
|
+
current=0
|
|
324
|
+
|
|
325
|
+
while IFS= read -r line; do
|
|
326
|
+
((current++))
|
|
327
|
+
printf "\rProcessing: %d/%d" "$current" "$total"
|
|
328
|
+
process "$line"
|
|
329
|
+
done
|
|
330
|
+
printf "\n"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Exit Code Handling
|
|
334
|
+
|
|
335
|
+
### Check command success
|
|
336
|
+
```bash
|
|
337
|
+
if ! command_that_might_fail; then
|
|
338
|
+
echo "Command failed" >&2
|
|
339
|
+
exit 1
|
|
340
|
+
fi
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Capture and check exit code
|
|
344
|
+
```bash
|
|
345
|
+
if output=$(some_command); then
|
|
346
|
+
echo "Success: $output"
|
|
347
|
+
else
|
|
348
|
+
exit_code=$?
|
|
349
|
+
echo "Failed with code: $exit_code" >&2
|
|
350
|
+
exit "$exit_code"
|
|
351
|
+
fi
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Input Validation
|
|
355
|
+
|
|
356
|
+
### Validate numeric input
|
|
357
|
+
```bash
|
|
358
|
+
is_number() {
|
|
359
|
+
local value="$1"
|
|
360
|
+
[[ "$value" =~ ^[0-9]+$ ]]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# Usage
|
|
364
|
+
if is_number "$port"; then
|
|
365
|
+
echo "Valid port number"
|
|
366
|
+
fi
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Validate within range
|
|
370
|
+
```bash
|
|
371
|
+
validate_port() {
|
|
372
|
+
local port="$1"
|
|
373
|
+
(( port >= 1 && port <= 65535 ))
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Validate required arguments
|
|
378
|
+
```bash
|
|
379
|
+
validate_inputs() {
|
|
380
|
+
local name="$1"
|
|
381
|
+
local port="$2"
|
|
382
|
+
|
|
383
|
+
[[ -n "$name" ]] || { echo "Error: name is required" >&2; return 1; }
|
|
384
|
+
validate_port "$port" || { echo "Error: port out of range" >&2; return 1; }
|
|
385
|
+
}
|
|
386
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Writing Secure Shell Scripts
|
|
2
|
+
|
|
3
|
+
Preventive patterns to apply while writing shell scripts. For auditing existing scripts for security vulnerabilities (destructive commands, credential exposure, system file risks), use `shell-security` instead.
|
|
4
|
+
|
|
5
|
+
## Command Injection Prevention
|
|
6
|
+
|
|
7
|
+
### NEVER use eval
|
|
8
|
+
```bash
|
|
9
|
+
# BAD - User can execute arbitrary commands
|
|
10
|
+
eval "echo $user_input"
|
|
11
|
+
|
|
12
|
+
# GOOD - Use arrays or direct execution
|
|
13
|
+
echo "$user_input"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### NEVER pipe to sh/bash
|
|
17
|
+
```bash
|
|
18
|
+
# BAD - Downloads and executes untrusted code
|
|
19
|
+
curl http://example.com/script.sh | sh
|
|
20
|
+
|
|
21
|
+
# GOOD - Download, review, then execute
|
|
22
|
+
curl -O http://example.com/script.sh
|
|
23
|
+
less script.sh
|
|
24
|
+
sh script.sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### ALWAYS quote variables in command positions
|
|
28
|
+
```bash
|
|
29
|
+
# BAD - Word splitting and globbing
|
|
30
|
+
func $user_var
|
|
31
|
+
|
|
32
|
+
# GOOD - Properly quoted
|
|
33
|
+
func "$user_var"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Input Validation
|
|
37
|
+
|
|
38
|
+
### Validate filenames against path traversal
|
|
39
|
+
```bash
|
|
40
|
+
validate_filename() {
|
|
41
|
+
local filename="$1"
|
|
42
|
+
|
|
43
|
+
# Reject paths with directory traversal
|
|
44
|
+
if [[ "$filename" == *".."* ]]; then
|
|
45
|
+
echo "Error: invalid filename" >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Reject absolute paths if not expected
|
|
50
|
+
if [[ "$filename" == /* ]]; then
|
|
51
|
+
echo "Error: absolute paths not allowed" >&2
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Only allow safe characters
|
|
56
|
+
if [[ ! "$filename" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
|
57
|
+
echo "Error: filename contains invalid characters" >&2
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For type/range validation helpers (e.g. `is_number`, `validate_port`), see `references/patterns.md`.
|
|
64
|
+
|
|
65
|
+
### Sanitise environment variables
|
|
66
|
+
```bash
|
|
67
|
+
# Clear PATH before using untrusted input
|
|
68
|
+
if [[ "$untrusted_mode" == "true" ]]; then
|
|
69
|
+
export PATH="/usr/bin:/bin"
|
|
70
|
+
fi
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Secrets Handling
|
|
74
|
+
|
|
75
|
+
### NEVER hardcode credentials
|
|
76
|
+
```bash
|
|
77
|
+
# BAD
|
|
78
|
+
api_key="sk-live-1234567890abcdef"
|
|
79
|
+
|
|
80
|
+
# GOOD - Read from environment
|
|
81
|
+
api_key="${API_KEY:-}"
|
|
82
|
+
[[ -n "$api_key" ]] || { echo "Error: API_KEY not set" >&2; exit 1; }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Prevent secrets in process list
|
|
86
|
+
```bash
|
|
87
|
+
# BAD - Secret visible in ps
|
|
88
|
+
curl -H "Authorization: Bearer $secret_token" https://api.example.com
|
|
89
|
+
|
|
90
|
+
# GOOD - Use file for secrets
|
|
91
|
+
echo "Authorization: Bearer $secret_token" > "$tmp_headers"
|
|
92
|
+
curl -H @"$tmp_headers" https://api.example.com
|
|
93
|
+
rm -f "$tmp_headers"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Prevent secrets in logs
|
|
97
|
+
```bash
|
|
98
|
+
# Redirect debug output that may contain secrets
|
|
99
|
+
process_with_secrets 2>/dev/null
|
|
100
|
+
|
|
101
|
+
# Or filter sensitive patterns
|
|
102
|
+
process_with_secrets 2>&1 | grep -v "password\|token\|secret"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Temporary Files
|
|
106
|
+
|
|
107
|
+
### ALWAYS use mktemp
|
|
108
|
+
```bash
|
|
109
|
+
# BAD - Predictable filename, race condition
|
|
110
|
+
tmp_file="/tmp/my_script_$$"
|
|
111
|
+
|
|
112
|
+
# GOOD - Atomic, unpredictable
|
|
113
|
+
tmp_file=$(mktemp)
|
|
114
|
+
trap 'rm -f "$tmp_file"' EXIT
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Set restrictive permissions on temp files
|
|
118
|
+
```bash
|
|
119
|
+
tmp_file=$(mktemp)
|
|
120
|
+
chmod 600 "$tmp_file"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## File Permissions
|
|
124
|
+
|
|
125
|
+
### Use restrictive defaults
|
|
126
|
+
```bash
|
|
127
|
+
# Create files with owner-only permissions
|
|
128
|
+
umask 077
|
|
129
|
+
|
|
130
|
+
# Or set explicitly
|
|
131
|
+
output_file=$(mktemp)
|
|
132
|
+
chmod 600 "$output_file"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Avoid world-writable locations
|
|
136
|
+
```bash
|
|
137
|
+
# BAD - /tmp is world-writable
|
|
138
|
+
output="/tmp/data.txt"
|
|
139
|
+
|
|
140
|
+
# GOOD - Use user's cache
|
|
141
|
+
output="$HOME/.cache/script/data.txt"
|
|
142
|
+
mkdir -p "$(dirname "$output")"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Safe Command Execution
|
|
146
|
+
|
|
147
|
+
### Use arrays for arguments
|
|
148
|
+
```bash
|
|
149
|
+
# BAD - Word splitting
|
|
150
|
+
files="*.txt"
|
|
151
|
+
cat $files
|
|
152
|
+
|
|
153
|
+
# GOOD - Proper expansion
|
|
154
|
+
files=("*.txt")
|
|
155
|
+
cat "${files[@]}"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Use set -f to disable globbing
|
|
159
|
+
```bash
|
|
160
|
+
# Disable glob expansion when processing untrusted input
|
|
161
|
+
set -f
|
|
162
|
+
process_untrusted "$user_input"
|
|
163
|
+
set +f
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Signal Handling
|
|
167
|
+
|
|
168
|
+
### Clean up on interruption
|
|
169
|
+
```bash
|
|
170
|
+
cleanup() {
|
|
171
|
+
rm -f "$tmp_file"
|
|
172
|
+
rm -rf "$tmp_dir"
|
|
173
|
+
echo "Cleaned up temporary files" >&2
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
trap cleanup EXIT INT TERM HUP
|
|
177
|
+
|
|
178
|
+
tmp_file=$(mktemp)
|
|
179
|
+
tmp_dir=$(mktemp -d)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Prevent partial state on exit
|
|
183
|
+
```bash
|
|
184
|
+
cleanup() {
|
|
185
|
+
# Remove incomplete output
|
|
186
|
+
[[ -f "${output}.partial" ]] && rm -f "${output}.partial"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
trap cleanup EXIT
|
|
190
|
+
|
|
191
|
+
# Write to partial file, rename on success
|
|
192
|
+
output="result.txt"
|
|
193
|
+
process_data > "${output}.partial"
|
|
194
|
+
mv "${output}.partial" "$output"
|
|
195
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shell-debugging
|
|
3
|
+
description: Debug a failing bash script: reproduce the failure, isolate the cause with non-invasive tracing (bash -x, bash -n, ShellCheck), then apply and verify the fix. Use when a script errors at runtime, crashes, exits non-zero, or produces wrong output ("debug this script", "fix my script", "why is this failing"). For a script that works but runs slowly, use shell-profiling; for quality review of a working script, use shell-review.
|
|
4
|
+
allowed-tools: Read, Edit, Bash, Grep, Glob
|
|
5
|
+
argument-hint: [script-path]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shell Debugging Skill
|
|
9
|
+
|
|
10
|
+
Guides systematic debugging of bash scripts to identify and resolve issues efficiently.
|
|
11
|
+
|
|
12
|
+
**Scope**: This skill handles runtime failures — scripts that error, crash, or produce wrong output. For quality assessment of a working script, use `shell-review` instead.
|
|
13
|
+
|
|
14
|
+
**Design principle**: Never leave debug instrumentation in the target script. Use non-invasive methods (`bash -x`) first; when targeted tracing is needed, instrument a temporary copy and discard it afterwards.
|
|
15
|
+
|
|
16
|
+
## Debugging Workflow
|
|
17
|
+
|
|
18
|
+
### 1. Gather Information
|
|
19
|
+
|
|
20
|
+
**Target script:** `$ARGUMENTS`
|
|
21
|
+
|
|
22
|
+
If `$ARGUMENTS` is not provided, ask the user which script needs debugging.
|
|
23
|
+
|
|
24
|
+
Understand the failure:
|
|
25
|
+
|
|
26
|
+
- **Error message**: What is the exact error?
|
|
27
|
+
- **Exit code**: What is `$?` after the failing command?
|
|
28
|
+
- **Failure point**: Where exactly does the script fail?
|
|
29
|
+
- **Reproduction**: Can the failure be reproduced consistently?
|
|
30
|
+
|
|
31
|
+
### 2. Read the Target Script
|
|
32
|
+
|
|
33
|
+
Read `$ARGUMENTS` to understand:
|
|
34
|
+
|
|
35
|
+
- Overall structure and purpose
|
|
36
|
+
- Functions called near the failure point
|
|
37
|
+
- Variable dependencies
|
|
38
|
+
- Error handling patterns
|
|
39
|
+
|
|
40
|
+
### 3. Enable Debug Mode
|
|
41
|
+
|
|
42
|
+
**Non-invasive** — run with tracing from the command line, no file modification:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bash -x script.sh args
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `+` prefix indicates the command being executed
|
|
49
|
+
- Variables are expanded before printing
|
|
50
|
+
|
|
51
|
+
**Targeted tracing** — when you need tracing around a specific section only, create a temporary copy:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cp script.sh /tmp/script.debug.sh
|
|
55
|
+
# Add set -x / set +x around the suspect section in the copy
|
|
56
|
+
bash /tmp/script.debug.sh args
|
|
57
|
+
rm /tmp/script.debug.sh
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Syntax Validation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bash -n script.sh
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 5. Incremental Execution
|
|
67
|
+
|
|
68
|
+
**Warning**: `source` executes all top-level code in the script. Before sourcing a broken script, inspect it for side effects (file operations, network calls, deployments). Consider using a container or VM for untrusted scripts.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Source the script to load functions
|
|
72
|
+
source script.sh
|
|
73
|
+
|
|
74
|
+
# Call functions individually with test data
|
|
75
|
+
your_function "test_input"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 6. Identify the Issue
|
|
79
|
+
|
|
80
|
+
Consult `references/debugging-guide.md` for:
|
|
81
|
+
|
|
82
|
+
- Error pattern tables and fixes (unbound variables, pipelines, subshells, whitespace)
|
|
83
|
+
- Advanced techniques: custom PS4, timing, call logging
|
|
84
|
+
- ShellCheck quick-reference table for common warning codes
|
|
85
|
+
|
|
86
|
+
### 7. Apply the Fix
|
|
87
|
+
|
|
88
|
+
Make the minimal change that resolves the root cause, not the symptom.
|
|
89
|
+
|
|
90
|
+
### 8. Verify
|
|
91
|
+
|
|
92
|
+
- Re-run the originally failing invocation — it now succeeds
|
|
93
|
+
- `bash -n` is clean; no new errors introduced
|
|
94
|
+
- No debug instrumentation remains in the script
|
|
95
|
+
|
|
96
|
+
**Done when** the reported failure no longer reproduces under the original invocation, `bash -n` and ShellCheck are clean, and the script holds no leftover `set -x` or debug prints.
|
|
97
|
+
|
|
98
|
+
## Additional Resources
|
|
99
|
+
|
|
100
|
+
### References
|
|
101
|
+
|
|
102
|
+
- `references/debugging-guide.md` -- Error pattern tables, debugging checklist, common issue solutions, advanced techniques (custom PS4, timing, call logging), ShellCheck quick reference
|
|
103
|
+
|
|
104
|
+
Always read all references and examples before producing output.
|
|
105
|
+
|
|
106
|
+
### Examples
|
|
107
|
+
|
|
108
|
+
- `examples/debug-session.md` -- Walks through diagnosing an unbound variable error from start to finish
|
|
109
|
+
|
|
110
|
+
## Integration
|
|
111
|
+
|
|
112
|
+
- **`shell-best-practices`** -- Prevent issues before they occur
|
|
113
|
+
- **`shell-review`** -- Quality assessment once the script is working
|
|
114
|
+
- **`shell-profiling`** -- For scripts that work correctly but run too slowly. See profiling workflow for timing, tracing, and benchmarking.
|
|
115
|
+
- **`/shell-audit`** command -- Comprehensive quality checks
|