@renault-digital/bash-base 2.4.1 → 2.5.1
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/CONTRIBUTING.md +83 -10
- package/README.md +87 -6
- package/bin/bash-base +596 -312
- package/docs/bash-zsh-compatibility.md +202 -0
- package/docs/coverage.md +289 -0
- package/docs/design-principles.md +176 -0
- package/docs/references.md +122 -68
- package/docs/roadmap.md +289 -0
- package/man/bash-base.1 +71 -4
- package/package.json +4 -3
- package/scripts/test-shell-compat.sh +391 -0
- package/scripts/test.sh +82 -14
- package/spec/array.spec.sh +8 -2
- package/spec/coverage_edges.spec.sh +88 -0
- package/spec/coverage_zsh_array_map.spec.sh +24 -0
- package/spec/process.spec.sh +109 -15
- package/spec/reflection.spec.sh +1 -1
- package/spec/spec_helper.sh +17 -0
- package/spec/string.spec.sh +14 -17
- package/src/alias.sh +35 -4
- package/src/argument.sh +222 -124
- package/src/array.sh +96 -98
- package/src/constant.sh +1 -1
- package/src/document.sh +17 -11
- package/src/out.sh +26 -15
- package/src/process.sh +4 -4
- package/src/reflection.sh +76 -17
- package/src/string.sh +92 -27
- package/src/tool.sh +28 -12
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -u
|
|
3
|
+
|
|
4
|
+
ROOT="${BASH_BASE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
5
|
+
ALLOW_NO_ZSH="${ALLOW_NO_ZSH:-0}"
|
|
6
|
+
|
|
7
|
+
pass=0
|
|
8
|
+
fail=0
|
|
9
|
+
|
|
10
|
+
ok() {
|
|
11
|
+
printf 'PASS %s\n' "$1"
|
|
12
|
+
pass=$((pass + 1))
|
|
13
|
+
}
|
|
14
|
+
bad() {
|
|
15
|
+
printf 'FAIL %s\n' "$1" >&2
|
|
16
|
+
fail=$((fail + 1))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
printf 'Bash/zsh bash-base compatibility tests\n'
|
|
20
|
+
printf 'Root: %s\n\n' "$ROOT"
|
|
21
|
+
|
|
22
|
+
src_files=(
|
|
23
|
+
alias.sh argument.sh array.sh constant.sh document.sh
|
|
24
|
+
out.sh process.sh reflection.sh string.sh tool.sh
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
for f in "${src_files[@]}"; do
|
|
28
|
+
if bash -n "$ROOT/src/$f"; then
|
|
29
|
+
ok "bash syntax: $f"
|
|
30
|
+
else
|
|
31
|
+
bad "bash syntax: $f"
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
36
|
+
for f in "${src_files[@]}"; do
|
|
37
|
+
if zsh -n "$ROOT/src/$f"; then
|
|
38
|
+
ok "zsh syntax: $f"
|
|
39
|
+
else
|
|
40
|
+
bad "zsh syntax: $f"
|
|
41
|
+
fi
|
|
42
|
+
done
|
|
43
|
+
else
|
|
44
|
+
if [[ "$ALLOW_NO_ZSH" == 1 ]]; then
|
|
45
|
+
printf 'SKIP zsh syntax/runtime checks (zsh unavailable)\n'
|
|
46
|
+
else
|
|
47
|
+
bad "zsh is required for shared-shell verification"
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Source-level guards for constructs intentionally removed from the shared code.
|
|
52
|
+
# Comments are stripped before matching so explanatory text does not trigger.
|
|
53
|
+
active="$({
|
|
54
|
+
for f in "${src_files[@]}"; do
|
|
55
|
+
sed -E '/^[[:space:]]*#/d' "$ROOT/src/$f"
|
|
56
|
+
done
|
|
57
|
+
} 2>/dev/null)"
|
|
58
|
+
|
|
59
|
+
static_bad=0
|
|
60
|
+
for pattern in \
|
|
61
|
+
'read[[:space:]].*-p' \
|
|
62
|
+
'\$\{[^}]*\^\^\}' \
|
|
63
|
+
'\$\{[^}]*,,\}' \
|
|
64
|
+
'\$\{[^}]*\^\}' \
|
|
65
|
+
'\$\{![^}]+\}' \
|
|
66
|
+
'(^|[[:space:]])declare[[:space:]]+-' \
|
|
67
|
+
'export[[:space:]]+-f' \
|
|
68
|
+
'(^|[[:space:]])shopt([[:space:]]|$)' \
|
|
69
|
+
'(^|[[:space:]])setopt([[:space:]]|$)' \
|
|
70
|
+
'(^|[[:space:]])\[[[:space:]].*[[:space:]]==[[:space:]]'
|
|
71
|
+
do
|
|
72
|
+
if printf '%s\n' "$active" | grep -Eq "$pattern"; then
|
|
73
|
+
printf 'Static forbidden pattern matched: %s\n' "$pattern" >&2
|
|
74
|
+
static_bad=1
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if ((static_bad == 0)); then
|
|
79
|
+
ok "no known Bash-only/zsh-only constructs in active library code"
|
|
80
|
+
else
|
|
81
|
+
bad "shared-syntax static scan"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Function declarations should use the common name() form.
|
|
85
|
+
if grep -RInE '^[[:space:]]*function[[:space:]]+' "$ROOT/src" >/dev/null 2>&1; then
|
|
86
|
+
bad "function keyword remains in active source declarations"
|
|
87
|
+
else
|
|
88
|
+
ok "common function declaration syntax"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
runtime_payload="$(mktemp "${TMPDIR:-/tmp}/bash-base-runtime.XXXXXX")" || exit 1
|
|
92
|
+
trap 'rm -f "$runtime_payload"' EXIT INT TERM HUP
|
|
93
|
+
cat >"$runtime_payload" <<'PAYLOAD'
|
|
94
|
+
set -u
|
|
95
|
+
root="$1"
|
|
96
|
+
cd "$root"
|
|
97
|
+
|
|
98
|
+
source src/constant.sh
|
|
99
|
+
source src/out.sh
|
|
100
|
+
source src/tool.sh
|
|
101
|
+
source src/string.sh
|
|
102
|
+
source src/array.sh
|
|
103
|
+
source src/reflection.sh
|
|
104
|
+
source src/argument.sh
|
|
105
|
+
source src/process.sh
|
|
106
|
+
source src/document.sh
|
|
107
|
+
source src/alias.sh
|
|
108
|
+
|
|
109
|
+
expected_script_name="${0##*/}"
|
|
110
|
+
|
|
111
|
+
runtime_failures=0
|
|
112
|
+
assert_eq() {
|
|
113
|
+
local name="$1" expected="$2" actual="$3"
|
|
114
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
115
|
+
printf 'ASSERT FAIL [%s]\nexpected: <%s>\nactual: <%s>\n' "$name" "$expected" "$actual" >&2
|
|
116
|
+
runtime_failures=$((runtime_failures + 1))
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assert_status() {
|
|
122
|
+
local name="$1" expected="$2" actual="$3"
|
|
123
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
124
|
+
printf 'ASSERT FAIL [%s] expected status %s got %s\n' "$name" "$expected" "$actual" >&2
|
|
125
|
+
runtime_failures=$((runtime_failures + 1))
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
assert_eq script_name "$expected_script_name" "$THIS_SCRIPT_NAME"
|
|
131
|
+
assert_eq upper ABC "$(string_upper aBc)"
|
|
132
|
+
assert_eq lower abc "$(string_lower aBc)"
|
|
133
|
+
assert_eq upper_first Abc "$(string_upper_first aBc)"
|
|
134
|
+
assert_eq substring 's fd' "$(string_sub 2 -1 ' as fd ')"
|
|
135
|
+
assert_eq substring_negative_start 'fd' "$(string_sub -3 -1 ' as fd ')"
|
|
136
|
+
assert_eq substring_empty '' "$(string_sub 0 0 'abc')"
|
|
137
|
+
assert_eq substring_out_of_range '' "$(string_sub -99 -1 'abc')"
|
|
138
|
+
|
|
139
|
+
# The heredoc may contain shell quotes and command syntax as plain text.
|
|
140
|
+
declare_heredoc literal_heredoc <<'LITERAL_HEREDOC'
|
|
141
|
+
It's text: $(printf NEVER_EXECUTE); "quoted".
|
|
142
|
+
LITERAL_HEREDOC
|
|
143
|
+
assert_eq literal_heredoc 'It'"'"'s text: $(printf NEVER_EXECUTE); "quoted".' "$literal_heredoc"
|
|
144
|
+
|
|
145
|
+
arr=(' a ' ' b c ')
|
|
146
|
+
assert_eq array_join ' a | b c ' "$(array_join '|' arr)"
|
|
147
|
+
array_contains arr ' b c '
|
|
148
|
+
assert_status array_contains 0 "$?"
|
|
149
|
+
|
|
150
|
+
arr_sort=('bb' 'aa' 'aa')
|
|
151
|
+
array_sort arr_sort
|
|
152
|
+
assert_eq array_sort 'aa|aa|bb' "$(array_join '|' arr_sort)"
|
|
153
|
+
|
|
154
|
+
array_equal_a=('bb' 'aa')
|
|
155
|
+
array_equal_b=('aa' 'bb')
|
|
156
|
+
array_equals array_equal_a array_equal_b true
|
|
157
|
+
assert_status array_equals 0 "$?"
|
|
158
|
+
|
|
159
|
+
left=('aa' 'bb' 'aa' 'cc')
|
|
160
|
+
right=('aa' 'aa' 'dd' 'bb')
|
|
161
|
+
array_union left right union_result
|
|
162
|
+
assert_eq array_union 'aa|bb|cc|dd' "$(array_join '|' union_result)"
|
|
163
|
+
|
|
164
|
+
remove_arr=('a' ' b' 'c' 'a b' 'f' 'g')
|
|
165
|
+
array_remove remove_arr 'a b'
|
|
166
|
+
assert_eq array_remove 'a| b|c|f|g' "$(array_join '|' remove_arr)"
|
|
167
|
+
|
|
168
|
+
map_arr=(' a ' " b '(c ")
|
|
169
|
+
array_map map_arr 'string_trim | wc -m | string_trim' mapped
|
|
170
|
+
assert_eq array_map '2|6' "$(array_join '|' mapped)"
|
|
171
|
+
|
|
172
|
+
filter_arr=('NAME' 'NAME B' 'OTHER')
|
|
173
|
+
array_filter filter_arr 'NAME' filtered
|
|
174
|
+
assert_eq array_filter 'NAME|NAME B' "$(array_join '|' filtered)"
|
|
175
|
+
|
|
176
|
+
# array_describe/array_from_describe use each shell's own declaration format;
|
|
177
|
+
# round-trip behavior, not declaration spelling, is the cross-shell contract.
|
|
178
|
+
desc="$(array_describe arr)"
|
|
179
|
+
array_from_describe roundtrip "$desc"
|
|
180
|
+
assert_eq array_roundtrip ' a | b c ' "$(array_join '|' roundtrip)"
|
|
181
|
+
|
|
182
|
+
# Bash regression: public array-by-name APIs must not shadow legitimate names.
|
|
183
|
+
values=('a' 'b')
|
|
184
|
+
assert_eq name_values_join 'a|b' "$(array_join '|' values)"
|
|
185
|
+
array_contains values b
|
|
186
|
+
assert_status name_values_contains 0 "$?"
|
|
187
|
+
array_map values string_upper mapped_values
|
|
188
|
+
assert_eq name_values_map 'A|B' "$(array_join '|' mapped_values)"
|
|
189
|
+
array_filter values a filtered_values
|
|
190
|
+
assert_eq name_values_filter 'a' "$(array_join '|' filtered_values)"
|
|
191
|
+
source=('a' 'b')
|
|
192
|
+
other=('b')
|
|
193
|
+
array_intersection source other inter false
|
|
194
|
+
assert_eq name_source_intersection 'b' "$(array_join '|' inter)"
|
|
195
|
+
array_subtract source other subtracted false
|
|
196
|
+
assert_eq name_source_subtract 'a' "$(array_join '|' subtracted)"
|
|
197
|
+
source1=('a')
|
|
198
|
+
array_union source1 other combined false
|
|
199
|
+
assert_eq name_source1_union 'a|b' "$(array_join '|' combined)"
|
|
200
|
+
source2=('b')
|
|
201
|
+
left_also=('a')
|
|
202
|
+
array_union left_also source2 combined false
|
|
203
|
+
assert_eq name_source2_union 'a|b' "$(array_join '|' combined)"
|
|
204
|
+
kept=(a b c)
|
|
205
|
+
array_remove kept b
|
|
206
|
+
assert_eq name_kept_remove 'a|c' "$(array_join '|' kept)"
|
|
207
|
+
|
|
208
|
+
# Exercise the historical Bash 3.2 `typeset -p` format even on newer
|
|
209
|
+
# Bash runners. Only the mocked builtin call is scoped to this subshell.
|
|
210
|
+
if [[ -n "${BASH_VERSION:-}" ]]; then
|
|
211
|
+
legacy_roundtrip="$(
|
|
212
|
+
typeset() {
|
|
213
|
+
if [[ "${1-}" == -p && "${2-}" == arr ]]; then
|
|
214
|
+
printf '%s\n' "declare -a arr='([0]=\" a \" [1]=\" b c \")'"
|
|
215
|
+
else
|
|
216
|
+
builtin typeset "$@"
|
|
217
|
+
fi
|
|
218
|
+
}
|
|
219
|
+
legacy_desc="$(array_describe arr)"
|
|
220
|
+
array_from_describe legacy_restored "$legacy_desc" || exit 1
|
|
221
|
+
array_join '|' legacy_restored
|
|
222
|
+
)"
|
|
223
|
+
assert_eq array_legacy_bash32_roundtrip ' a | b c ' "$legacy_roundtrip"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
assert_eq reflect_nth_arg 'ha ho' "$(reflect_nth_arg 3 'ab cdv "ha ho"')"
|
|
227
|
+
assert_eq reflect_nth_arg_complex 'SIA (lowercase, 3 chars)' "$(reflect_nth_arg 4 'args_valid_or_read myVar '\''^[0-9a-z]{3,3}$'\'' "SIA (lowercase, 3 chars)"')"
|
|
228
|
+
reflect_get_function_definition reflect_nth_arg >/dev/null
|
|
229
|
+
reflect_search_function '^reflect_nth_arg$' | grep -qx 'reflect_nth_arg'
|
|
230
|
+
reflect_search_variable '^COLOR_BLUE$' | grep -qx 'COLOR_BLUE'
|
|
231
|
+
|
|
232
|
+
set -- "param 1" "param 2"
|
|
233
|
+
args_parse $# "$@" var1 var2
|
|
234
|
+
assert_eq args_parse_var1 'param 1' "$var1"
|
|
235
|
+
assert_eq args_parse_var2 'param 2' "$var2"
|
|
236
|
+
|
|
237
|
+
positionalValue='OLD'
|
|
238
|
+
args_parse 1 X positionalValue
|
|
239
|
+
assert_eq args_parse_shadow_positionalValue X "$positionalValue"
|
|
240
|
+
prompt='OLD'
|
|
241
|
+
args_parse 1 Y prompt
|
|
242
|
+
assert_eq args_parse_shadow_prompt Y "$prompt"
|
|
243
|
+
|
|
244
|
+
if [[ -n "${BASH_VERSION:-}" ]]; then
|
|
245
|
+
# Legacy Bash output and implicit shell-state behavior; zsh uses its own semantics.
|
|
246
|
+
assert_eq reflect_zero 0 "$(reflect_nth_arg 0 'a b c')"
|
|
247
|
+
assert_eq reflect_oob '' "$(set +u; reflect_nth_arg 99 'a b c')"
|
|
248
|
+
mapped_input=('-n' 'b')
|
|
249
|
+
array_map mapped_input cat mapped_output
|
|
250
|
+
assert_eq map_legacy_echo '|'"b" "$(array_join '|' mapped_output)"
|
|
251
|
+
display_name=' abc '
|
|
252
|
+
expected_display="$(printf '%-30.30s%s' 'display_name:' "${COLOR_BLUE}abc${COLOR_END}")"
|
|
253
|
+
assert_eq print_args_legacy_spaces "$expected_display" "$(print_args display_name)"
|
|
254
|
+
default_name=' abc '
|
|
255
|
+
args_valid_or_default default_name '^.*abc.*$' desc >/dev/null
|
|
256
|
+
assert_eq args_default_legacy_spaces abc "$default_name"
|
|
257
|
+
read_out="$(printf ' abc \n' | args_valid_or_read read_name '^.+$' 'enter')"
|
|
258
|
+
assert_eq args_read_legacy_spaces "Inputted value: ${COLOR_BLUE}'abc'${COLOR_END}" "$read_out"
|
|
259
|
+
proposed_out="$(args_valid_or_read proposed_name '^.+$' 'enter' ' foo ' <<<'')"
|
|
260
|
+
assert_eq args_read_legacy_proposed "Inputted value: ${COLOR_BLUE}'foo'${COLOR_END}" "$proposed_out"
|
|
261
|
+
default_name=''
|
|
262
|
+
fallback_out="$(args_valid_or_default default_name '^.+$' desc 'a b')"
|
|
263
|
+
if [[ "$fallback_out" != *"Inputted value: ${COLOR_BLUE}'a b'${COLOR_END}"* ]]; then
|
|
264
|
+
printf 'ASSERT FAIL [args_default_legacy_fallback] %s\n' "$fallback_out" >&2
|
|
265
|
+
runtime_failures=$((runtime_failures + 1))
|
|
266
|
+
fi
|
|
267
|
+
# Bash 4+/5+ case conversion must use native locale-sensitive rules.
|
|
268
|
+
if ((BASH_VERSINFO[0] >= 4)); then
|
|
269
|
+
utf8_locale="$(locale -a 2>/dev/null | grep -Ei -m1 '^(en_US\.UTF-8|C\.utf-?8|fr_FR\.UTF-8)$' || :)"
|
|
270
|
+
if [[ -n "$utf8_locale" ]]; then
|
|
271
|
+
original_upper="$(LC_ALL="$utf8_locale" bash -c 'v=école; echo "${v^^}"')"
|
|
272
|
+
if [[ "$original_upper" == 'ÉCOLE' ]]; then
|
|
273
|
+
assert_eq upper_legacy_unicode ÉCOLE "$(LC_ALL="$utf8_locale" string_upper école)"
|
|
274
|
+
assert_eq lower_legacy_unicode école "$(LC_ALL="$utf8_locale" string_lower ÉCOLE)"
|
|
275
|
+
assert_eq first_legacy_unicode École "$(LC_ALL="$utf8_locale" string_upper_first éCOLE)"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# read -r must use default IFS so Bash continues to accept spaced confirmations.
|
|
282
|
+
modeQuiet=false
|
|
283
|
+
spaced_yes="$(printf ' y \n' | confirm_to_continue)"
|
|
284
|
+
assert_eq confirm_spaced_yes 'Continue...' "$spaced_yes"
|
|
285
|
+
|
|
286
|
+
sel='abc'
|
|
287
|
+
select_out="$(printf '%s\n' 2 | args_valid_or_select_args sel 'which value' a ab d | grep 'Selected value')"
|
|
288
|
+
assert_eq args_select "Selected value: ${COLOR_BLUE}'ab'${COLOR_END}" "$select_out"
|
|
289
|
+
|
|
290
|
+
modeQuiet=false
|
|
291
|
+
read_out="$(printf '%s\n' 70022 | args_valid_or_read irn '^[0-9]{5,5}$' 'IRN')"
|
|
292
|
+
assert_eq args_valid_or_read "Inputted value: ${COLOR_BLUE}'70022'${COLOR_END}" "$read_out"
|
|
293
|
+
|
|
294
|
+
irn=70033
|
|
295
|
+
default_out="$(args_valid_or_default irn '^[0-9]{5,5}$' 'IRN' 70088)"
|
|
296
|
+
assert_eq args_valid_or_default "Inputted value: ${COLOR_BLUE}'70033'${COLOR_END}" "$default_out"
|
|
297
|
+
assert_eq args_valid_or_default_value 70033 "$irn"
|
|
298
|
+
|
|
299
|
+
modeQuiet=true
|
|
300
|
+
quiet_out="$(confirm_to_continue)"
|
|
301
|
+
assert_eq confirm_quiet '' "$quiet_out"
|
|
302
|
+
|
|
303
|
+
modeQuiet=false
|
|
304
|
+
yes_out="$(printf '%s\n' y | confirm_to_continue)"
|
|
305
|
+
assert_eq confirm_yes 'Continue...' "$yes_out"
|
|
306
|
+
|
|
307
|
+
# Legacy names intentionally remain callable, but are wrapper functions rather
|
|
308
|
+
# than shell aliases so non-interactive Bash/zsh behavior is deterministic.
|
|
309
|
+
modeQuiet=true
|
|
310
|
+
assert_eq args_confirm '' "$(args_confirm)"
|
|
311
|
+
var1='value 1'
|
|
312
|
+
assert_eq args_print "var1: ${COLOR_BLUE}value 1${COLOR_END}" "$(args_print var1)"
|
|
313
|
+
|
|
314
|
+
# Both old and new function declaration styles must remain discoverable.
|
|
315
|
+
cat > "${TMPDIR:-/tmp}/bash-base-reflect-test.$$" <<'EOS'
|
|
316
|
+
old_style() { :; }
|
|
317
|
+
function legacy_style() { :; }
|
|
318
|
+
EOS
|
|
319
|
+
names="$(reflect_function_names_of_file "${TMPDIR:-/tmp}/bash-base-reflect-test.$$")"
|
|
320
|
+
rm -f "${TMPDIR:-/tmp}/bash-base-reflect-test.$$"
|
|
321
|
+
printf '%s\n' "$names" | grep -qx old_style
|
|
322
|
+
printf '%s\n' "$names" | grep -qx legacy_style
|
|
323
|
+
|
|
324
|
+
if ((runtime_failures > 0)); then
|
|
325
|
+
printf 'RUNTIME ASSERTIONS FAILED: %s\n' "$runtime_failures" >&2
|
|
326
|
+
exit 1
|
|
327
|
+
fi
|
|
328
|
+
printf 'RUNTIME PASS\n'
|
|
329
|
+
PAYLOAD
|
|
330
|
+
|
|
331
|
+
run_runtime() {
|
|
332
|
+
local shell_name="$1" shell_bin="$2"
|
|
333
|
+
local output rc
|
|
334
|
+
output="$($shell_bin "$runtime_payload" "$ROOT" 2>&1)"
|
|
335
|
+
rc=$?
|
|
336
|
+
if [[ $rc -eq 0 && "$output" == *'RUNTIME PASS'* ]]; then
|
|
337
|
+
ok "$shell_name runtime behavior"
|
|
338
|
+
else
|
|
339
|
+
bad "$shell_name runtime behavior"
|
|
340
|
+
printf '%s\n' "$output" >&2
|
|
341
|
+
fi
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
run_runtime Bash bash
|
|
345
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
346
|
+
run_runtime zsh zsh
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
# args_parse scans the caller script for args_valid_* rules. In zsh, function
|
|
350
|
+
# execution can change $0, so exercise this through a real script rather than
|
|
351
|
+
# only calling args_parse from the smoke-test function body.
|
|
352
|
+
metadata_script="$(mktemp "${TMPDIR:-/tmp}/bash-base-args-metadata.XXXXXX")" || exit 1
|
|
353
|
+
cat >"$metadata_script" <<'META'
|
|
354
|
+
root="$1"
|
|
355
|
+
shift
|
|
356
|
+
cd "$root" || exit 1
|
|
357
|
+
source src/constant.sh
|
|
358
|
+
source src/out.sh
|
|
359
|
+
source src/tool.sh
|
|
360
|
+
source src/string.sh
|
|
361
|
+
source src/array.sh
|
|
362
|
+
source src/reflection.sh
|
|
363
|
+
source src/argument.sh
|
|
364
|
+
args_parse $# "$@" destination region
|
|
365
|
+
args_valid_or_read destination '^ok$' 'validated destination'
|
|
366
|
+
META
|
|
367
|
+
|
|
368
|
+
run_metadata_test() {
|
|
369
|
+
local shell_name="$1" shell_bin="$2"
|
|
370
|
+
local output rc
|
|
371
|
+
output="$($shell_bin "$metadata_script" "$ROOT" -h 2>&1)"
|
|
372
|
+
rc=$?
|
|
373
|
+
if [[ $rc -eq 0 &&
|
|
374
|
+
"$output" == *'validated destination'* &&
|
|
375
|
+
"$output" == *'"destinationValue" "regionValue"'* &&
|
|
376
|
+
"$output" != *'destinationValue\" \"regionValue'* ]]; then
|
|
377
|
+
ok "$shell_name args_parse caller-script metadata/help rendering"
|
|
378
|
+
else
|
|
379
|
+
bad "$shell_name args_parse caller-script metadata/help rendering"
|
|
380
|
+
printf '%s\n' "$output" >&2
|
|
381
|
+
fi
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
run_metadata_test Bash bash
|
|
385
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
386
|
+
run_metadata_test zsh zsh
|
|
387
|
+
fi
|
|
388
|
+
rm -f "$metadata_script"
|
|
389
|
+
|
|
390
|
+
printf '\n%d passed, %d failed\n' "$pass" "$fail"
|
|
391
|
+
((fail == 0))
|
package/scripts/test.sh
CHANGED
|
@@ -1,21 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Verify Bash/zsh compatibility first, then run the historical Bash ShellSpec
|
|
4
|
+
# suite. Pin the test framework version across macOS and GitHub Actions.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
set -u
|
|
7
|
+
set -o pipefail
|
|
8
|
+
|
|
9
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
cd "$ROOT" || exit 1
|
|
11
|
+
|
|
12
|
+
bash scripts/test-shell-compat.sh || exit $?
|
|
13
|
+
|
|
14
|
+
shellspec_version=0.28.1
|
|
15
|
+
shellspec_dir="$HOME/.local/lib/shellspec"
|
|
16
|
+
shellspec_bin="$shellspec_dir/shellspec"
|
|
17
|
+
|
|
18
|
+
installed_version=''
|
|
19
|
+
if [[ -f "$shellspec_bin" ]]; then
|
|
20
|
+
installed_version="$(bash "$shellspec_bin" --version 2>/dev/null || true)"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [[ "$installed_version" != "$shellspec_version" ]]; then
|
|
24
|
+
# Existing git installations can switch versions without deleting the
|
|
25
|
+
# installation directory or breaking the ~/.local/bin/shellspec link.
|
|
26
|
+
if [[ -d "$shellspec_dir/.git" ]]; then
|
|
27
|
+
curl -fsSL https://git.io/shellspec |
|
|
28
|
+
sh -s "$shellspec_version" --switch --yes || exit $?
|
|
29
|
+
elif [[ ! -e "$shellspec_dir" ]]; then
|
|
30
|
+
curl -fsSL https://git.io/shellspec |
|
|
31
|
+
sh -s "$shellspec_version" --yes || exit $?
|
|
32
|
+
else
|
|
33
|
+
printf '%s\n' \
|
|
34
|
+
"ERROR: $shellspec_dir exists but is not a ShellSpec git installation." \
|
|
35
|
+
'Cannot safely switch its version automatically.' >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Fail closed if installation/switching did not select the pinned version.
|
|
41
|
+
installed_version="$(bash "$shellspec_bin" --version 2>/dev/null || true)"
|
|
42
|
+
if [[ "$installed_version" != "$shellspec_version" ]]; then
|
|
43
|
+
printf 'ERROR: expected ShellSpec %s, got %s\n' \
|
|
44
|
+
"$shellspec_version" "${installed_version:-<not installed>}" >&2
|
|
45
|
+
exit 1
|
|
9
46
|
fi
|
|
10
|
-
bash -c 'echo BASH_VERSION: $BASH_VERSION'
|
|
11
|
-
bash "$HOME"/.local/lib/shellspec/shellspec "$@" spec/*.spec.sh
|
|
12
47
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
48
|
+
# Preserve the original optional Homebrew kcov setup for local macOS coverage.
|
|
49
|
+
# Normal `npm test` does not require kcov.
|
|
50
|
+
for arg in "$@"; do
|
|
51
|
+
if [[ "$arg" == --kcov ]] && ! command -v kcov >/dev/null 2>&1; then
|
|
52
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
53
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
54
|
+
printf '%s\n' 'ERROR: Homebrew is required to install kcov on macOS.' >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
brew install kcov || exit $?
|
|
58
|
+
else
|
|
59
|
+
printf '%s\n' 'ERROR: kcov is required for --kcov.' >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
# On macOS, kcov 43 can fail to attach to ShellSpec's test executor when
|
|
66
|
+
# RLIMIT_NOFILE is exceptionally high (observed soft limit: 1048576). A
|
|
67
|
+
# temporary 256-descriptor soft limit allowed ShellSpec --kcov to complete.
|
|
68
|
+
# This script is executed as a child process, so the caller's limit is intact.
|
|
69
|
+
# Apply only when coverage is explicitly requested; normal tests are unchanged.
|
|
70
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
71
|
+
for arg in "$@"; do
|
|
72
|
+
if [[ "$arg" == --kcov ]]; then
|
|
73
|
+
if ! ulimit -Sn 256; then
|
|
74
|
+
printf '%s\n' 'ERROR: unable to set macOS kcov soft FD limit to 256.' >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
printf 'macOS kcov soft FD limit: %s\n' "$(ulimit -Sn)"
|
|
78
|
+
break
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
fi
|
|
16
82
|
|
|
17
|
-
|
|
18
|
-
|
|
83
|
+
printf 'ShellSpec version: %s\n' "$installed_version"
|
|
84
|
+
printf 'BASH_VERSION: %s\n' "$BASH_VERSION"
|
|
19
85
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
86
|
+
# Since ShellSpec 0.28.0, explicitly named specfiles are still filtered by
|
|
87
|
+
# --pattern. Our historical files are named *.spec.sh (not *_spec.sh).
|
|
88
|
+
# --fail-no-examples prevents an empty test run from reporting success.
|
|
89
|
+
bash "$shellspec_bin" --pattern '*.spec.sh' --fail-no-examples "$@" spec/*.spec.sh
|
package/spec/array.spec.sh
CHANGED
|
@@ -278,13 +278,19 @@ Describe 'array_from_describe'
|
|
|
278
278
|
strDescribe="([1]=' as' [5]='fd ')"
|
|
279
279
|
It 'with value'
|
|
280
280
|
When call array_from_describe actual "${strDescribe}"
|
|
281
|
-
The
|
|
281
|
+
The status should be success
|
|
282
|
+
The value "${#actual[@]}" should eq "2"
|
|
283
|
+
The value "${actual[1]}" should eq " as"
|
|
284
|
+
The value "${actual[5]}" should eq "fd "
|
|
282
285
|
End
|
|
283
286
|
|
|
284
287
|
It 'with file'
|
|
285
288
|
echo "${strDescribe}" > temp_file.txt
|
|
286
289
|
When call eval "array_from_describe actual < temp_file.txt"
|
|
287
|
-
The
|
|
290
|
+
The status should be success
|
|
291
|
+
The value "${#actual[@]}" should eq "2"
|
|
292
|
+
The value "${actual[1]}" should eq " as"
|
|
293
|
+
The value "${actual[5]}" should eq "fd "
|
|
288
294
|
rm -fr temp_file.txt
|
|
289
295
|
End
|
|
290
296
|
End
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#shellcheck shell=bash
|
|
2
|
+
# Targeted coverage/regression tests for reachable Bash/zsh edge cases.
|
|
3
|
+
# Keep the Bash legacy behavior for reflect_nth_arg unchanged.
|
|
4
|
+
|
|
5
|
+
Include src/string.sh
|
|
6
|
+
Include src/reflection.sh
|
|
7
|
+
Include src/argument.sh
|
|
8
|
+
|
|
9
|
+
coverage_not_zsh() {
|
|
10
|
+
[[ -z "${ZSH_VERSION:-}" ]]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
coverage_bash_4_or_later() {
|
|
14
|
+
[[ -n "${BASH_VERSION:-}" ]] && (( BASH_VERSINFO[0] >= 4 ))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
coverage_source_argument() {
|
|
18
|
+
. src/argument.sh
|
|
19
|
+
[[ -n "${THIS_SCRIPT_PATH:-}" && -n "${THIS_SCRIPT_NAME:-}" ]]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Describe 'coverage edge branches with observable behavior'
|
|
23
|
+
It 'zsh rejects a nonnumeric reflect_nth_arg index'
|
|
24
|
+
Skip if 'zsh-specific return status' coverage_not_zsh
|
|
25
|
+
When call reflect_nth_arg invalid alpha beta
|
|
26
|
+
The status should eq 1
|
|
27
|
+
The output should eq ''
|
|
28
|
+
End
|
|
29
|
+
|
|
30
|
+
It 'zsh rejects an out-of-range reflect_nth_arg index'
|
|
31
|
+
Skip if 'zsh-specific return status' coverage_not_zsh
|
|
32
|
+
When call reflect_nth_arg 99 alpha beta
|
|
33
|
+
The status should eq 1
|
|
34
|
+
The output should eq ''
|
|
35
|
+
End
|
|
36
|
+
|
|
37
|
+
It 'returns empty output for an out-of-range substring start'
|
|
38
|
+
When call string_sub 99 1 abc
|
|
39
|
+
The status should eq 0
|
|
40
|
+
The output should eq ''
|
|
41
|
+
End
|
|
42
|
+
|
|
43
|
+
It 'rejects an invalid negative substring length on the portable path'
|
|
44
|
+
# Bash 4+ follows native substring expansion semantics and can abort
|
|
45
|
+
# the calling shell on an invalid negative length. This test targets
|
|
46
|
+
# only the Bash 3.2 / zsh portable fallback, and must not change Bash 4+.
|
|
47
|
+
Skip if 'Bash 4+ uses native substring expansion' coverage_bash_4_or_later
|
|
48
|
+
When call string_sub 2 -20 abc
|
|
49
|
+
The status should eq 1
|
|
50
|
+
The output should eq ''
|
|
51
|
+
End
|
|
52
|
+
|
|
53
|
+
It 'reports a valid value without an eval or pipe test wrapper'
|
|
54
|
+
coverage_read_value='12345'
|
|
55
|
+
When call args_valid_or_read coverage_read_value '^12345$' 'IRN'
|
|
56
|
+
The status should eq 0
|
|
57
|
+
The variable coverage_read_value should eq '12345'
|
|
58
|
+
The output should include 'Inputted value:'
|
|
59
|
+
End
|
|
60
|
+
|
|
61
|
+
It 'reports a defaulted value without an eval or pipe test wrapper'
|
|
62
|
+
coverage_default_value=''
|
|
63
|
+
When call args_valid_or_default coverage_default_value '^valid$' 'Value' valid
|
|
64
|
+
The status should eq 0
|
|
65
|
+
The variable coverage_default_value should eq 'valid'
|
|
66
|
+
The output should include 'Inputted value:'
|
|
67
|
+
End
|
|
68
|
+
|
|
69
|
+
It 'prints an explicit placeholder for an empty variable'
|
|
70
|
+
LOG_LEVEL=$LOG_LEVEL_WARN
|
|
71
|
+
empty_for_coverage=''
|
|
72
|
+
When call print_args empty_for_coverage
|
|
73
|
+
The status should eq 0
|
|
74
|
+
The output should include '<EMPTY>'
|
|
75
|
+
End
|
|
76
|
+
|
|
77
|
+
It 'rejects a heredoc target that is not a shell identifier'
|
|
78
|
+
When call declare_heredoc 'invalid-name'
|
|
79
|
+
The status should eq 2
|
|
80
|
+
The output should eq ''
|
|
81
|
+
End
|
|
82
|
+
|
|
83
|
+
It 'uses the zsh-specific entry path when argument.sh is sourced'
|
|
84
|
+
Skip if 'zsh-only source branch' coverage_not_zsh
|
|
85
|
+
When call coverage_source_argument
|
|
86
|
+
The status should eq 0
|
|
87
|
+
End
|
|
88
|
+
End
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#shellcheck shell=bash
|
|
2
|
+
# Target the zsh-specific array_map branch (src/array.sh:449).
|
|
3
|
+
# Does not change Bash semantics or existing tests.
|
|
4
|
+
Include src/array.sh
|
|
5
|
+
|
|
6
|
+
coverage_not_zsh_array() {
|
|
7
|
+
[[ -z "${ZSH_VERSION:-}" ]]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
coverage_map_zsh_values() {
|
|
11
|
+
local -a original_values=(alpha beta)
|
|
12
|
+
local -a mapped_values=()
|
|
13
|
+
array_map original_values cat mapped_values || return $?
|
|
14
|
+
printf '<%s>\n' "${mapped_values[@]}"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Describe 'zsh array_map portable input branch'
|
|
18
|
+
It 'maps two elements through the zsh-specific pipeline'
|
|
19
|
+
Skip if 'requires zsh' coverage_not_zsh_array
|
|
20
|
+
When call coverage_map_zsh_values
|
|
21
|
+
The status should eq 0
|
|
22
|
+
The output should eq $'<alpha>\n<beta>'
|
|
23
|
+
End
|
|
24
|
+
End
|