@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.
@@ -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
- # use this script to check if bash-base is compatible with your bash and environment
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
- # Mac OSX
6
- if ! [ -f "$HOME"/.local/lib/shellspec/shellspec ]; then
7
- brew install kcov
8
- curl -fsSL https://git.io/shellspec | sh -s 0.27.2 --yes
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
- # Uninstall shellspec:
14
- # rm -fr $HOME/.local/bin/shellspec
15
- # rm -fr $HOME/.local/lib/shellspec
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
- # Debian
18
- #docker run --rm -v "$(pwd)":/bash-base -w /bash-base shellspec/shellspec-debian:0.27-kcov "$@" spec/*.spec.sh
83
+ printf 'ShellSpec version: %s\n' "$installed_version"
84
+ printf 'BASH_VERSION: %s\n' "$BASH_VERSION"
19
85
 
20
- # Alpine
21
- #docker run --rm -v "$(pwd)":/bash-base -w /bash-base shellspec/shellspec:0.27-kcov "$@" spec/*.spec.sh
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
@@ -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 value "$(declare -p actual)" should eq "declare -a actual=([1]=\" as\" [5]=\"fd \")"
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 value "$(declare -p actual)" should eq "declare -a actual=([1]=\" as\" [5]=\"fd \")"
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