@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,184 @@
|
|
|
1
|
+
# Bashunit Assertions Reference
|
|
2
|
+
|
|
3
|
+
Complete list of bashunit assertions for test cases.
|
|
4
|
+
|
|
5
|
+
> **Verify against your installed version.** This is a static summary — the authoritative list, including exact signatures, is `bashunit doc assert` (filter with `bashunit doc <term>`, e.g. `bashunit doc exit`). Run it before relying on an assertion: versions differ (e.g. `assert_success` was removed; `assert_exec` was added; the exit-code family takes **no** command argument — see Exit Code Assertions below).
|
|
6
|
+
|
|
7
|
+
## Core Assertions
|
|
8
|
+
|
|
9
|
+
| Assertion | Use Case | Example |
|
|
10
|
+
|-----------|----------|---------|
|
|
11
|
+
| `assert_equals "expected" "$actual"` | String comparison (ignores ANSI colours) | `assert_equals "hello" "$output"` |
|
|
12
|
+
| `assert_same "expected" "$actual"` | Exact comparison (including special chars) | `assert_same "hello" "hello"` |
|
|
13
|
+
| `assert_not_same "a" "b"` | Exact inequality | `assert_not_same "hello" "world"` |
|
|
14
|
+
| `assert_matches "regex" "$string"` | Regex pattern matching | `assert_matches "^[0-9]+$" "$var"` |
|
|
15
|
+
| `assert_not_matches "regex" "$string"` | Negative regex match | `assert_not_matches "^[a-z]+$" "123"` |
|
|
16
|
+
| `assert_contains "needle" "$haystack"` | Substring check | `assert_contains "error" "$log"` |
|
|
17
|
+
| `assert_not_contains "needle" "$haystack"` | Negative substring check | `assert_not_contains "ok" "$log"` |
|
|
18
|
+
| `assert_contains_ignore_case "NEEDLE" "$haystack"` | Case-insensitive substring | `assert_contains_ignore_case "hello" "Hello World"` |
|
|
19
|
+
| `assert_empty "$var"` | Assert value is empty | `assert_empty ""` |
|
|
20
|
+
| `assert_not_empty "$var"` | Assert value is non-empty | `assert_not_empty "content"` |
|
|
21
|
+
| `assert_string_starts_with "prefix" "$string"` | Prefix check | `assert_string_starts_with "Hello" "Hello World"` |
|
|
22
|
+
| `assert_string_ends_with "suffix" "$string"` | Suffix check | `assert_string_ends_with "World" "Hello World"` |
|
|
23
|
+
| `assert_string_matches_format "%d items" "$string"` | Format matching with placeholders | `assert_string_matches_format "%d items at %f each" "42 items at 9.99 each"` |
|
|
24
|
+
| `assert_line_count N "$multiline"` | Assert number of lines | `assert_line_count 3 "$output"` |
|
|
25
|
+
|
|
26
|
+
## Exit Code Assertions
|
|
27
|
+
|
|
28
|
+
These read `$?` from the command run immediately before the assertion — they take **no** command argument. To run a command string and check its exit in one call, use `assert_exec`.
|
|
29
|
+
|
|
30
|
+
| Assertion | Use Case | Example |
|
|
31
|
+
|-----------|----------|---------|
|
|
32
|
+
| `assert_exit_code "N"` | Last command exited with code N | `cmd; assert_exit_code 2` |
|
|
33
|
+
| `assert_successful_code` | Last command exited 0 | `ok_cmd; assert_successful_code` |
|
|
34
|
+
| `assert_unsuccessful_code` | Last command exited non-zero | `bad_cmd; assert_unsuccessful_code` |
|
|
35
|
+
| `assert_general_error` | Last command exited 1 | `bad_input; assert_general_error` |
|
|
36
|
+
| `assert_command_not_found` | Last command exited 127 | `missing; assert_command_not_found` |
|
|
37
|
+
| `assert_exec "cmd" --exit N` | Run a command string, assert its exit | `assert_exec "curl -s http://x" --exit 0` |
|
|
38
|
+
|
|
39
|
+
## File Assertions
|
|
40
|
+
|
|
41
|
+
| Assertion | Use Case | Example |
|
|
42
|
+
|-----------|----------|---------|
|
|
43
|
+
| `assert_file_exists "$path"` | File exists | `assert_file_exists "/tmp/output.txt"` |
|
|
44
|
+
| `assert_file_contains "needle" "$path"` | File contains substring | `assert_file_contains "error" "/var/log/app.log"` |
|
|
45
|
+
| `assert_file_not_exists "$path"` | File does not exist | `assert_file_not_exists "/tmp/old"` |
|
|
46
|
+
| `assert_directory_exists "$path"` | Directory exists | `assert_directory_exists "/var/log"` |
|
|
47
|
+
| `assert_directory_not_exists "$path"` | Directory does not exist | `assert_directory_not_exists "/tmp/missing"` |
|
|
48
|
+
|
|
49
|
+
## Array Assertions
|
|
50
|
+
|
|
51
|
+
| Assertion | Use Case | Example |
|
|
52
|
+
|-----------|----------|---------|
|
|
53
|
+
| `assert_array_contains "needle" "${arr[@]}"` | Array contains value | `assert_array_contains "apple" "${fruits[@]}"` |
|
|
54
|
+
| `assert_array_not_contains "needle" "${arr[@]}"` | Array does not contain value | `assert_array_not_contains "pear" "${fruits[@]}"` |
|
|
55
|
+
|
|
56
|
+
## Usage Patterns
|
|
57
|
+
|
|
58
|
+
### Testing Function Output
|
|
59
|
+
```bash
|
|
60
|
+
function test_function_returns_expected() {
|
|
61
|
+
my_function "input" > "$TEMP_FILE" # main-shell call so coverage records the body
|
|
62
|
+
assert_equals "expected_value" "$(<"$TEMP_FILE")"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Testing Exit Codes
|
|
67
|
+
```bash
|
|
68
|
+
function test_function_succeeds() {
|
|
69
|
+
my_function "valid_input"
|
|
70
|
+
assert_successful_code
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function test_function_fails_on_invalid_input() {
|
|
74
|
+
my_function "invalid_input"
|
|
75
|
+
assert_general_error
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Testing File Operations
|
|
80
|
+
```bash
|
|
81
|
+
function test_creates_output_file() {
|
|
82
|
+
my_function "$TEMP_FILE"
|
|
83
|
+
assert_file_exists "$TEMP_FILE"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function test_output_contains_expected_content() {
|
|
87
|
+
my_function "$TEMP_FILE"
|
|
88
|
+
assert_file_contains "expected text" "$TEMP_FILE"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Testing String Content
|
|
93
|
+
```bash
|
|
94
|
+
function test_output_contains_error_message() {
|
|
95
|
+
local result
|
|
96
|
+
result=$(my_function)
|
|
97
|
+
assert_contains "Error:" "$result"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Testing Pattern Matching
|
|
102
|
+
```bash
|
|
103
|
+
function test_returns_numeric_id() {
|
|
104
|
+
local result
|
|
105
|
+
result=$(get_id)
|
|
106
|
+
assert_matches "^[0-9]+$" "$result"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Mocking External Commands
|
|
111
|
+
```bash
|
|
112
|
+
# Simple mock with fixed output
|
|
113
|
+
function test_fetch_returns_data() {
|
|
114
|
+
bashunit::mock curl <<< '{"status":"ok"}'
|
|
115
|
+
local result
|
|
116
|
+
result=$(fetch_data "http://example.com")
|
|
117
|
+
assert_contains "ok" "$result"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Conditional mock based on arguments
|
|
121
|
+
function test_deploy_selects_region() {
|
|
122
|
+
mockAws() {
|
|
123
|
+
if [[ "$1" == "region" ]]; then echo "eu-west-1"; fi
|
|
124
|
+
}
|
|
125
|
+
bashunit::mock aws mockAws
|
|
126
|
+
local result
|
|
127
|
+
result=$(deploy_function)
|
|
128
|
+
assert_equals "eu-west-1" "$result"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Multi-line mock with heredoc
|
|
132
|
+
function test_ps_lists_processes() {
|
|
133
|
+
bashunit::mock ps <<EOF
|
|
134
|
+
PID TTY TIME CMD
|
|
135
|
+
1234 pts/0 00:00:01 bash
|
|
136
|
+
EOF
|
|
137
|
+
local result
|
|
138
|
+
result=$(list_processes)
|
|
139
|
+
assert_line_count 2 "$result"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Edge Case Testing
|
|
144
|
+
|
|
145
|
+
| Edge Case | Test Pattern |
|
|
146
|
+
|-----------|--------------|
|
|
147
|
+
| Empty input | `function_name ""; assert_general_error` |
|
|
148
|
+
| Whitespace input | `function_name " "; assert_equals "trimmed" "$result"` |
|
|
149
|
+
| Special characters | `function_name '$!*@#'; assert_successful_code` |
|
|
150
|
+
| Large input | `function_name "$(seq 1 10000)"; assert_successful_code` |
|
|
151
|
+
| Null/zero values | `function_name 0; assert_equals "0" "$result"` |
|
|
152
|
+
|
|
153
|
+
## Coverage
|
|
154
|
+
|
|
155
|
+
bashunit tracks **whole-file** line-level code coverage natively via the `--coverage` flag. This includes main blocks, private helpers, and untestable external calls, so it can under-state how well public functions are tested or be unreachable on some scripts. To measure **public-function** coverage directly (scoped to `<namespace>::` functions, excluding private `_` helpers and non-function code), run `scripts/public-coverage.sh`.
|
|
156
|
+
|
|
157
|
+
### CLI Flags
|
|
158
|
+
|
|
159
|
+
| Flag | Purpose | Example |
|
|
160
|
+
|------|---------|---------|
|
|
161
|
+
| `--coverage` | Enable coverage tracking | `bashunit tests/ --coverage` |
|
|
162
|
+
| `--coverage-min N` | Fail if coverage below N% | `bashunit tests/ --coverage-min 80` |
|
|
163
|
+
| `--coverage-paths` | Source paths to track | `--coverage-paths "src/,lib/"` |
|
|
164
|
+
| `--coverage-exclude` | Exclude glob patterns | `--coverage-exclude "vendor/*,*_mock.sh"` |
|
|
165
|
+
| `--coverage-report-html` | Generate HTML report | `--coverage-report-html coverage/html` |
|
|
166
|
+
| `--no-coverage-report` | Console output only (skip LCOV) | `--coverage --no-coverage-report` |
|
|
167
|
+
|
|
168
|
+
### Enforcement
|
|
169
|
+
|
|
170
|
+
- **Default threshold**: 80% (set via `--coverage-min 80`)
|
|
171
|
+
- **Behaviour**: bashunit exits with code 1 if coverage is below the threshold -- suitable for CI pipelines
|
|
172
|
+
- **Override**: pass a different threshold to `/shell-test-run`
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Standard run with default 80% threshold
|
|
176
|
+
bashunit tests/ --coverage --coverage-paths src/ --coverage-min 80
|
|
177
|
+
|
|
178
|
+
# Lower threshold for scripts with untestable external calls
|
|
179
|
+
bashunit tests/ --coverage --coverage-min 50
|
|
180
|
+
|
|
181
|
+
# Example failure output
|
|
182
|
+
# Coverage: 75.5% (below minimum 80%)
|
|
183
|
+
# Exit code: 1
|
|
184
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Bashunit Test Template
|
|
2
|
+
|
|
3
|
+
Standard test file structure for bashunit tests.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
#!/usr/bin/env bash
|
|
7
|
+
# tests/[script-name]-test.sh
|
|
8
|
+
|
|
9
|
+
# Runs once before all tests in this file.
|
|
10
|
+
# Source with strict-mode save/restore: scripts under test open with
|
|
11
|
+
# `set -euo pipefail`, which would abort tests before assertions can inspect $?
|
|
12
|
+
function set_up_before_script() {
|
|
13
|
+
local _opts
|
|
14
|
+
_opts=$(shopt -po errexit nounset pipefail 2>/dev/null || true)
|
|
15
|
+
source path/to/[script_name].sh
|
|
16
|
+
eval "$_opts"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Runs before each test
|
|
20
|
+
function set_up() {
|
|
21
|
+
TEMP_FILE=$(bashunit::temp_file)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Runs after each test
|
|
25
|
+
function tear_down() {
|
|
26
|
+
rm -f "$TEMP_FILE"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function test_[function_name]_does_something() {
|
|
30
|
+
[function_name] "input" > "$TEMP_FILE" # main-shell call (see Coverage and Subshells)
|
|
31
|
+
assert_equals "expected" "$(<"$TEMP_FILE")"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function test_[function_name]_handles_empty_input() {
|
|
35
|
+
[function_name] ""
|
|
36
|
+
assert_general_error
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Runs once after all tests in this file
|
|
40
|
+
function tear_down_after_script() {
|
|
41
|
+
# Cleanup after all tests
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Template Components
|
|
46
|
+
|
|
47
|
+
| Component | Scope | Purpose |
|
|
48
|
+
|-----------|-------|---------|
|
|
49
|
+
| `set_up_before_script()` | Once before all tests | Source the script under test, start services |
|
|
50
|
+
| `set_up()` | Before each test | Create temp files, set environment variables |
|
|
51
|
+
| `tear_down()` | After each test | Remove temp files, unset variables |
|
|
52
|
+
| `test_[name]()` | Per test case | Individual test case -- name describes what is being tested |
|
|
53
|
+
| `tear_down_after_script()` | Once after all tests | Stop services, final cleanup |
|
|
54
|
+
|
|
55
|
+
## Naming Conventions
|
|
56
|
+
|
|
57
|
+
- **Test file:** `[script-name]-test.sh` in `tests/` directory
|
|
58
|
+
- **Test function:** `test_[function_name]_[scenario]()`
|
|
59
|
+
- **Custom title:** Use `set_test_title "Description"` inside test functions for descriptive reporting
|
|
60
|
+
- **Assertions:** Use descriptive assertions that clearly show expected vs actual
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# public-coverage.sh -- Measure PUBLIC-function line coverage (plugin convention).
|
|
3
|
+
#
|
|
4
|
+
# Public functions are <namespace>::function_name (e.g. shroutines::add, myapp::run).
|
|
5
|
+
# Private functions are _function_name (leading underscore). This script reports line
|
|
6
|
+
# coverage scoped to PUBLIC functions only -- excluding private helpers and all
|
|
7
|
+
# non-function code (main blocks, top-level statements, constants).
|
|
8
|
+
#
|
|
9
|
+
# Why: bashunit's --coverage / --coverage-min measure WHOLE-FILE line coverage, which
|
|
10
|
+
# includes untestable code (main blocks, external calls) and can be unreachable or
|
|
11
|
+
# meaningless. The ~80% target in the shell-test skill refers to public-function
|
|
12
|
+
# coverage; this script measures that figure directly.
|
|
13
|
+
#
|
|
14
|
+
# Requires: bashunit >= 0.36 (BASHUNIT_COVERAGE_SHOW_FUNCTIONS / per-function LCOV).
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# public-coverage.sh [--min N] [bashunit args...]
|
|
18
|
+
# --min N Minimum public-function coverage percent (default 80)
|
|
19
|
+
# Examples:
|
|
20
|
+
# public-coverage.sh tests/ --coverage-paths src/
|
|
21
|
+
# public-coverage.sh --min 90 tests/ --coverage-paths src/,lib/
|
|
22
|
+
#
|
|
23
|
+
# Exit status: 0 if public coverage >= threshold; 1 if below or no public functions.
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
MIN=80
|
|
28
|
+
ARGS=()
|
|
29
|
+
while (($# > 0)); do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--min)
|
|
32
|
+
MIN="${2:?--min requires a value}"
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
--)
|
|
36
|
+
shift
|
|
37
|
+
ARGS=("$@")
|
|
38
|
+
break
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
ARGS+=("$1")
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
# Run bashunit with per-function output forced on. Swallow its exit code: a test
|
|
48
|
+
# failure or a whole-file --coverage-min miss must not abort the public-coverage check.
|
|
49
|
+
RAW=$(BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true bashunit "${ARGS[@]}" --coverage 2>&1) || true
|
|
50
|
+
|
|
51
|
+
# From the "Functions" section, extract each entry as "<name> <hit> <total>".
|
|
52
|
+
FUNCS=$(printf '%s\n' "$RAW" |
|
|
53
|
+
sed $'s/\x1b\\[[0-9;]*m//g' |
|
|
54
|
+
awk '/^Functions$/{f=1;next} /^Coverage report written/{f=0} f && /lines \(/' |
|
|
55
|
+
sed -nE 's/^ +([^ ]+) +([0-9]+)\/ *([0-9]+) lines.*/\1 \2 \3/p') || true
|
|
56
|
+
|
|
57
|
+
echo "Public-function coverage (<namespace>::name; private _ helpers excluded)"
|
|
58
|
+
echo "------------------------------------------------------------------------"
|
|
59
|
+
|
|
60
|
+
PUB_HIT=0
|
|
61
|
+
PUB_TOTAL=0
|
|
62
|
+
while IFS=' ' read -r name hit total; do
|
|
63
|
+
[[ -z "${name:-}" ]] && continue
|
|
64
|
+
# public = namespaced with :: and not private (_-prefixed)
|
|
65
|
+
if [[ "$name" == *::* && "$name" != _* ]]; then
|
|
66
|
+
[[ "${total:-0}" =~ ^[0-9]+$ ]] || continue
|
|
67
|
+
((total == 0)) && continue
|
|
68
|
+
pct=$((hit * 100 / total))
|
|
69
|
+
printf ' %-36s %s/%s (%d%%)\n' "$name" "$hit" "$total" "$pct"
|
|
70
|
+
PUB_HIT=$((PUB_HIT + hit))
|
|
71
|
+
PUB_TOTAL=$((PUB_TOTAL + total))
|
|
72
|
+
fi
|
|
73
|
+
done <<<"${FUNCS}"
|
|
74
|
+
|
|
75
|
+
echo "------------------------------------------------------------------------"
|
|
76
|
+
if ((PUB_TOTAL == 0)); then
|
|
77
|
+
echo "No public (<namespace>::name) functions found in the coverage report." >&2
|
|
78
|
+
echo "Namespace your public functions (e.g. myapp::run) per the plugin convention," >&2
|
|
79
|
+
echo "or fall back to whole-file coverage: bashunit tests/ --coverage --coverage-min N." >&2
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
PUB_PCT=$((PUB_HIT * 100 / PUB_TOTAL))
|
|
84
|
+
printf 'Public functions: %d/%d lines (%d%%) -- threshold %d%%\n' \
|
|
85
|
+
"$PUB_HIT" "$PUB_TOTAL" "$PUB_PCT" "$MIN"
|
|
86
|
+
|
|
87
|
+
if ((PUB_PCT >= MIN)); then
|
|
88
|
+
echo "PASS"
|
|
89
|
+
exit 0
|
|
90
|
+
else
|
|
91
|
+
echo "FAIL: public-function coverage below threshold" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|