@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,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Example: Batch file processing
|
|
4
|
+
# Description: Find all .txt files, count lines in each, return summary statistics
|
|
5
|
+
# Usage: ./file-batch.sh [directory]
|
|
6
|
+
#
|
|
7
|
+
# shellcheck disable=SC1091 # dynamic source paths resolved at runtime
|
|
8
|
+
# shellcheck disable=SC2034
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_NAME="${0##*/}"
|
|
13
|
+
|
|
14
|
+
# Source batch utilities
|
|
15
|
+
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
|
|
16
|
+
source "${CLAUDE_PLUGIN_ROOT}/scripts/lib-batch.sh"
|
|
17
|
+
elif [[ -f "$(dirname "$0")/../../scripts/lib-batch.sh" ]]; then
|
|
18
|
+
source "$(dirname "$0")/../../scripts/lib-batch.sh"
|
|
19
|
+
else
|
|
20
|
+
echo "Error: Cannot find lib-batch.sh" >&2
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Configuration
|
|
25
|
+
SEARCH_DIR="${1:-.}"
|
|
26
|
+
MAX_SIZE="${MAX_SIZE:-10485760}" # 10MB default max file size
|
|
27
|
+
|
|
28
|
+
# Process a single text file
|
|
29
|
+
function process_file() {
|
|
30
|
+
local file="$1"
|
|
31
|
+
local lines
|
|
32
|
+
local size
|
|
33
|
+
local filename
|
|
34
|
+
|
|
35
|
+
filename="${file##*/}"
|
|
36
|
+
lines=$(wc -l <"$file" 2>/dev/null)
|
|
37
|
+
lines="${lines// /}"
|
|
38
|
+
size=$(wc -c <"$file" 2>/dev/null)
|
|
39
|
+
size="${size// /}"
|
|
40
|
+
|
|
41
|
+
printf '%s|%s|%s\n' "$filename" "$lines" "$size"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Main processing
|
|
45
|
+
function main() {
|
|
46
|
+
declare -A RESULTS
|
|
47
|
+
declare -a METADATA
|
|
48
|
+
declare -a ERRORS
|
|
49
|
+
|
|
50
|
+
# Metadata
|
|
51
|
+
batch_add_metadata METADATA "script" "$SCRIPT_NAME"
|
|
52
|
+
batch_add_metadata METADATA "search_dir" "$SEARCH_DIR"
|
|
53
|
+
batch_add_metadata METADATA "started" "$(date -Iseconds)"
|
|
54
|
+
|
|
55
|
+
batch_progress "Searching for .txt files in: ${SEARCH_DIR}"
|
|
56
|
+
|
|
57
|
+
# Variables for statistics
|
|
58
|
+
local file_count=0
|
|
59
|
+
local total_lines=0
|
|
60
|
+
local total_size=0
|
|
61
|
+
local largest_file=""
|
|
62
|
+
local largest_lines=0
|
|
63
|
+
local smallest_file=""
|
|
64
|
+
local smallest_lines=""
|
|
65
|
+
local first_file=true
|
|
66
|
+
|
|
67
|
+
# Process files
|
|
68
|
+
local temp_file
|
|
69
|
+
temp_file=$(mktemp)
|
|
70
|
+
trap 'rm -f "$temp_file"' EXIT
|
|
71
|
+
|
|
72
|
+
while IFS= read -r -d '' file; do
|
|
73
|
+
batch_progress "Found: ${file}"
|
|
74
|
+
|
|
75
|
+
# Check file size
|
|
76
|
+
local file_size
|
|
77
|
+
file_size=$(wc -c <"$file" 2>/dev/null)
|
|
78
|
+
file_size="${file_size// /}"
|
|
79
|
+
|
|
80
|
+
if ((file_size > MAX_SIZE)); then
|
|
81
|
+
batch_add_error ERRORS "File too large, skipping: ${file} (${file_size} bytes)"
|
|
82
|
+
continue
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Process file
|
|
86
|
+
if output=$(process_file "$file"); then
|
|
87
|
+
echo "$output" >>"$temp_file"
|
|
88
|
+
((file_count++))
|
|
89
|
+
else
|
|
90
|
+
batch_add_error ERRORS "Failed to process: ${file}"
|
|
91
|
+
fi
|
|
92
|
+
done < <(find "$SEARCH_DIR" -name "*.txt" -print0 2>/dev/null)
|
|
93
|
+
|
|
94
|
+
batch_progress "Processing statistics from ${file_count} files"
|
|
95
|
+
|
|
96
|
+
# Calculate statistics
|
|
97
|
+
while IFS='|' read -r filename lines size; do
|
|
98
|
+
total_lines=$((total_lines + lines))
|
|
99
|
+
total_size=$((total_size + size))
|
|
100
|
+
|
|
101
|
+
# Track largest/smallest
|
|
102
|
+
if ((lines > largest_lines)); then
|
|
103
|
+
largest_lines=$lines
|
|
104
|
+
largest_file="$filename"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if $first_file || ((lines < smallest_lines)); then
|
|
108
|
+
first_file=false
|
|
109
|
+
smallest_lines=$lines
|
|
110
|
+
smallest_file="$filename"
|
|
111
|
+
fi
|
|
112
|
+
done <"$temp_file"
|
|
113
|
+
|
|
114
|
+
# Calculate averages
|
|
115
|
+
local avg_lines=0
|
|
116
|
+
local avg_size=0
|
|
117
|
+
if ((file_count > 0)); then
|
|
118
|
+
avg_lines=$((total_lines / file_count))
|
|
119
|
+
avg_size=$((total_size / file_count))
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Store results
|
|
123
|
+
batch_add_result RESULTS "file_count" "$file_count"
|
|
124
|
+
batch_add_result RESULTS "total_lines" "$total_lines"
|
|
125
|
+
batch_add_result RESULTS "total_size" "$total_size"
|
|
126
|
+
batch_add_result RESULTS "avg_lines" "$avg_lines"
|
|
127
|
+
batch_add_result RESULTS "avg_size" "$avg_size"
|
|
128
|
+
batch_add_result RESULTS "largest_file" "$largest_file"
|
|
129
|
+
batch_add_result RESULTS "largest_lines" "$largest_lines"
|
|
130
|
+
batch_add_result RESULTS "smallest_file" "$smallest_file"
|
|
131
|
+
batch_add_result RESULTS "smallest_lines" "$smallest_lines"
|
|
132
|
+
|
|
133
|
+
# Complete metadata
|
|
134
|
+
batch_add_metadata METADATA "completed" "$(date -Iseconds)"
|
|
135
|
+
|
|
136
|
+
# Output JSON
|
|
137
|
+
batch_output RESULTS METADATA ERRORS
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
main "$@"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Batch vs Individual Operations Decision Tree
|
|
2
|
+
|
|
3
|
+
Visual guide for deciding when to write batch scripts vs using individual tool calls.
|
|
4
|
+
|
|
5
|
+
## Decision Flowchart
|
|
6
|
+
|
|
7
|
+
1. **1-2 operations** → Individual calls
|
|
8
|
+
2. **10+ operations** → Batch script
|
|
9
|
+
3. **3-10 operations** → Evaluate:
|
|
10
|
+
- Same operation on different inputs → Batch script
|
|
11
|
+
- Different operations + interactive → Individual calls
|
|
12
|
+
- Different operations + non-interactive → Batch script
|
|
13
|
+
|
|
14
|
+
## Decision Matrix
|
|
15
|
+
|
|
16
|
+
| Scenario | Operations | Data Type | Interactive | Recommendation |
|
|
17
|
+
| --------------------------------- | ---------- | --------- | ----------- | ---------------- |
|
|
18
|
+
| Count files in directory | 1 | N/A | No | Individual call |
|
|
19
|
+
| Find and count lines in each .txt | 2-N | Files | No | **Batch script** |
|
|
20
|
+
| Debug failing deployment | 3+ | Commands | Yes | Individual calls |
|
|
21
|
+
| Rename 100 files by pattern | N | Files | No | **Batch script** |
|
|
22
|
+
| Check service status | 1 | N/A | Maybe | Individual call |
|
|
23
|
+
| Extract, transform, load data | 3+ | Pipeline | No | **Batch script** |
|
|
24
|
+
| Generate 10 reports | N | Files | No | **Batch script** |
|
|
25
|
+
|
|
26
|
+
## Key Decision Factors
|
|
27
|
+
|
|
28
|
+
### 1. Number of Operations
|
|
29
|
+
|
|
30
|
+
- **1-2 operations**: Individual calls (overhead of script not worth it)
|
|
31
|
+
- **3-10 operations**: Depends on other factors
|
|
32
|
+
- **10+ operations**: Almost always batch
|
|
33
|
+
|
|
34
|
+
### 2. Operation Similarity
|
|
35
|
+
|
|
36
|
+
- **Same operation, different inputs**: Batch (file processing, bulk operations)
|
|
37
|
+
- **Different operations**: Depends on complexity and interactivity
|
|
38
|
+
|
|
39
|
+
### 3. Interactivity Needs
|
|
40
|
+
|
|
41
|
+
- **Need to see results between steps**: Individual calls
|
|
42
|
+
- **Can proceed without human input**: Batch
|
|
43
|
+
- **Debugging/diagnosing**: Individual calls
|
|
44
|
+
|
|
45
|
+
### 4. Token Constraints
|
|
46
|
+
|
|
47
|
+
- **Constrained context**: Batch (saves up to 98.7% tokens)
|
|
48
|
+
- **Plenty of context**: Either approach works
|
|
49
|
+
|
|
50
|
+
### 5. Error Handling
|
|
51
|
+
|
|
52
|
+
- **Operations may fail unpredictably**: Individual calls for diagnosis
|
|
53
|
+
- **Predictable operations with known error modes**: Batch with error collection
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shell-best-practices
|
|
3
|
+
description: Write secure, portable bash scripts with proper structure, error handling, and quoting. Use when scaffolding a new script ("write a bash script", "scaffold", "new shell file") or modifying, auditing, or hardening an existing one ("fix this script", "refactor bash", "add error handling to"). Covers bash functions, helpers, deployment scripts, and file operations.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash
|
|
5
|
+
argument-hint: [script-name-or-path]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shell Best Practices Skill
|
|
9
|
+
|
|
10
|
+
Guides Claude to write secure, portable, well-structured bash scripts — and to scaffold new scripts from the right template when starting from scratch.
|
|
11
|
+
|
|
12
|
+
## When This Skill Applies
|
|
13
|
+
|
|
14
|
+
- Writing or modifying any shell script
|
|
15
|
+
- Creating a new bash script ("create a script", "new bash file", "scaffold", "script template")
|
|
16
|
+
- Improving or auditing existing shell code
|
|
17
|
+
- Writing bash functions, helpers, or libraries
|
|
18
|
+
|
|
19
|
+
## Two Modes
|
|
20
|
+
|
|
21
|
+
**Mode A — Modify/improve**: Apply the core standards below to the existing script at `$ARGUMENTS`.
|
|
22
|
+
|
|
23
|
+
**Mode B — New script**: Select the appropriate template (see Templates section), create the file at `$ARGUMENTS` (see naming rules in Scaffolding Process), fill in the placeholders, then apply core standards.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core Standards
|
|
28
|
+
|
|
29
|
+
Every script must include:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
#!/usr/bin/env bash
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **Shebang**: `#!/usr/bin/env bash` — not `/bin/bash`
|
|
37
|
+
- **Strict mode**: `set -euo pipefail` — catches unbound variables, failed commands, and pipe errors
|
|
38
|
+
- **Strict mode caveat**: `set -e` does not exit on failures inside `if`/`while` conditions, `&&`/`||` chains, subshells, or negated commands. Use explicit exit-code checks or `trap ERR` for reliable error handling:
|
|
39
|
+
```bash
|
|
40
|
+
trap 'echo "Error at line $LINENO" >&2; exit 1' ERR
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Quoting
|
|
44
|
+
|
|
45
|
+
- Always quote variable expansions: `"$var"`, `"${array[@]}"`
|
|
46
|
+
- Quote command substitutions: `"$(cmd)"`
|
|
47
|
+
- Never leave variables unquoted in command positions
|
|
48
|
+
|
|
49
|
+
### Naming Conventions
|
|
50
|
+
|
|
51
|
+
- Local variables: `lower_case_with_underscores`
|
|
52
|
+
- Constants and globals/exports: `UPPERCASE_WITH_UNDERSCORES`
|
|
53
|
+
- Public functions: `<namespace>::function_name` — use `shroutines::` for plugin-internal scripts, or the project name (e.g. `myapp::`) for project-specific scripts
|
|
54
|
+
- Private functions: `_function_name` — leading underscore signals internal use; not part of any public API
|
|
55
|
+
- Use `local -r` for constants inside functions — scopes the variable and protects it. Use `readonly` for script-level constants (maximum portability). Use `declare -r` at the top level only when you need type flags (`-i`, `-a`, `-lx`). The meaningful distinction is `local -r` (scoped) vs `readonly`/`declare -r` (global):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Script-level — readonly for portability
|
|
59
|
+
SCRIPT_NAME=$(basename "$0")
|
|
60
|
+
readonly SCRIPT_NAME
|
|
61
|
+
readonly VERSION="0.1.0"
|
|
62
|
+
|
|
63
|
+
# Script-level with type flag — declare -r
|
|
64
|
+
declare -ar EXIT_CODES=(["ok"]=0 ["error"]=1 ["usage"]=2)
|
|
65
|
+
|
|
66
|
+
# Function-scoped — local -r
|
|
67
|
+
_parse_args() {
|
|
68
|
+
local -r max_retries=3
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Public function — shroutines:: namespace
|
|
72
|
+
shroutines::process_file() {
|
|
73
|
+
local input="$1"
|
|
74
|
+
# ...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Functions
|
|
79
|
+
|
|
80
|
+
> Replace the `shroutines::` prefix with the target project's namespace when scaffolding scripts for external projects.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
shroutines::process_file() {
|
|
84
|
+
local input_path="$1"
|
|
85
|
+
|
|
86
|
+
if [[ ! -r "$input_path" ]]; then
|
|
87
|
+
echo "Error: cannot read file: $input_path" >&2
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# logic here
|
|
92
|
+
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- Use `local` for all function-scoped variables
|
|
98
|
+
- **Separate declaration and assignment** when the value comes from a command substitution — `local` does not propagate the exit code:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# BAD - $? is always 0 (exit code of 'local', not my_func)
|
|
102
|
+
local my_var="$(my_func)"
|
|
103
|
+
(( $? == 0 )) || return
|
|
104
|
+
|
|
105
|
+
# GOOD - separate lines preserve the exit code
|
|
106
|
+
local my_var
|
|
107
|
+
my_var="$(my_func)"
|
|
108
|
+
(( $? == 0 )) || return
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- End functions with explicit `return 0` — makes success exit point visible and distinguishes "fell off the end" from "deliberately succeeded"
|
|
112
|
+
- Use `[[ ]]` for bash tests, `[ ]` for POSIX sh
|
|
113
|
+
- Errors go to stderr: `>&2`
|
|
114
|
+
- Return meaningful exit codes: 0 = success, 1 = error, 2 = misuse
|
|
115
|
+
|
|
116
|
+
### File Structure
|
|
117
|
+
|
|
118
|
+
Every script must follow this top-to-bottom order for any sections that are present:
|
|
119
|
+
|
|
120
|
+
1. **Shebang and strict mode** — `#!/usr/bin/env bash` + `set -euo pipefail`
|
|
121
|
+
2. **Constants** — `readonly` / `declare -r` values that never change
|
|
122
|
+
3. **Globals** — `UPPERCASE` variables with script-wide scope
|
|
123
|
+
4. **Private functions** — `_function_name` helpers, internal utilities
|
|
124
|
+
5. **Public functions** — `shroutines::function_name` entry points and API
|
|
125
|
+
6. **Guard and execution** — `BASH_SOURCE[0]` guard + `main "$@"`
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
#!/usr/bin/env bash
|
|
129
|
+
set -euo pipefail
|
|
130
|
+
|
|
131
|
+
readonly VERSION="0.1.0"
|
|
132
|
+
|
|
133
|
+
VERBOSE=0
|
|
134
|
+
OUTPUT_FILE=""
|
|
135
|
+
|
|
136
|
+
_log_info() { printf '[INFO] %s\n' "$*" >&2; return 0; }
|
|
137
|
+
|
|
138
|
+
shroutines::process() {
|
|
139
|
+
local input="$1"
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
144
|
+
main "$@"
|
|
145
|
+
fi
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Line Length
|
|
149
|
+
|
|
150
|
+
- Soft limit: **120 characters** per line
|
|
151
|
+
- Break long strings, pipelines, or argument lists across lines
|
|
152
|
+
- Prefer `printf` over `echo` for multi-line output
|
|
153
|
+
|
|
154
|
+
- **Do not use `&& ... || ...` as if/else** — if the middle command fails, the `||` branch runs even though the `&&` condition succeeded:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# BAD - rollback runs if deploy succeeds but healthcheck fails
|
|
158
|
+
deploy && healthcheck || rollback
|
|
159
|
+
|
|
160
|
+
# GOOD - explicit if/else
|
|
161
|
+
if deploy && healthcheck; then
|
|
162
|
+
echo "Deploy succeeded"
|
|
163
|
+
else
|
|
164
|
+
rollback
|
|
165
|
+
fi
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Comment Conventions
|
|
169
|
+
|
|
170
|
+
Only comment when the code itself cannot convey the information: a hidden constraint, a subtle invariant, a bug workaround, or behaviour that would surprise a careful reader. Write one explaining _why_ not _what_. If removing the comment wouldn't confuse a future reader, don't write it.
|
|
171
|
+
|
|
172
|
+
**File header** — every script starts with one:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
#!/usr/bin/env bash
|
|
176
|
+
#
|
|
177
|
+
# [BRIEF DESCRIPTION OF WHAT THIS SCRIPT DOES]
|
|
178
|
+
# Usage: [SCRIPT_NAME] [ARGUMENTS]
|
|
179
|
+
#
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Section dividers** — only when an individual section exceeds ~50 lines or complexity makes structure worth signposting. The standard file structure ordering is self-evident; dividers between short sections add noise. `#` tail fills to the nearest of 40, 80, or 120 columns:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
###
|
|
186
|
+
### :::: [description] :::: ###########
|
|
187
|
+
###
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Public function docs** — only when the function's name and arguments don't fully convey its contract: non-obvious return codes, argument constraints, side effects, or failure conditions. Public (`shroutines::`) functions only; never on private (`_`) helpers.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# [description]
|
|
194
|
+
# Arguments:
|
|
195
|
+
# $1 - [name]: [description]
|
|
196
|
+
# $2 - [name]: [description]
|
|
197
|
+
# Returns:
|
|
198
|
+
# 0 - [success description]
|
|
199
|
+
# 1 - [failure description]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Inline comments** — trailing `#` on the same line:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# GOOD — explains why
|
|
206
|
+
local -r threshold=$((mem_total / 10)) # 10% of total memory
|
|
207
|
+
|
|
208
|
+
# BAD — restates what
|
|
209
|
+
local -r threshold=$((mem_total / 10)) # calculate threshold
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Annotation comments** — bare `#` line above a block:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Track descriptors so the trap can close them even if the list changes later
|
|
216
|
+
exec 3>/var/log/daemon.log
|
|
217
|
+
exec 4>&1
|
|
218
|
+
_OPEN_FDS+=(3 4)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Security Prohibitions
|
|
222
|
+
|
|
223
|
+
- **Never use `eval`** — command injection risk
|
|
224
|
+
- **Never pipe to `sh` or `bash`** — injection risk
|
|
225
|
+
|
|
226
|
+
For input validation, secrets handling, temp-file hygiene, and file permissions, see `references/security.md`.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Templates (Mode B — New Scripts)
|
|
231
|
+
|
|
232
|
+
Choose based on complexity and purpose:
|
|
233
|
+
|
|
234
|
+
| Template | Use When |
|
|
235
|
+
| -------------------- | ---------------------------------------------------------------------------- |
|
|
236
|
+
| `assets/standard.sh` | Most scripts — argument parsing, error handling, direct execution |
|
|
237
|
+
| `assets/minimal.sh` | Simple one-task utilities, no complex flag parsing |
|
|
238
|
+
| `assets/library.sh` | Sourced by other scripts; provides reusable functions, no direct execution |
|
|
239
|
+
| `assets/posix.sh` | POSIX sh — containers, embedded, Alpine, CI base images, maximum portability |
|
|
240
|
+
|
|
241
|
+
### Template Selection Guide
|
|
242
|
+
|
|
243
|
+
- Does it need `--flag` style options or multiple arguments? → **standard**
|
|
244
|
+
- Is it a short utility doing one thing? → **minimal**
|
|
245
|
+
- Will other scripts `source` it? → **library**
|
|
246
|
+
- Must run in containers, embedded, Alpine, or under dash? → **posix**
|
|
247
|
+
|
|
248
|
+
### Scaffolding Process
|
|
249
|
+
|
|
250
|
+
1. Determine script name from `$ARGUMENTS` or ask the user
|
|
251
|
+
2. Select template type based on purpose
|
|
252
|
+
3. Create file at `$ARGUMENTS` — for directly executed scripts, omit the `.sh` extension (e.g. `deploy` not `deploy.sh`); for libraries meant to be sourced, keep the `.sh` extension (e.g. `lib-common.sh`)
|
|
253
|
+
4. Copy template content and fill in placeholders:
|
|
254
|
+
- Script description
|
|
255
|
+
- Usage examples
|
|
256
|
+
- Function implementations
|
|
257
|
+
5. Apply all core standards above
|
|
258
|
+
6. If POSIX portability is required: use the POSIX template instead, apply `#!/bin/sh` shebang, and follow the POSIX sh Feature Restrictions below. Run `checkbashisms` to verify the final result
|
|
259
|
+
|
|
260
|
+
**Done when:** the automated ShellCheck and shfmt hooks report clean on the scaffolded file — and `checkbashisms` for `#!/bin/sh` scripts. Resolve every finding before considering the script complete.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## POSIX vs Bash
|
|
265
|
+
|
|
266
|
+
This plugin targets **Bash 4.4+** as its primary compatibility tier. POSIX sh is a secondary tier for maximum portability.
|
|
267
|
+
|
|
268
|
+
**Rule: If the shebang says `#!/bin/sh`, the script must contain zero bashisms.** Use `#!/usr/bin/env bash` if you need bash features. The hook pipeline detects the shebang and configures ShellCheck, shfmt, and checkbashisms accordingly.
|
|
269
|
+
|
|
270
|
+
### Shebang Discipline
|
|
271
|
+
|
|
272
|
+
| Shebang | Meaning | Tooling |
|
|
273
|
+
| --------------------- | -------------------------------- | -------------------------------------------------------- |
|
|
274
|
+
| `#!/usr/bin/env bash` | Bash-specific. Bashisms allowed. | shellcheck `-s bash`, shfmt `-ln bash`, `bash -n` |
|
|
275
|
+
| `#!/bin/sh` | POSIX sh only. No bash features. | shellcheck `-s sh`, shfmt `-ln posix`, `checkbashisms` |
|
|
276
|
+
| `#!/usr/bin/dash` | Explicit dash. Same as POSIX sh. | shellcheck `-s dash`, shfmt `-ln posix`, `checkbashisms` |
|
|
277
|
+
|
|
278
|
+
### When to Choose POSIX sh
|
|
279
|
+
|
|
280
|
+
Choose `#!/bin/sh` when:
|
|
281
|
+
|
|
282
|
+
- The script runs in containers with minimal base images (Alpine, distroless)
|
|
283
|
+
- It must execute on embedded systems or CI runners with no bash
|
|
284
|
+
- It is a lightweight utility (init script, hook, wrapper) that doesn't need arrays or complex string manipulation
|
|
285
|
+
|
|
286
|
+
Choose `#!/usr/bin/env bash` when:
|
|
287
|
+
|
|
288
|
+
- You need arrays, associative arrays, or pattern matching with `[[ ]]`
|
|
289
|
+
- The script does complex string manipulation, argument parsing, or data processing
|
|
290
|
+
- It sources a library (the library template uses namerefs and associative arrays)
|
|
291
|
+
|
|
292
|
+
For POSIX sh scripts, ensure no bashisms are present. Common traps: `[[ ]]`, arrays, `${var,,}`, `<<<`, `source`, `function` keyword, `echo -e`. Use `checkbashisms` to verify.
|
|
293
|
+
|
|
294
|
+
When the user specifies POSIX portability, use `assets/posix.sh` instead of the bash templates.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Reference Files
|
|
299
|
+
|
|
300
|
+
- `references/patterns.md` — Argument parsing, temp files, arrays, string manipulation, parallel processing, progress output, exit code handling
|
|
301
|
+
- `references/security.md` — Preventive security patterns for writing: injection prevention, input validation, temp files, signal handling
|
|
302
|
+
- `${CLAUDE_PLUGIN_ROOT}/scripts/lib-common.sh` — General-purpose runtime library (logging, validation, temp files, string/array utilities), all functions `shroutines::`-namespaced. Source directly when the script can depend on the plugin being installed
|
|
303
|
+
|
|
304
|
+
Always consult these reference files before producing output — both for reviewing existing scripts and scaffolding new ones.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Integration
|
|
309
|
+
|
|
310
|
+
- **`shell-security`** — Destructive commands, credential exposure, system file risks
|
|
311
|
+
- **`shell-review`** — Structured quality review of a completed script
|
|
312
|
+
- **`shell-architect`** agent — Multi-file project design, performance decisions, library vs executable structure
|
|
313
|
+
- **Hook automation** — ShellCheck and shfmt run automatically after file creation/modification
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Description: [LIBRARY NAME] - Reusable bash functions
|
|
4
|
+
# Usage: source /path/to/[FILE].sh
|
|
5
|
+
#
|
|
6
|
+
# This file provides reusable functions for [PURPOSE]
|
|
7
|
+
#
|
|
8
|
+
# For general-purpose utilities (logging, validation, temp files), consider
|
|
9
|
+
# sourcing the plugin runtime library instead:
|
|
10
|
+
# source "${CLAUDE_PLUGIN_ROOT}/scripts/lib-common.sh"
|
|
11
|
+
#
|
|
12
|
+
# Functions:
|
|
13
|
+
# shroutines::function_name - Description of what the function does
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
# Guard against direct execution
|
|
17
|
+
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && {
|
|
18
|
+
echo "Error: This file should be sourced, not executed" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
###
|
|
25
|
+
### :::: Constants :::: ###############
|
|
26
|
+
###
|
|
27
|
+
|
|
28
|
+
# shellcheck disable=SC2034 # template placeholder, used after scaffolding
|
|
29
|
+
readonly _LIB_VERSION="0.1.0"
|
|
30
|
+
|
|
31
|
+
###
|
|
32
|
+
### :::: Globals :::: #################
|
|
33
|
+
###
|
|
34
|
+
|
|
35
|
+
# shellcheck disable=SC2034
|
|
36
|
+
_LIB_TEMP_FILES=()
|
|
37
|
+
|
|
38
|
+
###
|
|
39
|
+
### :::: Private functions :::: #######
|
|
40
|
+
###
|
|
41
|
+
|
|
42
|
+
# Cleanup tracking
|
|
43
|
+
function _lib_cleanup() {
|
|
44
|
+
local f
|
|
45
|
+
for f in "${_LIB_TEMP_FILES[@]+"${_LIB_TEMP_FILES[@]}"}"; do
|
|
46
|
+
rm -f "$f"
|
|
47
|
+
done
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
trap _lib_cleanup EXIT
|
|
52
|
+
|
|
53
|
+
###
|
|
54
|
+
### :::: Public functions :::: ########
|
|
55
|
+
###
|
|
56
|
+
|
|
57
|
+
# Validates user input against a pattern
|
|
58
|
+
# Arguments:
|
|
59
|
+
# $1 - input: The string to validate
|
|
60
|
+
# $2 - pattern: Regex pattern to match (optional, defaults to alphanumeric)
|
|
61
|
+
# Returns:
|
|
62
|
+
# 0 - valid input
|
|
63
|
+
# 1 - invalid input
|
|
64
|
+
|
|
65
|
+
function shroutines::validate_input() {
|
|
66
|
+
local input="$1"
|
|
67
|
+
local pattern="${2:-^[a-zA-Z0-9_-]+$}"
|
|
68
|
+
|
|
69
|
+
[[ "$input" =~ $pattern ]]
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Log a message with timestamp and level (no subprocess)
|
|
74
|
+
# Arguments:
|
|
75
|
+
# $1 - level: Log level (INFO, WARN, ERROR, DEBUG)
|
|
76
|
+
# $2 - message: The message to log
|
|
77
|
+
# Returns: None
|
|
78
|
+
function shroutines::log_message() {
|
|
79
|
+
printf '[%(%Y-%m-%d %H:%M:%S)T] [%s] %s\n' -1 "$1" "${*:2}" >&2
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Check if a command is available
|
|
84
|
+
# Arguments:
|
|
85
|
+
# $1 - command: Name of the command to check
|
|
86
|
+
# Returns:
|
|
87
|
+
# 0 - command exists
|
|
88
|
+
# 1 - command not found
|
|
89
|
+
function shroutines::require_command() {
|
|
90
|
+
local cmd="$1"
|
|
91
|
+
|
|
92
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
93
|
+
shroutines::log_message "ERROR" "Required command not found: $cmd"
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
return 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Ensure a directory exists, create if missing
|
|
101
|
+
# Arguments:
|
|
102
|
+
# $1 - path: Directory path to ensure
|
|
103
|
+
# $2 - mode: Optional permissions (default: 0755)
|
|
104
|
+
# Returns:
|
|
105
|
+
# 0 - directory exists or was created
|
|
106
|
+
# 1 - failed to create directory
|
|
107
|
+
function shroutines::ensure_dir() {
|
|
108
|
+
local path="$1"
|
|
109
|
+
local mode="${2:-0755}"
|
|
110
|
+
|
|
111
|
+
if [[ ! -d "$path" ]]; then
|
|
112
|
+
mkdir -p "$path" || return 1
|
|
113
|
+
chmod "$mode" "$path"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Create a temporary file with automatic cleanup
|
|
120
|
+
# Arguments:
|
|
121
|
+
# $1 - var_name: Name of variable to store temp file path
|
|
122
|
+
# Returns:
|
|
123
|
+
# 0 - temp file created successfully
|
|
124
|
+
# 1 - failed to create temp file
|
|
125
|
+
function shroutines::temp_file() {
|
|
126
|
+
local -n var_ref="$1"
|
|
127
|
+
local tmp
|
|
128
|
+
|
|
129
|
+
tmp=$(mktemp) || return 1
|
|
130
|
+
# shellcheck disable=SC2034 # nameref: assignment is the intended use
|
|
131
|
+
var_ref="$tmp"
|
|
132
|
+
|
|
133
|
+
_LIB_TEMP_FILES+=("$tmp")
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Export functions for use in subshells
|
|
138
|
+
export -f shroutines::validate_input
|
|
139
|
+
export -f shroutines::log_message
|
|
140
|
+
export -f shroutines::require_command
|
|
141
|
+
export -f shroutines::ensure_dir
|
|
142
|
+
export -f shroutines::temp_file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Description: [BRIEF DESCRIPTION]
|
|
4
|
+
# Usage: [SCRIPT_NAME] [ARGUMENTS]
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
###
|
|
10
|
+
### :::: Constants :::: ###############
|
|
11
|
+
###
|
|
12
|
+
|
|
13
|
+
readonly VERSION="0.1.0"
|
|
14
|
+
|
|
15
|
+
###
|
|
16
|
+
### :::: Globals :::: ###############
|
|
17
|
+
###
|
|
18
|
+
|
|
19
|
+
INPUT=""
|
|
20
|
+
|
|
21
|
+
###
|
|
22
|
+
### :::: Private functions :::: ########
|
|
23
|
+
###
|
|
24
|
+
|
|
25
|
+
# Main logic
|
|
26
|
+
function _main() {
|
|
27
|
+
local input="$1"
|
|
28
|
+
|
|
29
|
+
if [[ -z "$input" ]]; then
|
|
30
|
+
echo "Error: input required" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Process input
|
|
35
|
+
echo "Processing: $input"
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
###
|
|
40
|
+
### :::: Public functions :::: ########
|
|
41
|
+
###
|
|
42
|
+
|
|
43
|
+
function shroutines::main() {
|
|
44
|
+
_main "$@"
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
###
|
|
49
|
+
### :::: Guard and execution :::: #####
|
|
50
|
+
###
|
|
51
|
+
|
|
52
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
53
|
+
shroutines::main "$@"
|
|
54
|
+
fi
|