@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/bin/bash-base CHANGED
@@ -1,11 +1,51 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- shopt -s expand_aliases 2>/dev/null # This file is for compatibility with previous versions
3
+ # Backward-compatible command names. Use wrapper functions rather than aliases:
4
+ # aliases need different non-interactive expansion rules in Bash and zsh, while
5
+ # functions work identically when this library is sourced by either shell.
4
6
 
5
- alias args_confirm='confirm_to_continue'
6
- alias args_print='print_args'
7
+ # @NAME
8
+ # args_confirm -- show the name and value of variables, and continue execute if args_confirm by user, or exit if not
9
+ # @SYNOPSIS
10
+ # args_confirm variableName...
11
+ # @DESCRIPTION
12
+ # **variableName...** some existed variable names to show its value
13
+ # @EXAMPLES
14
+ # a="correct value"
15
+ # b="wrong value"
16
+ # args_confirm a b
17
+ # @SEE_ALSO
18
+ # args_print, stop_if_failed
19
+ args_confirm() {
20
+ confirm_to_continue "$@"
21
+ }
22
+
23
+ # @NAME
24
+ # args_print -- if LOG_LEVEL<=$LOG_LEVEL_WARN, show the name and value of variables
25
+ # @SYNOPSIS
26
+ # args_print variableName...
27
+ # @DESCRIPTION
28
+ # **variableName...** some existed variable names to show its value
29
+ # @EXAMPLES
30
+ # var1="value 1"
31
+ # var2="value 2"
32
+ # args_print var1 var2
33
+ # @SEE_ALSO
34
+ # print_header, print_error, print_success, print_warn, print_info, print_debug
35
+ args_print() {
36
+ print_args "$@"
37
+ }
7
38
 
8
- THIS_SCRIPT_NAME="$(basename "$0")" # the main script name
39
+ if [[ -n "${ZSH_VERSION:-}" ]]; then
40
+ # zsh may temporarily replace $0 with the sourced filename when
41
+ # FUNCTION_ARGZERO is enabled. ZSH_ARGZERO always keeps the invocation name.
42
+ _bash_base_argzero="${ZSH_ARGZERO:-$0}"
43
+ else
44
+ _bash_base_argzero="$0"
45
+ fi
46
+ THIS_SCRIPT_PATH="$_bash_base_argzero" # script source path for args_valid_* lookup
47
+ THIS_SCRIPT_NAME="$(basename "$_bash_base_argzero")" # the main script name
48
+ unset _bash_base_argzero
9
49
 
10
50
  SHORT_DESC="${THIS_SCRIPT_NAME%%.*}"
11
51
  SHORT_DESC="${SHORT_DESC/[\-_]/ }" # redefine it to show your script short description in the 'NAME' field of generated -h response
@@ -20,9 +60,14 @@ USAGE='' # redefine it in your script only if the gene
20
60
  # @EXAMPLES
21
61
  # args_parse $# "$@" newVar1 newVar2 newVar3
22
62
  # @SEE_ALSO
23
- function args_parse() {
24
- local nbArgValues nbPositionalVarNames option showUsage OPTARG OPTIND nbPositionalArgValues positionalArgValues positionalVarNames
25
- local element validCommand description defaultUsage strPositionalVarNames strPositionalValuesExamples
63
+ args_parse() {
64
+ local nbArgValues nbPositionalVarNames option OPTARG OPTIND nbPositionalArgValues
65
+ local showUsage=false
66
+ local element validCommand description defaultUsage
67
+ local strPositionalVarNames="" strPositionalValuesExamples=""
68
+ local -a positionalArgValues positionalVarNames
69
+ # Bash legacy contract: prompt, possibleValues, i, descriptions and
70
+ # onlyPositionalVarNames were caller-visible, not function-local.
26
71
 
27
72
  nbArgValues=$1
28
73
  shift 1
@@ -45,79 +90,103 @@ function args_parse() {
45
90
  shift $((OPTIND - 1))
46
91
 
47
92
  nbPositionalArgValues=$((nbArgValues - OPTIND + 1))
48
- positionalArgValues=("${@:1:nbPositionalArgValues}")
49
- positionalVarNames=("${@:nbPositionalArgValues+1:nbPositionalVarNames}")
93
+ positionalArgValues=()
94
+ # Split positional values from destination names without a new local
95
+ # counter; the historical Bash `i` global is handled below.
96
+ while (($# > nbPositionalVarNames)); do
97
+ positionalArgValues+=("$1")
98
+ shift
99
+ done
100
+ positionalVarNames=("$@")
50
101
 
51
102
  onlyPositionalVarNames=()
52
103
  descriptions=''
53
- if ((nbPositionalVarNames > 0)); then
54
- for i in $(seq 0 $((nbPositionalVarNames - 1))); do
55
- element=${positionalVarNames[i]}
56
104
 
105
+ # Pair values with variable names without relying on Bash's 0-based array
106
+ # indexes (zsh uses a different default array index base).
107
+ set -- "${positionalArgValues[@]}"
108
+ for element in "${positionalVarNames[@]}"; do
109
+ # Do not introduce a local value variable: callers may legally name
110
+ # their destination positionalValue (or prompt). Save it only in $1.
111
+
112
+ # zsh FUNCTION_ARGZERO may change $0 to the current function name.
113
+ # Only scan a caller script when there is an actual script file.
114
+ validCommand=''
115
+ if [[ -f "${THIS_SCRIPT_PATH:-}" ]]; then
57
116
  validCommand="$(
58
- grep -E "^\s*args_valid.*\s+${element}\s+" "$0" | head -1 |
117
+ grep -E "^[[:space:]]*args_valid.*[[:space:]]+${element}[[:space:]]+" "$THIS_SCRIPT_PATH" | head -1 |
59
118
  awk -F "'" -v OFS="'" '{
60
- for (i=2; i<=NF; i+=2) {
61
- gsub(/ /, "_SPACE_", $i);
62
- gsub(/\$/, "_DOLLAR_", $i);
63
- gsub(/\(/, "_PARENTHESES_LEFT_", $i);
64
- gsub(/\)/, "_PARENTHESES_RIGHT_", $i);
65
- }
66
- print
67
- }' |
119
+ for (i=2; i<=NF; i+=2) {
120
+ gsub(/ /, "_SPACE_", $i);
121
+ gsub(/\$/, "_DOLLAR_", $i);
122
+ gsub(/\(/, "_PARENTHESES_LEFT_", $i);
123
+ gsub(/\)/, "_PARENTHESES_RIGHT_", $i);
124
+ }
125
+ print
126
+ }' |
68
127
  sed -e "s/\'//g"
69
128
  )"
129
+ fi
70
130
 
71
- if [[ -z ${validCommand} ]]; then
72
- eval "${positionalVarNames[i]}='${positionalArgValues[i]}'"
131
+ if [[ -z $validCommand ]]; then
132
+ eval "${element}=\"\${1-}\""
133
+ onlyPositionalVarNames+=("${element}")
134
+ description="a valid value for ${element}"
135
+ else
136
+ if [[ "$validCommand" == *args_valid_or_select_pipe* ]]; then
137
+ eval "${element}=\"\${1-}\""
73
138
  onlyPositionalVarNames+=("${element}")
74
- description="a valid value for ${element}"
75
- else
76
- if [[ "${validCommand}" =~ 'args_valid_or_select_pipe' ]]; then
77
- eval "${positionalVarNames[i]}='${positionalArgValues[i]}'"
78
- onlyPositionalVarNames+=("${element}")
79
- prompt="$(reflect_nth_arg 4 "${validCommand}")"
80
- possibleValues=", possible values: $(reflect_nth_arg 3 "$validCommand")"
81
- elif [[ "${validCommand}" =~ 'args_valid_or_select_args' ]]; then
82
- eval "${positionalVarNames[i]}='${positionalArgValues[i]}'"
83
- onlyPositionalVarNames+=("${element}")
84
- prompt="$(reflect_nth_arg 3 "${validCommand}")"
85
- possibleValues=", you can select one using wizard if you do not know which value is valid"
86
- elif [[ "${validCommand}" =~ 'args_valid_or_select' ]]; then
87
- eval "${positionalVarNames[i]}='${positionalArgValues[i]}'"
88
- onlyPositionalVarNames+=("${element}")
89
- prompt="$(reflect_nth_arg 4 "${validCommand}")"
90
- possibleValues=", you can select one using wizard if you do not know which value is valid"
91
- elif [[ "${validCommand}" =~ args_valid_or_read ]]; then
92
- eval "${positionalVarNames[i]}='${positionalArgValues[i]}'"
93
- onlyPositionalVarNames+=("${element}")
94
- prompt="$(reflect_nth_arg 4 "${validCommand}")"
95
- if [[ -n $(reflect_nth_arg 5 "${validCommand}") ]]; then
96
- prompt="${prompt}, proposed value: $(reflect_nth_arg 5 "${validCommand}")"
97
- fi
98
- possibleValues=""
99
- elif [[ "${validCommand}" =~ args_valid_or_default ]]; then
100
- prompt="optional, $(reflect_nth_arg 4 "${validCommand}"). should be set like '${element}=value' before ${THIS_SCRIPT_NAME} or export to OS environment"
101
- if [[ -n $(reflect_nth_arg 5 "${validCommand}") ]]; then
102
- prompt="${prompt}. if absent, fallback to: $(reflect_nth_arg 5 "${validCommand}")"
103
- else
104
- prompt="${prompt}, empty if absent"
105
- fi
106
- possibleValues=""
139
+ prompt="$(reflect_nth_arg 4 "$validCommand")"
140
+ possibleValues=", possible values: $(reflect_nth_arg 3 "$validCommand")"
141
+ elif [[ "$validCommand" == *args_valid_or_select_args* ]]; then
142
+ eval "${element}=\"\${1-}\""
143
+ onlyPositionalVarNames+=("${element}")
144
+ prompt="$(reflect_nth_arg 3 "$validCommand")"
145
+ possibleValues=", you can select one using wizard if you do not know which value is valid"
146
+ elif [[ "$validCommand" == *args_valid_or_select* ]]; then
147
+ eval "${element}=\"\${1-}\""
148
+ onlyPositionalVarNames+=("${element}")
149
+ prompt="$(reflect_nth_arg 4 "$validCommand")"
150
+ possibleValues=", you can select one using wizard if you do not know which value is valid"
151
+ elif [[ "$validCommand" == *args_valid_or_read* ]]; then
152
+ eval "${element}=\"\${1-}\""
153
+ onlyPositionalVarNames+=("${element}")
154
+ prompt="$(reflect_nth_arg 4 "$validCommand")"
155
+ if [[ -n $(reflect_nth_arg 5 "$validCommand") ]]; then
156
+ prompt="${prompt}, proposed value: $(reflect_nth_arg 5 "$validCommand")"
157
+ fi
158
+ possibleValues=""
159
+ elif [[ "$validCommand" == *args_valid_or_default* ]]; then
160
+ prompt="optional, $(reflect_nth_arg 4 "$validCommand"). should be set like ${element}=value before ${THIS_SCRIPT_NAME} or export to OS environment"
161
+ if [[ -n $(reflect_nth_arg 5 "$validCommand") ]]; then
162
+ prompt="${prompt}. if absent, fallback to: $(reflect_nth_arg 5 "$validCommand")"
163
+ else
164
+ prompt="${prompt}, empty if absent"
107
165
  fi
166
+ possibleValues=""
167
+ fi
108
168
 
109
- prompt="$(
110
- echo "${prompt}" |
111
- string_replace "_SPACE_" " " |
112
- string_replace "_DOLLAR_" "$" |
113
- string_replace "_PARENTHESES_LEFT_" "(" |
114
- string_replace "_PARENTHESES_RIGHT_" ")"
115
- )"
169
+ prompt="$(
170
+ printf '%s\n' "${prompt}" |
171
+ string_replace "_SPACE_" " " |
172
+ string_replace "_DOLLAR_" "$" |
173
+ string_replace "_PARENTHESES_LEFT_" "(" |
174
+ string_replace "_PARENTHESES_RIGHT_" ")"
175
+ )"
116
176
 
117
- description="${prompt}${possibleValues}"
118
- fi
119
- descriptions+="$(printf "\n %-20s%s" "${element} " "${description}")"
120
- done
177
+ description="${prompt}${possibleValues}"
178
+ fi
179
+ descriptions+="$(printf "\n %-20s%s" "${element} " "${description}")"
180
+ if (($# > 0)); then
181
+ shift
182
+ fi
183
+ done
184
+ if [[ -n "${BASH_VERSION:-}" && $nbPositionalVarNames -gt 0 ]]; then
185
+ # The original Bash implementation used a caller-visible `i` as
186
+ # a loop counter over output names. Respect a destination named i.
187
+ if ! array_in i "${positionalVarNames[@]}"; then
188
+ i=$((nbPositionalVarNames - 1))
189
+ fi
121
190
  fi
122
191
 
123
192
  if [ ${#onlyPositionalVarNames[@]} -gt 0 ]; then
@@ -125,36 +194,36 @@ function args_parse() {
125
194
  strPositionalValuesExamples=" \"$(array_join 'Value" "' onlyPositionalVarNames)Value\""
126
195
  fi
127
196
 
128
- declare_heredoc defaultUsage <<-EOF
129
- ${COLOR_BOLD_YELLOW}NAME${COLOR_END}
130
- ${THIS_SCRIPT_NAME} -- ${SHORT_DESC}
197
+ declare_heredoc defaultUsage <<EOF
198
+ ${COLOR_BOLD_YELLOW}NAME${COLOR_END}
199
+ ${THIS_SCRIPT_NAME} -- ${SHORT_DESC}
131
200
 
132
- ${COLOR_BOLD_YELLOW}SYNOPSIS${COLOR_END}
133
- [optionalVariable=value ...] ./${THIS_SCRIPT_NAME} [-qh]${strPositionalVarNames}
201
+ ${COLOR_BOLD_YELLOW}SYNOPSIS${COLOR_END}
202
+ [optionalVariable=value ...] ./${THIS_SCRIPT_NAME} [-qh]${strPositionalVarNames}
134
203
 
135
- ${COLOR_BOLD_YELLOW}DESCRIPTION${COLOR_END}
136
- [-h] help, print the usage
137
- [-q] optional, Run quietly, no confirmation
138
- ${descriptions}
204
+ ${COLOR_BOLD_YELLOW}DESCRIPTION${COLOR_END}
205
+ [-h] help, print the usage
206
+ [-q] optional, Run quietly, no confirmation
207
+ ${descriptions}
139
208
 
140
- ${COLOR_BOLD_YELLOW}EXAMPLES${COLOR_END}
141
- help, print the usage:
142
- ./${THIS_SCRIPT_NAME} -h
209
+ ${COLOR_BOLD_YELLOW}EXAMPLES${COLOR_END}
210
+ help, print the usage:
211
+ ./${THIS_SCRIPT_NAME} -h
143
212
 
144
- run with all params, if run in quiet mode with -q, be sure all the params are valid:
145
- ./${THIS_SCRIPT_NAME} [-q]${strPositionalValuesExamples}
213
+ run with all params, if run in quiet mode with -q, be sure all the params are valid:
214
+ ./${THIS_SCRIPT_NAME} [-q]${strPositionalValuesExamples}
146
215
 
147
- run using wizard, input value for params step by step:
148
- ./${THIS_SCRIPT_NAME}
216
+ run using wizard, input value for params step by step:
217
+ ./${THIS_SCRIPT_NAME}
149
218
 
150
- run with optional variables, example:
151
- myOpt1=myValue1 myOpt2=myValue2 ./${THIS_SCRIPT_NAME}
219
+ run with optional variables, example:
220
+ myOpt1=myValue1 myOpt2=myValue2 ./${THIS_SCRIPT_NAME}
152
221
 
153
- or you can run with some params, and input value for other params using wizard.
154
- EOF
222
+ or you can run with some params, and input value for other params using wizard.
223
+ EOF
155
224
 
156
- if [ "$showUsage" == true ]; then
157
- echo -e "${USAGE:-$defaultUsage}"
225
+ if [[ "$showUsage" == true ]]; then
226
+ printf '%b\n' "${USAGE:-$defaultUsage}"
158
227
  exit 0
159
228
  fi
160
229
  }
@@ -175,26 +244,38 @@ function args_parse() {
175
244
  # args_valid_or_select varEmpty arr "Which app"
176
245
  # @SEE_ALSO
177
246
  # args_valid_or_select_pipe, args_valid_or_read, args_valid_or_default
178
- function args_valid_or_select() {
179
- local valueVarName validValuesVarName prompt value validValues PS3
247
+ args_valid_or_select() {
248
+ local valueVarName validValuesVarName prompt value PS3
249
+ local -a validValues
250
+
180
251
  valueVarName="${1}"
181
- validValuesVarName=$2
182
- validValues="$2[@]"
252
+ validValuesVarName="$2"
183
253
  prompt="${3}"
184
-
185
- value=$(eval eval "echo '$'${valueVarName}")
254
+ eval 'value="${'"$valueVarName"'-}"'
255
+ if [[ -n "${BASH_VERSION:-}" ]]; then
256
+ # Preserve Bash's historical echo/word-splitting normalization, without
257
+ # reparsing the value with eval (which would execute input as code).
258
+ # shellcheck disable=SC2086,SC2116
259
+ value="$(echo $value)"
260
+ fi
261
+ eval "validValues=( \"\${${validValuesVarName}[@]}\" )"
186
262
 
187
263
  while ! array_contains "${validValuesVarName}" "${value}"; do
188
- echo -e "\n${prompt} ?"
264
+ printf '\n%s ?\n' "$prompt"
189
265
  [[ -n "${value}" ]] && print_error "the input '${value}' is not valid."
190
266
 
191
267
  PS3="choose one by ${COLOR_BOLD_YELLOW}number${COLOR_END} [1|2|...] ? "
192
- select value in "${!validValues}"; do
268
+ select value in "${validValues[@]}"; do
193
269
  break
194
270
  done
195
271
  done
196
- eval "${valueVarName}='${value}'"
197
- printf "Selected value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(eval echo '$'"${valueVarName}")"
272
+
273
+ eval "${valueVarName}=\"\$value\""
274
+ if [[ -n "${BASH_VERSION:-}" ]]; then
275
+ # shellcheck disable=SC2086,SC2116
276
+ value="$(echo $value)"
277
+ fi
278
+ printf "Selected value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$value"
198
279
  }
199
280
 
200
281
  # @NAME
@@ -210,8 +291,9 @@ function args_valid_or_select() {
210
291
  # args_valid_or_select_pipe sel "a|ab|d" "which value"
211
292
  # @SEE_ALSO
212
293
  # args_valid_or_select, args_valid_or_select_args, args_valid_or_read, args_valid_or_default
213
- function args_valid_or_select_pipe() {
214
- local valueVarName validValues prompt newArray
294
+ args_valid_or_select_pipe() {
295
+ local valueVarName validValues prompt
296
+ local -a newArray
215
297
  valueVarName="${1}"
216
298
  validValues="${2}"
217
299
  prompt="${3}"
@@ -233,15 +315,21 @@ function args_valid_or_select_pipe() {
233
315
  # args_valid_or_select_args sel "which value" "a" "ab" "d"
234
316
  # @SEE_ALSO
235
317
  # args_valid_or_select, args_valid_or_select_pipe, args_valid_or_read, args_valid_or_default
236
- function args_valid_or_select_args() {
318
+ args_valid_or_select_args() {
237
319
  local valueVarName value prompt PS3
238
320
  valueVarName="${1}"
239
- value=$(eval eval "echo '$'${valueVarName}")
321
+ eval 'value="${'"$valueVarName"'-}"'
322
+ if [[ -n "${BASH_VERSION:-}" ]]; then
323
+ # Preserve Bash's historical echo/word-splitting normalization, without
324
+ # reparsing the value with eval (which would execute input as code).
325
+ # shellcheck disable=SC2086,SC2116
326
+ value="$(echo $value)"
327
+ fi
240
328
  prompt="${2}"
241
329
  shift 2
242
330
 
243
331
  while ! array_in "${value}" "$@"; do
244
- echo -e "\n${prompt} ?"
332
+ printf '\n%s ?\n' "$prompt"
245
333
  [[ -n "${value}" ]] && print_error "the input '${value}' is not valid."
246
334
 
247
335
  PS3="choose one by ${COLOR_BOLD_YELLOW}number${COLOR_END} [1|2|...] ? "
@@ -249,8 +337,13 @@ function args_valid_or_select_args() {
249
337
  break
250
338
  done
251
339
  done
252
- eval "${valueVarName}='${value}'"
253
- printf "Selected value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(eval echo '$'"${valueVarName}")"
340
+
341
+ eval "${valueVarName}=\"\$value\""
342
+ if [[ -n "${BASH_VERSION:-}" ]]; then
343
+ # shellcheck disable=SC2086,SC2116
344
+ value="$(echo $value)"
345
+ fi
346
+ printf "Selected value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$value"
254
347
  }
255
348
 
256
349
  # @NAME
@@ -268,28 +361,48 @@ function args_valid_or_select_args() {
268
361
  # args_valid_or_read destRootPackage '^.+$' "Destination root package" "${defaultDestRootPackage}"
269
362
  # @SEE_ALSO
270
363
  # args_valid_or_select, args_valid_or_select_args, args_valid_or_select_pipe, args_valid_or_default
271
- function args_valid_or_read() {
272
- local value regExp prompt proposedValue
273
- value=$(eval eval "echo '$'$1")
274
- regExp=${2}
275
- prompt="${3}"
276
- proposedValue="${4}"
364
+ args_valid_or_read() {
365
+ local valueVarName="$1"
366
+ local regExp="${2}"
367
+ local prompt="${3}"
368
+ local proposedValue="${4-}"
369
+ local value
370
+
371
+ eval 'value="${'"$valueVarName"'-}"'
372
+ if [[ -n "${BASH_VERSION:-}" ]]; then
373
+ # Preserve Bash's historical echo/word-splitting normalization, without
374
+ # reparsing the value with eval (which would execute input as code).
375
+ # shellcheck disable=SC2086,SC2116
376
+ value="$(echo $value)"
377
+ fi
277
378
 
278
379
  if [[ -n "${proposedValue}" ]]; then
279
380
  prompt="${prompt} [${proposedValue}]"
280
- if [[ -z "${value}" && "${modeQuiet}" == true ]]; then
381
+ if [[ -z "${value}" && "${modeQuiet:-false}" == true ]]; then
281
382
  value="${proposedValue}"
282
383
  fi
283
384
  fi
284
- while ! [[ ${value} =~ ${regExp} ]]; do
385
+
386
+ while ! [[ $value =~ $regExp ]]; do
285
387
  [[ -n "${value}" ]] && print_error "the input '${value}' is not valid, please input again."
286
- read -r -p "${prompt}: " value
388
+ if [[ -t 0 ]]; then
389
+ printf '%s: ' "$prompt" >&2
390
+ fi
391
+ read -r value || value=""
287
392
  if [[ -z "${value}" ]]; then
288
393
  value="${proposedValue}"
289
394
  fi
290
395
  done
291
- eval "${1}='${value}'"
292
- printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(eval echo '$'"${1}")"
396
+
397
+ eval "${valueVarName}=\"\$value\""
398
+ if [[ -n "${BASH_VERSION:-}" ]]; then
399
+ # Legacy Bash printed via `eval echo $name`: collapse display-only
400
+ # whitespace, but leave the actual assigned value unchanged.
401
+ # shellcheck disable=SC2086,SC2116
402
+ printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(echo $value)"
403
+ else
404
+ printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$value"
405
+ fi
293
406
  }
294
407
 
295
408
  # @NAME
@@ -310,22 +423,38 @@ function args_valid_or_read() {
310
423
  # args_valid_or_default destRootPackage '^.+$' "Destination root package" "${defaultDestRootPackage}"
311
424
  # @SEE_ALSO
312
425
  # args_valid_or_select, args_valid_or_select_args, args_valid_or_select_pipe, args_valid_or_read
313
- function args_valid_or_default() {
314
- local value regExp prompt defaultValue
315
- value=$(eval eval "echo '$'$1")
316
- regExp=${2}
317
- prompt="${3}"
318
- defaultValue="${4}"
426
+ args_valid_or_default() {
427
+ local valueVarName="$1"
428
+ local regExp="${2}"
429
+ local prompt="${3}"
430
+ local defaultValue="${4-}"
431
+ local value
432
+
433
+ eval 'value="${'"$valueVarName"'-}"'
434
+ if [[ -n "${BASH_VERSION:-}" ]]; then
435
+ # Preserve Bash's historical echo/word-splitting normalization, without
436
+ # reparsing the value with eval (which would execute input as code).
437
+ # shellcheck disable=SC2086,SC2116
438
+ value="$(echo $value)"
439
+ fi
319
440
 
320
- if [[ -z "${value}" || ! ${value} =~ ${regExp} ]]; then
321
- prompt="for param ${1}:"
441
+ if [[ -z "${value}" || ! $value =~ $regExp ]]; then
442
+ prompt="for param ${valueVarName}:"
322
443
  [[ -n "${value}" ]] && prompt+=" the input '${value}' is not valid" || prompt+=" no input"
323
444
  [[ -n "${defaultValue}" ]] && prompt+=", fallback to default value '${defaultValue}'" || prompt+=", fallback to empty"
324
445
  print_warn "${prompt}"
325
446
  value="${defaultValue}"
326
447
  fi
327
- eval "${1}='${value}'"
328
- printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(eval echo '$'"${1}")"
448
+
449
+ eval "${valueVarName}=\"\$value\""
450
+ if [[ -n "${BASH_VERSION:-}" ]]; then
451
+ # Legacy Bash printed via `eval echo $name`: collapse display-only
452
+ # whitespace, but leave the actual assigned value unchanged.
453
+ # shellcheck disable=SC2086,SC2116
454
+ printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$(echo $value)"
455
+ else
456
+ printf "Inputted value: ${COLOR_BLUE}'%s'${COLOR_END}\n" "$value"
457
+ fi
329
458
  }
330
459
 
331
460
  # @NAME
@@ -340,25 +469,28 @@ function args_valid_or_default() {
340
469
  # array_join '|' myArry ==> " a | b c "
341
470
  # @SEE_ALSO
342
471
  # string_split_to_array, array_describe, array_from_describe
343
- function array_join() {
472
+ array_join() {
344
473
  local delimiter="$1"
345
- local array="$2[@]"
474
+ local array="$2[@]" # Same local names as the pre-PR Bash implementation.
475
+ local element result="" delimiterLength
346
476
 
347
- local element result delimiterLength
348
- for element in "${!array}"; do
477
+ # Expand the named array directly into function positional parameters. This
478
+ # avoids introducing new local variables that shadow caller array names.
479
+ eval 'set -- "${'"$2"'[@]}"'
480
+ for element in "$@"; do
349
481
  result="${result}${element}${delimiter}"
350
482
  done
351
483
 
352
484
  delimiterLength=$(string_length "${delimiter}")
353
485
  if string_is_empty "${result}"; then
354
- echo ''
486
+ printf '\n'
355
487
  else
356
488
  string_sub 0 $((0 - delimiterLength)) "${result}"
357
489
  fi
358
490
  }
359
491
 
360
492
  # @NAME
361
- # array_describe -- convert the array to its string representation
493
+ # array_describe -- convert the array to its shell-native string representation
362
494
  # @SYNOPSIS
363
495
  # array_describe arrayVarName
364
496
  # @DESCRIPTION
@@ -368,8 +500,20 @@ function array_join() {
368
500
  # array_describe myArray ==> ([0]='a' [1]='b')
369
501
  # @SEE_ALSO
370
502
  # string_split_to_array, array_join, array_from_describe
371
- function array_describe() {
372
- declare -p "$1" | string_after_first "=" | tr '"' "'"
503
+ array_describe() {
504
+ local _bash_base_declaration _bash_base_values
505
+
506
+ _bash_base_declaration="$(typeset -p "$1")" || return 1
507
+ _bash_base_values="${_bash_base_declaration#*=}"
508
+
509
+ # macOS Bash 3.2 wraps array assignments in one outer single-quoted
510
+ # word. Newer Bash prints the compound assignment directly. Decode
511
+ # the outer shell quoting before preserving the existing output format.
512
+ if [[ -n "${BASH_VERSION:-}" && "$_bash_base_values" == \'*\' ]]; then
513
+ eval "_bash_base_values=$_bash_base_values" || return 1
514
+ fi
515
+
516
+ printf '%s\n' "$_bash_base_values" | tr '"' "'"
373
517
  }
374
518
 
375
519
  # @NAME
@@ -384,7 +528,7 @@ function array_describe() {
384
528
  # array_from_describe myNewArray < fileNameContentString
385
529
  # @SEE_ALSO
386
530
  # string_split_to_array, array_join, array_describe
387
- function array_from_describe() {
531
+ array_from_describe() {
388
532
  local newArrayVarName="$1"
389
533
  local string="${2-$(cat)}"
390
534
 
@@ -405,11 +549,14 @@ function array_from_describe() {
405
549
  # echo "ab" | array_contains arr
406
550
  # @SEE_ALSO
407
551
  # array_remove, array_in
408
- function array_contains() {
552
+ array_contains() {
409
553
  local array="$1[@]"
410
554
  local seeking="${2-$(cat)}"
411
555
 
412
- array_in "${seeking}" "${!array}"
556
+ # Use the same locals as before; adding a `values` or `arrayVarName`
557
+ # local here would hide callers' arrays with those legitimate names.
558
+ eval 'set -- "${'"$1"'[@]}"'
559
+ array_in "${seeking}" "$@"
413
560
  }
414
561
 
415
562
  # @NAME
@@ -424,7 +571,7 @@ function array_contains() {
424
571
  # array_in "a b" "a" "b" "c" "a b" "f" "g"
425
572
  # @SEE_ALSO
426
573
  # array_remove, array_contains
427
- function array_in() {
574
+ array_in() {
428
575
  local seeking="$1"
429
576
  shift
430
577
  local element
@@ -446,16 +593,14 @@ function array_in() {
446
593
  # array_sort myArray ==> ([0]='aa' [1]='aa' [2]='bb')
447
594
  # @SEE_ALSO
448
595
  # array_sort_distinct
449
- function array_sort() {
596
+ array_sort() {
450
597
  local arrayVarName="$1"
451
598
  local strSorted arrSorted
452
599
 
453
600
  strSorted="$(array_join $'\n' "${arrayVarName}" | sort)"
454
601
  string_split_to_array $'\n' arrSorted "${strSorted}"
455
602
 
456
- local string="\${arrSorted[@]}"
457
- local command="${arrayVarName}=(\"${string}\")"
458
- eval "${command}"
603
+ eval "${arrayVarName}=( \"\${arrSorted[@]}\" )"
459
604
  }
460
605
 
461
606
  # @NAME
@@ -469,16 +614,14 @@ function array_sort() {
469
614
  # array_sort_distinct myArray ==> ([0]='aa' [1]='bb')
470
615
  # @SEE_ALSO
471
616
  # array_sort
472
- function array_sort_distinct() {
617
+ array_sort_distinct() {
473
618
  local arrayVarName="$1"
474
619
  local strSorted arrSorted
475
620
 
476
621
  strSorted="$(array_join $'\n' "${arrayVarName}" | sort -u)"
477
622
  string_split_to_array $'\n' arrSorted "${strSorted}"
478
623
 
479
- local string="\${arrSorted[@]}"
480
- local command="${arrayVarName}=(\"${string}\")"
481
- eval "${command}"
624
+ eval "${arrayVarName}=( \"\${arrSorted[@]}\" )"
482
625
  }
483
626
 
484
627
  # @NAME
@@ -491,38 +634,30 @@ function array_sort_distinct() {
491
634
  # myArray=('aa' 'bb' 'aa')
492
635
  # array_length myArray ==> 3
493
636
  # @SEE_ALSO
494
- function array_length() {
637
+ array_length() {
495
638
  local arrayVarName="$1"
496
- local string command tmp
497
-
498
- eval "string='$'{${arrayVarName}[@]}"
499
- command="tmp=(\"${string}\")"
500
- eval "${command}"
639
+ local -a tmp
501
640
 
502
- echo "${#tmp[@]}"
641
+ eval "tmp=( \"\${${arrayVarName}[@]}\" )"
642
+ printf '%s\n' "${#tmp[@]}"
503
643
  }
504
644
 
505
645
  # @NAME
506
- # array_reset_index -- reset the indexes of array to the sequence 0,1,2..., save the result to original variable name
646
+ # array_reset_index -- reset indexes to the shell-native contiguous sequence, save the result to original variable name
507
647
  # @SYNOPSIS
508
648
  # array_reset_index arrayVarName
509
649
  # @DESCRIPTION
510
650
  # **arrayVarName** the variable name of the array to be processed
511
651
  # @EXAMPLES
512
- # myArray=([2]='a' [5]='c' [11]='dd')
652
+ # Bash example: myArray=([2]='a' [5]='c' [11]='dd')
513
653
  # array_reset_index myArray ==> ([0]='a' [1]='c' [2]='dd')
514
654
  # @SEE_ALSO
515
- function array_reset_index() {
655
+ array_reset_index() {
516
656
  local arrayVarName="$1"
517
- local string command tmp
518
-
519
- eval "string='$'{${arrayVarName}[@]}"
520
- command="tmp=(\"${string}\")"
521
- eval "${command}"
657
+ local -a tmp
522
658
 
523
- string="\${tmp[@]}"
524
- command="${arrayVarName}=(\"${string}\")"
525
- eval "${command}"
659
+ eval "tmp=( \"\${${arrayVarName}[@]}\" )"
660
+ eval "${arrayVarName}=( \"\${tmp[@]}\" )"
526
661
  }
527
662
 
528
663
  # @NAME
@@ -540,7 +675,7 @@ function array_reset_index() {
540
675
  # array_equals myArray1 myArray2 false && echo Y || echo N ==> N
541
676
  # array_equals myArray1 myArray2 true && echo Y || echo N ==> Y
542
677
  # @SEE_ALSO
543
- function array_equals() {
678
+ array_equals() {
544
679
  local arrayVarName1="$1"
545
680
  local arrayVarName2="$2"
546
681
  local ignoreOrder=${3:-true}
@@ -563,7 +698,7 @@ function array_equals() {
563
698
  array_reset_index tmp2
564
699
  fi
565
700
 
566
- [ "$(array_describe tmp1)" == "$(array_describe tmp2)" ]
701
+ [[ "$(array_describe tmp1)" == "$(array_describe tmp2)" ]]
567
702
  }
568
703
 
569
704
  # @NAME
@@ -582,25 +717,23 @@ function array_equals() {
582
717
  # array_intersection myArray1 myArray2 newArray false
583
718
  # @SEE_ALSO
584
719
  # array_subtract, array_union
585
- function array_intersection() {
720
+ array_intersection() {
586
721
  local array1="$1[@]"
587
722
  local arrayVarName2="$2"
588
723
  local newArrayVarName="$3"
589
724
  local ignoreOrderAndDuplicated=${4:-true}
590
-
591
725
  local tmp element2 string command
726
+
727
+ eval 'set -- "${'"$1"'[@]}"'
592
728
  tmp=()
593
- for element2 in "${!array1}"; do
729
+ for element2 in "$@"; do
594
730
  array_contains "$arrayVarName2" "$element2" && array_append tmp "$element2"
595
731
  done
596
732
 
597
733
  if [ "${ignoreOrderAndDuplicated}" = true ]; then
598
734
  array_sort_distinct tmp
599
735
  fi
600
-
601
- string="\${tmp[@]}"
602
- command="${newArrayVarName}=(\"${string}\")"
603
- eval "${command}"
736
+ eval "${newArrayVarName}=( \"\${tmp[@]}\" )"
604
737
  }
605
738
 
606
739
  # @NAME
@@ -619,25 +752,23 @@ function array_intersection() {
619
752
  # array_subtract myArray1 myArray2 newArray false
620
753
  # @SEE_ALSO
621
754
  # array_intersection, array_union
622
- function array_subtract() {
755
+ array_subtract() {
623
756
  local array1="$1[@]"
624
757
  local arrayVarName2="$2"
625
758
  local newArrayVarName="$3"
626
759
  local ignoreOrderAndDuplicated=${4:-true}
627
-
628
760
  local tmp element2 string command
761
+
762
+ eval 'set -- "${'"$1"'[@]}"'
629
763
  tmp=()
630
- for element2 in "${!array1}"; do
764
+ for element2 in "$@"; do
631
765
  array_contains "$arrayVarName2" "$element2" || array_append tmp "$element2"
632
766
  done
633
767
 
634
768
  if [ "${ignoreOrderAndDuplicated}" = true ]; then
635
769
  array_sort_distinct tmp
636
770
  fi
637
-
638
- string="\${tmp[@]}"
639
- command="${newArrayVarName}=(\"${string}\")"
640
- eval "${command}"
771
+ eval "${newArrayVarName}=( \"\${tmp[@]}\" )"
641
772
  }
642
773
 
643
774
  # @NAME
@@ -656,28 +787,25 @@ function array_subtract() {
656
787
  # array_union myArray1 myArray2 newArray false
657
788
  # @SEE_ALSO
658
789
  # array_intersection, array_union
659
- function array_union() {
790
+ array_union() {
660
791
  local array1="$1[@]"
661
792
  local array2="$2[@]"
662
793
  local newArrayVarName="$3"
663
794
  local ignoreOrderAndDuplicated=${4:-true}
664
-
665
795
  local tmp element2 string command
796
+
797
+ # Union consumes both arrays as a single ordered sequence. Both are
798
+ # expanded before positional arguments are replaced.
799
+ eval 'set -- "${'"$1"'[@]}" "${'"$2"'[@]}"'
666
800
  tmp=()
667
- for element2 in "${!array1}"; do
668
- array_append tmp "$element2"
669
- done
670
- for element2 in "${!array2}"; do
801
+ for element2 in "$@"; do
671
802
  array_append tmp "$element2"
672
803
  done
673
804
 
674
805
  if [ "${ignoreOrderAndDuplicated}" = true ]; then
675
806
  array_sort_distinct tmp
676
807
  fi
677
-
678
- string="\${tmp[@]}"
679
- command="${newArrayVarName}=(\"${string}\")"
680
- eval "${command}"
808
+ eval "${newArrayVarName}=( \"\${tmp[@]}\" )"
681
809
  }
682
810
 
683
811
  # @NAME
@@ -692,14 +820,15 @@ function array_union() {
692
820
  # array_append myArray "ele ment1" "ele ment2"
693
821
  # @SEE_ALSO
694
822
  # array_remove
695
- function array_append() {
823
+ array_append() {
696
824
  local arrayVarName="$1"
697
825
  shift
698
826
 
699
- local elementToAppend command
827
+ local elementToAppend
700
828
  for elementToAppend in "$@"; do
701
- command="$arrayVarName+=(\"${elementToAppend}\")"
702
- eval "${command}"
829
+ # Keep the value out of the eval program text; expand it only after eval
830
+ # has selected the destination array name.
831
+ eval "${arrayVarName}+=( \"\$elementToAppend\" )"
703
832
  done
704
833
  }
705
834
 
@@ -715,24 +844,19 @@ function array_append() {
715
844
  # array_remove arr "ab"
716
845
  # @SEE_ALSO
717
846
  # array_contains, array_append
718
- function array_remove() {
847
+ array_remove() {
719
848
  local arrayVarName="$1"
720
849
  local element="$2"
721
-
722
850
  local string command tmp index
723
- eval "string='$'{${arrayVarName}[@]}"
724
- command="tmp=(\"${string}\")"
725
- eval "${command}"
726
851
 
727
- for index in "${!tmp[@]}"; do
728
- if [[ "${tmp[$index]}" == "${element}" ]]; then
729
- unset tmp["${index}"]
852
+ eval 'set -- "${'"$arrayVarName"'[@]}"'
853
+ tmp=()
854
+ for index in "$@"; do
855
+ if [[ "$index" != "$element" ]]; then
856
+ tmp+=("$index")
730
857
  fi
731
858
  done
732
-
733
- string="\${tmp[@]}"
734
- command="${arrayVarName}=(\"${string}\")"
735
- eval "${command}"
859
+ eval "${arrayVarName}=( \"\${tmp[@]}\" )"
736
860
  }
737
861
 
738
862
  # @NAME
@@ -746,7 +870,7 @@ function array_remove() {
746
870
  # arr=(" a " " b c ")
747
871
  # array_clone arr newArray
748
872
  # @SEE_ALSO
749
- function array_clone() {
873
+ array_clone() {
750
874
  local arrayVarName="$1"
751
875
  local arrayVarName2="$2"
752
876
 
@@ -765,23 +889,27 @@ function array_clone() {
765
889
  # arr=(" a " " b c ")
766
890
  # array_map arr "string_trim | wc -m | string_trim" newArray
767
891
  # @SEE_ALSO
768
- function array_map() {
892
+ array_map() {
769
893
  local array="$1[@]"
770
894
  local pipedOperators="$2"
771
- local newArrayVarName="$3"
772
-
895
+ local newArrayVarName="${3-}"
773
896
  local tmp element mapped_value string command
897
+
898
+ eval 'set -- "${'"$1"'[@]}"'
774
899
  tmp=()
775
- for element in "${!array}"; do
776
- escaped="${element//\'/\'"\'"\'}" # escape the single quote
777
- mapped_value="$(eval "echo '${escaped}' | ${pipedOperators}")"
900
+ for element in "$@"; do
901
+ if [[ -n "${BASH_VERSION:-}" ]]; then
902
+ # Retain the original Bash echo semantics for values such as -n,
903
+ # but never evaluate an input element as shell source.
904
+ mapped_value="$(echo "$element" | eval "$pipedOperators")"
905
+ else
906
+ mapped_value="$(printf '%s\n' "$element" | eval "$pipedOperators")"
907
+ fi
778
908
  tmp+=("${mapped_value}")
779
909
  done
780
910
 
781
911
  if [[ -n "${newArrayVarName}" ]]; then
782
- string="\${tmp[@]}"
783
- command="${newArrayVarName}=(\"${string}\")"
784
- eval "${command}"
912
+ eval "${newArrayVarName}=( \"\${tmp[@]}\" )"
785
913
  else
786
914
  array_join $'\n' tmp
787
915
  fi
@@ -799,29 +927,28 @@ function array_map() {
799
927
  # arr=("NAME A" "NAME B" "OTHER")
800
928
  # array_filter arr 'NAME' newArray
801
929
  # @SEE_ALSO
802
- function array_filter() {
930
+ array_filter() {
803
931
  local array="$1[@]"
804
932
  local regExp="$2"
805
- local newArrayVarName="$3"
806
-
933
+ local newArrayVarName="${3-}"
807
934
  local tmp element string command
935
+
936
+ eval 'set -- "${'"$1"'[@]}"'
808
937
  tmp=()
809
- for element in "${!array}"; do
810
- if [[ ${element} =~ ${regExp} ]]; then
938
+ for element in "$@"; do
939
+ if [[ $element =~ $regExp ]]; then
811
940
  array_append tmp "${element}"
812
941
  fi
813
942
  done
814
943
 
815
944
  if [[ -n "${newArrayVarName}" ]]; then
816
- string="\${tmp[@]}"
817
- command="${newArrayVarName}=(\"${string}\")"
818
- eval "${command}"
945
+ eval "${newArrayVarName}=( \"\${tmp[@]}\" )"
819
946
  else
820
947
  array_join $'\n' tmp
821
948
  fi
822
949
  }
823
950
 
824
- NEW_LINE_SED="\\$(echo -e '\r\n')" # Constant: the return and new line character, used with sed
951
+ NEW_LINE_SED="\\$(printf '\r\n')" # Constant: the return and new line character, used with sed
825
952
  export NEW_LINE_SED
826
953
 
827
954
  COLOR_BOLD_MAGENTA=$'\e[1;35m' # Constant: color for printing Header
@@ -853,9 +980,10 @@ export LOG_LEVEL_DEBUG LOG_LEVEL_INFO LOG_LEVEL_WARN LOG_LEVEL_ERROR
853
980
  # doc_lint_script_comment "${shellScriptFile}"
854
981
  # @SEE_ALSO
855
982
  # doc_comment_to_markdown
856
- function doc_lint_script_comment() {
983
+ doc_lint_script_comment() {
857
984
  local shellScriptFile="$1"
858
- local element strAllFunctionsAndTheirTags arrAllFunctionsAndTheirTags manTags strFunctionAndItsTags arrFunctionAndItsTags intersection counter
985
+ local element strAllFunctionsAndTheirTags arrAllFunctionsAndTheirTags manTags strFunctionAndItsTags arrFunctionAndItsTags intersection
986
+ local counter=0 lastFunction=""
859
987
 
860
988
  # format the comment
861
989
  sed -E -i'.bk' \
@@ -863,22 +991,27 @@ function doc_lint_script_comment() {
863
991
  -e "s/^#/# /g" \
864
992
  -e "s/^#[[:space:]]+$/#/g" \
865
993
  -e "s/^#[[:space:]]*!/#!/g" \
866
- -e "s/^#[[:space:]]*(@|-|#+)/# \1/g" \
994
+ -e "s/^#[[:space:]]*(@|-|#+)/# \\1/g" \
867
995
  "${shellScriptFile}"
868
996
  rm -fr "${shellScriptFile}.bk"
869
997
 
870
- # valid comment tags by man page convention
871
- strAllFunctionsAndTheirTags=$(grep -e '^# @' -e '^function ' "${shellScriptFile}" | string_replace_regex '\(\)|#' '' | string_trim)
998
+ # Accept both historical `function name()` declarations and the shared
999
+ # Bash/zsh `name()` form.
1000
+ strAllFunctionsAndTheirTags=$(grep -E -e '^# @' -e '^[[:space:]]*(function[[:space:]]+)?[0-9A-Za-z_-]+[[:space:]]*\(\)[[:space:]]*\{' "${shellScriptFile}" | string_replace_regex '\(\)|#' '' | string_trim)
872
1001
  string_split_to_array "{" arrAllFunctionsAndTheirTags "${strAllFunctionsAndTheirTags}"
873
- local manTags=('@NAME' '@SYNOPSIS' '@DESCRIPTION' '@EXAMPLES' '@SEE_ALSO')
1002
+ manTags=('@NAME' '@SYNOPSIS' '@DESCRIPTION' '@EXAMPLES' '@SEE_ALSO')
874
1003
  for strFunctionAndItsTags in "${arrAllFunctionsAndTheirTags[@]}"; do
875
1004
  string_split_to_array $'\n' arrFunctionAndItsTags "${strFunctionAndItsTags}"
876
1005
  array_intersection manTags arrFunctionAndItsTags intersection false
877
1006
  array_equals manTags intersection false
878
1007
  if [[ $? -ne 0 ]]; then
879
- ((counter++))
880
- declare -p manTags intersection
881
- print_error "the comments is not the same as template for ${arrFunctionAndItsTags[-1]}"
1008
+ counter=$((counter + 1))
1009
+ typeset -p manTags intersection
1010
+ lastFunction=""
1011
+ for element in "${arrFunctionAndItsTags[@]}"; do
1012
+ lastFunction="$element"
1013
+ done
1014
+ print_error "the comments is not the same as template for ${lastFunction}"
882
1015
  fi
883
1016
  done
884
1017
 
@@ -899,7 +1032,7 @@ function doc_lint_script_comment() {
899
1032
  # doc_comment_to_markdown src/reflection.sh docs/references.md
900
1033
  # @SEE_ALSO
901
1034
  # doc_lint_script_comment
902
- function doc_comment_to_markdown() {
1035
+ doc_comment_to_markdown() {
903
1036
  local fromShellFile="$1"
904
1037
  local toMarkdownFile="$2"
905
1038
 
@@ -918,7 +1051,7 @@ function doc_comment_to_markdown() {
918
1051
  string_replace_regex '^(\*\*)' "- \1"
919
1052
  )"
920
1053
 
921
- echo -e "@NAME\n${md//${NEW_LINE_SED}/\\n}" | #NEW_LINE_SED is not compatible by pandoc
1054
+ printf '%b\n' "@NAME\n${md//${NEW_LINE_SED}/\\n}" | # NEW_LINE_SED is not compatible by pandoc
922
1055
  string_replace_regex '@([A-Z]+)' "##### \1" >"${toMarkdownFile}"
923
1056
  }
924
1057
 
@@ -934,9 +1067,9 @@ LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO} # the default value INFO will be used if
934
1067
  # print_info "my message"
935
1068
  # @SEE_ALSO
936
1069
  # print_header, print_error, print_success, print_warn, print_args, print_info
937
- function print_debug() {
1070
+ print_debug() {
938
1071
  if [[ $LOG_LEVEL -le $LOG_LEVEL_DEBUG ]]; then
939
- echo -e "${COLOR_GRAY}DEBUG: ${1-$(cat)}${COLOR_END}"
1072
+ printf '%b\n' "${COLOR_GRAY}DEBUG: ${1-$(cat)}${COLOR_END}"
940
1073
  fi
941
1074
  }
942
1075
 
@@ -950,9 +1083,9 @@ function print_debug() {
950
1083
  # print_info "my message"
951
1084
  # @SEE_ALSO
952
1085
  # print_header, print_error, print_success, print_warn, print_args, print_debug
953
- function print_info() {
1086
+ print_info() {
954
1087
  if [[ $LOG_LEVEL -le $LOG_LEVEL_INFO ]]; then
955
- echo -e "${COLOR_END}INFO: ${1-$(cat)}"
1088
+ printf '%b\n' "${COLOR_END}INFO: ${1-$(cat)}"
956
1089
  fi
957
1090
  }
958
1091
 
@@ -966,9 +1099,9 @@ function print_info() {
966
1099
  # print_warn "my message"
967
1100
  # @SEE_ALSO
968
1101
  # print_header, print_error, print_success, print_info, print_args, print_debug
969
- function print_warn() {
1102
+ print_warn() {
970
1103
  if [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]]; then
971
- echo -e "${COLOR_BOLD_YELLOW}WARN: ${1-$(cat)} ${COLOR_END}"
1104
+ printf '%b\n' "${COLOR_BOLD_YELLOW}WARN: ${1-$(cat)} ${COLOR_END}"
972
1105
  fi
973
1106
  }
974
1107
 
@@ -982,9 +1115,9 @@ function print_warn() {
982
1115
  # print_error "my error message"
983
1116
  # @SEE_ALSO
984
1117
  # print_header, print_success, print_warn, print_info, print_args, print_debug
985
- function print_error() {
1118
+ print_error() {
986
1119
  if [[ $LOG_LEVEL -le $LOG_LEVEL_ERROR ]]; then
987
- echo -e "${COLOR_BOLD_RED}ERROR: ${1-$(cat)} ${COLOR_END}"
1120
+ printf '%b\n' "${COLOR_BOLD_RED}ERROR: ${1-$(cat)} ${COLOR_END}"
988
1121
  fi
989
1122
  }
990
1123
 
@@ -998,9 +1131,9 @@ function print_error() {
998
1131
  # print_success "my message"
999
1132
  # @SEE_ALSO
1000
1133
  # print_header, print_error, print_warn, print_info, print_args, print_debug
1001
- function print_success() {
1134
+ print_success() {
1002
1135
  if [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]]; then
1003
- echo -e "${COLOR_BOLD_GREEN}OK: ${1-$(cat)} ${COLOR_END}"
1136
+ printf '%b\n' "${COLOR_BOLD_GREEN}OK: ${1-$(cat)} ${COLOR_END}"
1004
1137
  fi
1005
1138
  }
1006
1139
 
@@ -1016,12 +1149,23 @@ function print_success() {
1016
1149
  # print_args var1 var2
1017
1150
  # @SEE_ALSO
1018
1151
  # print_header, print_error, print_success, print_warn, print_info, print_debug
1019
- function print_args() {
1152
+ print_args() {
1020
1153
  if [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]]; then
1021
1154
  local varName varValue varValueOutput
1022
1155
  for varName in "$@"; do
1023
- varValue=$(eval echo '$'"${varName}")
1024
- varValueOutput=$([[ -z "${varValue}" ]] && echo "<EMPTY>" || echo "${COLOR_BLUE}${varValue}${COLOR_END}")
1156
+ eval 'varValue="${'"$varName"'-}"'
1157
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1158
+ # Historical Bash print_args used `eval echo $name`, which
1159
+ # normalized whitespace and echo options in displayed values.
1160
+ # Echo expanded DATA, never eval it as new shell source.
1161
+ # shellcheck disable=SC2086,SC2116
1162
+ varValue="$(echo $varValue)"
1163
+ fi
1164
+ if [[ -z "$varValue" ]]; then
1165
+ varValueOutput="<EMPTY>"
1166
+ else
1167
+ varValueOutput="${COLOR_BLUE}${varValue}${COLOR_END}"
1168
+ fi
1025
1169
  printf "%-30.30s%s\n" "${varName}:" "${varValueOutput}"
1026
1170
  done
1027
1171
  fi
@@ -1037,9 +1181,9 @@ function print_args() {
1037
1181
  # print_header "My header1"
1038
1182
  # @SEE_ALSO
1039
1183
  # print_error, print_success, print_warn, print_info, print_args, print_debug
1040
- function print_header() {
1184
+ print_header() {
1041
1185
  if [[ $LOG_LEVEL -le $LOG_LEVEL_ERROR ]]; then
1042
- echo -e "${COLOR_BOLD_MAGENTA}\n### ${1-$(cat)} ${COLOR_END}"
1186
+ printf '%b\n' "${COLOR_BOLD_MAGENTA}\n### ${1-$(cat)} ${COLOR_END}"
1043
1187
  fi
1044
1188
  }
1045
1189
 
@@ -1053,7 +1197,7 @@ function print_header() {
1053
1197
  # prc_filter_by_port 9090
1054
1198
  # @SEE_ALSO
1055
1199
  # prc_kill_by_port
1056
- function prc_filter_by_port() {
1200
+ prc_filter_by_port() {
1057
1201
  if [ $# -eq 0 ]; then
1058
1202
  lsof -iTCP -sTCP:LISTEN -n -P
1059
1203
  elif [ $# -eq 1 ]; then
@@ -1072,7 +1216,7 @@ function prc_filter_by_port() {
1072
1216
  # prc_kill_by_port 9090
1073
1217
  # @SEE_ALSO
1074
1218
  # prc_filter_by_port
1075
- function prc_kill_by_port() {
1219
+ prc_kill_by_port() {
1076
1220
  local port="$1"
1077
1221
  local signal="${2-15}"
1078
1222
 
@@ -1095,7 +1239,7 @@ function prc_kill_by_port() {
1095
1239
  # prc_filter_by_cmd node
1096
1240
  # @SEE_ALSO
1097
1241
  # prc_kill_by_cmd
1098
- function prc_filter_by_cmd() {
1242
+ prc_filter_by_cmd() {
1099
1243
  if [ $# -eq 0 ]; then
1100
1244
  ps
1101
1245
  elif [ $# -eq 1 ]; then
@@ -1114,7 +1258,7 @@ function prc_filter_by_cmd() {
1114
1258
  # prc_kill_by_cmd my-app
1115
1259
  # @SEE_ALSO
1116
1260
  # prc_filter_by_cmd
1117
- function prc_kill_by_cmd() {
1261
+ prc_kill_by_cmd() {
1118
1262
  local cmd="$1"
1119
1263
  local signal="${2-15}"
1120
1264
 
@@ -1140,15 +1284,50 @@ function prc_kill_by_cmd() {
1140
1284
  # string="args_valid_or_read myVar '^[0-9a-z]{3,3}$' \"SIA\""
1141
1285
  # reflect_nth_arg 4 $string ==> "SIA"
1142
1286
  # @SEE_ALSO
1143
- function reflect_nth_arg() {
1144
- local index string args
1287
+ reflect_nth_arg() {
1288
+ local index string word position
1145
1289
 
1146
- string="$(echo "$@" | string_replace_regex '(\\|\||\{|>|<|&)' '\\\1')"
1147
- args=()
1148
- eval 'for word in '"${string}"'; do args+=("$word"); done'
1290
+ if [[ -n "${ZSH_VERSION:-}" ]]; then
1291
+ # zsh's lexer splits shell-quoted words without executing their contents.
1292
+ # Keep this expansion in eval so Bash can still parse the source file.
1293
+ string="$*"
1294
+ local -a __bash_base_reflect_words
1295
+ eval '__bash_base_reflect_words=( "${(@Q)${(z)string}}" )' || return 1
1296
+ set -- "${__bash_base_reflect_words[@]}"
1297
+ else
1298
+ string="$(printf '%s\n' "$*" | string_replace_regex '(\\|\||\{|>|<|&)' '\\\1')"
1299
+
1300
+ # Parse the same shell-like argument string as the historical implementation,
1301
+ # but use positional parameters instead of an indexed array so Bash/zsh array
1302
+ # index bases cannot change the result.
1303
+ eval "set -- ${string}"
1304
+ fi
1305
+ index="${1-}"
1306
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1307
+ # Bash legacy indexing addressed the entire parsed list, including
1308
+ # the index argument at [0]; missing indexes printed a blank line.
1309
+ # Keep that behavior, including Bash's native negative indexes.
1310
+ local -a __bash_base_legacy_reflect_words
1311
+ __bash_base_legacy_reflect_words=("$@")
1312
+ echo "${__bash_base_legacy_reflect_words[$index]}"
1313
+ return 0
1314
+ fi
1315
+ shift || true
1316
+
1317
+ if ! [[ "$index" =~ ^[0-9]+$ ]] || ((index < 1)); then
1318
+ return 1
1319
+ fi
1320
+
1321
+ position=1
1322
+ for word in "$@"; do
1323
+ if ((position == index)); then
1324
+ printf '%s\n' "$word"
1325
+ return 0
1326
+ fi
1327
+ position=$((position + 1))
1328
+ done
1149
1329
 
1150
- index="${args[0]}"
1151
- echo "${args[$index]}"
1330
+ return 1
1152
1331
  }
1153
1332
 
1154
1333
  # @NAME
@@ -1161,9 +1340,9 @@ function reflect_nth_arg() {
1161
1340
  # reflect_get_function_definition confirm_to_continue
1162
1341
  # @SEE_ALSO
1163
1342
  # reflect_function_names_of_file
1164
- function reflect_get_function_definition() {
1343
+ reflect_get_function_definition() {
1165
1344
  local functionName="$1"
1166
- declare -f "$functionName"
1345
+ typeset -f "$functionName"
1167
1346
  }
1168
1347
 
1169
1348
  # @NAME
@@ -1177,9 +1356,14 @@ function reflect_get_function_definition() {
1177
1356
  # reflect_function_names_of_file scripts/my_script.sh
1178
1357
  # @SEE_ALSO
1179
1358
  # reflect_get_function_definition
1180
- function reflect_function_names_of_file() {
1359
+ reflect_function_names_of_file() {
1181
1360
  local shellScriptFile="$1"
1182
- grep -E '^[[:space:]]*(function)?[[:space:]]*[0-9A-Za-z_\-]+[[:space:]]*\(\)[[:space:]]*\{?' "${shellScriptFile}" | cut -d'(' -f1 | sed -e "s/function//"
1361
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1362
+ # Existing Bash callers may rely on the original output spelling.
1363
+ grep -E '^[[:space:]]*(function)?[[:space:]]*[0-9A-Za-z_\-]+[[:space:]]*\(\)[[:space:]]*\{?' "$shellScriptFile" | cut -d'(' -f1 | sed -e 's/function//'
1364
+ return
1365
+ fi
1366
+ grep -E '^[[:space:]]*(function[[:space:]]+)?[0-9A-Za-z_-]+[[:space:]]*\(\)[[:space:]]*\{?' "$shellScriptFile" | sed -E -e 's/^[[:space:]]*function[[:space:]]+//' -e 's/[[:space:]]*\(\).*//' -e 's/[[:space:]]+$//'
1183
1367
  }
1184
1368
 
1185
1369
  # @NAME
@@ -1193,9 +1377,13 @@ function reflect_function_names_of_file() {
1193
1377
  # reflect_function_definitions_of_file scripts/my_script.sh
1194
1378
  # @SEE_ALSO
1195
1379
  # reflect_get_function_definition
1196
- function reflect_function_definitions_of_file() {
1380
+ reflect_function_definitions_of_file() {
1197
1381
  local shellScriptFile="$1"
1198
- sed -E -n '/^[[:space:]]*(function)?[[:space:]]*[0-9A-Za-z_\-]+[[:space:]]*\(\)[[:space:]]*\{?/,/^[[:space:]]*}/p' "${shellScriptFile}"
1382
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1383
+ sed -E -n '/^[[:space:]]*(function)?[[:space:]]*[0-9A-Za-z_\-]+[[:space:]]*\(\)[[:space:]]*\{?/,/^[[:space:]]*}/p' "$shellScriptFile"
1384
+ return
1385
+ fi
1386
+ sed -E -n '/^[[:space:]]*(function[[:space:]]+)?[0-9A-Za-z_-]+[[:space:]]*\(\)[[:space:]]*\{?/,/^[[:space:]]*}/p' "$shellScriptFile"
1199
1387
  }
1200
1388
 
1201
1389
  # @NAME
@@ -1209,9 +1397,17 @@ function reflect_function_definitions_of_file() {
1209
1397
  # reflect_search_function '^args_.*'
1210
1398
  # @SEE_ALSO
1211
1399
  # reflect_search_variable
1212
- function reflect_search_function() {
1400
+ reflect_search_function() {
1213
1401
  local functionNamePattern="$1"
1214
- declare -f | grep -E '\s+\(\)\s+' | sed -E 's/[(){ ]//g' | grep -Ei "${functionNamePattern}"
1402
+
1403
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1404
+ # Exact Bash legacy extraction/output (no new sorting or dedupe).
1405
+ typeset -f | grep -E '\s+\(\)\s+' | sed -E 's/[(){ ]//g' | grep -Ei "$functionNamePattern"
1406
+ elif [[ -n "${ZSH_VERSION:-}" ]]; then
1407
+ # Hide zsh-only parameter-expansion flags inside eval so the file still
1408
+ # parses in Bash.
1409
+ eval 'printf "%s\\n" "${(k)functions[@]}"' | LC_ALL=C sort -u | grep -Ei "$functionNamePattern"
1410
+ fi
1215
1411
  }
1216
1412
 
1217
1413
  # @NAME
@@ -1225,9 +1421,16 @@ function reflect_search_function() {
1225
1421
  # reflect_search_variable '^COLOR'
1226
1422
  # @SEE_ALSO
1227
1423
  # reflect_search_function
1228
- function reflect_search_variable() {
1424
+ reflect_search_variable() {
1229
1425
  local variableNamePattern="$1"
1230
- declare -p | grep -Eo '\s+\w+=' | sed -E 's/[= ]//g' | grep -Ei "${variableNamePattern}"
1426
+
1427
+ if [[ -n "${BASH_VERSION:-}" ]]; then
1428
+ # Enumerate Bash variables directly. Parsing typeset -p using \s/\w
1429
+ # depends on grep extensions unavailable on some macOS versions.
1430
+ compgen -A variable | grep -Ei "$variableNamePattern"
1431
+ elif [[ -n "${ZSH_VERSION:-}" ]]; then
1432
+ eval 'printf "%s\\n" "${(k)parameters[@]}"' | LC_ALL=C sort -u | grep -Ei "$variableNamePattern"
1433
+ fi
1231
1434
  }
1232
1435
 
1233
1436
  # @NAME
@@ -1241,7 +1444,7 @@ function reflect_search_variable() {
1241
1444
  # string_trim < logfile
1242
1445
  # echo " add " | string_trim
1243
1446
  # @SEE_ALSO
1244
- function string_trim() {
1447
+ string_trim() {
1245
1448
  local string="${1-$(cat)}"
1246
1449
  echo "${string}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' #trim ' '
1247
1450
  }
@@ -1257,7 +1460,7 @@ function string_trim() {
1257
1460
  # string_repeat 'abc' 5
1258
1461
  # echo 5 | string_repeat 'abc'
1259
1462
  # @SEE_ALSO
1260
- function string_repeat() {
1463
+ string_repeat() {
1261
1464
  local string="$1"
1262
1465
  local nbTimes=${2-$(cat)}
1263
1466
  printf "${string}%.0s" $(seq 1 "$nbTimes")
@@ -1274,7 +1477,7 @@ function string_repeat() {
1274
1477
  # string_length < logfile
1275
1478
  # echo " add " | string_length
1276
1479
  # @SEE_ALSO
1277
- function string_length() {
1480
+ string_length() {
1278
1481
  local string index
1279
1482
  string="${1-$(cat)}"
1280
1483
 
@@ -1294,7 +1497,7 @@ function string_length() {
1294
1497
  # echo " add " | string_is_empty
1295
1498
  # @SEE_ALSO
1296
1499
  # string_length
1297
- function string_is_empty() {
1500
+ string_is_empty() {
1298
1501
  local string="${1-$(cat)}"
1299
1502
 
1300
1503
  [[ $(string_length "$string") -eq 0 ]]
@@ -1310,7 +1513,7 @@ function string_is_empty() {
1310
1513
  # string_revert 'aBc'
1311
1514
  # echo 'aBc' | string_revert
1312
1515
  # @SEE_ALSO
1313
- function string_revert() {
1516
+ string_revert() {
1314
1517
  local string="${1-$(cat)}"
1315
1518
  echo "${string}" | rev
1316
1519
  }
@@ -1326,9 +1529,16 @@ function string_revert() {
1326
1529
  # echo 'abc' | string_upper
1327
1530
  # @SEE_ALSO
1328
1531
  # string_upper_first, string_lower
1329
- function string_upper() {
1532
+ string_upper() {
1330
1533
  local string=${1-$(cat)}
1331
- echo "${string^^}"
1534
+ if [[ -n "${BASH_VERSION:-}" ]] && ((BASH_VERSINFO[0] >= 4)); then
1535
+ # Bash's case expansion is locale-aware (unlike many tr versions).
1536
+ # Keep the Bash 4+/5+ echo behavior verbatim. Construct the expression
1537
+ # at runtime so Bash 3.2 can source this file without evaluating it.
1538
+ eval 'echo "${string'"^^"'}"'
1539
+ return
1540
+ fi
1541
+ printf '%s\n' "$string" | tr '[:lower:]' '[:upper:]'
1332
1542
  }
1333
1543
 
1334
1544
  # @NAME
@@ -1342,9 +1552,13 @@ function string_upper() {
1342
1552
  # echo 'aBc' | string_lower
1343
1553
  # @SEE_ALSO
1344
1554
  # string_upper, string_upper_first
1345
- function string_lower() {
1555
+ string_lower() {
1346
1556
  local string=${1-$(cat)}
1347
- echo "${string,,}"
1557
+ if [[ -n "${BASH_VERSION:-}" ]] && ((BASH_VERSINFO[0] >= 4)); then
1558
+ eval 'echo "${string'",,"'}"'
1559
+ return
1560
+ fi
1561
+ printf '%s\n' "$string" | tr '[:upper:]' '[:lower:]'
1348
1562
  }
1349
1563
 
1350
1564
  # @NAME
@@ -1358,10 +1572,29 @@ function string_lower() {
1358
1572
  # echo 'aBc' | string_upper_first
1359
1573
  # @SEE_ALSO
1360
1574
  # string_lower, string_upper
1361
- function string_upper_first() {
1575
+ string_upper_first() {
1362
1576
  local string=${1-$(cat)}
1363
- local lower="${string,,}"
1364
- echo "${lower^}"
1577
+ local lower first rest
1578
+
1579
+ if [[ -n "${BASH_VERSION:-}" ]] && ((BASH_VERSINFO[0] >= 4)); then
1580
+ # Do not round-trip through command substitution: it strips the
1581
+ # trailing newlines that the old Bash parameter expansion preserved.
1582
+ eval 'lower="${string'",,"'}"'
1583
+ eval 'echo "${lower'"^"'}"'
1584
+ return
1585
+ fi
1586
+
1587
+ lower="$(string_lower "$string")"
1588
+ [[ -n "$lower" ]] || {
1589
+ printf '\n'
1590
+ return 0
1591
+ }
1592
+
1593
+ # `${var^^}` / `${var,,}` / `${var^}` are Bash-specific. Parameter
1594
+ # prefix/suffix removal is shared by Bash and zsh.
1595
+ rest="${lower#?}"
1596
+ first="${lower%"$rest"}"
1597
+ printf '%s%s\n' "$(string_upper "$first")" "$rest"
1365
1598
  }
1366
1599
 
1367
1600
  # @NAME
@@ -1377,11 +1610,37 @@ function string_upper_first() {
1377
1610
  # string_sub 3 5 < temp_file.txt
1378
1611
  # echo ' as fd ' | string_sub 2 4
1379
1612
  # @SEE_ALSO
1380
- function string_sub() {
1613
+ string_sub() {
1381
1614
  local startIndex=$1
1382
1615
  local subStringLength=$2
1383
1616
  local string="${3-$(cat)}"
1384
- echo "${string:$startIndex:$subStringLength}"
1617
+ if [[ -n "${BASH_VERSION:-}" ]] && ((BASH_VERSINFO[0] >= 4)); then
1618
+ # Keep Bash 4+/5+ substring and error behavior exactly as before.
1619
+ # Bash 3.2 continues to use the portable negative-length fallback.
1620
+ echo "${string:$startIndex:$subStringLength}"
1621
+ return
1622
+ fi
1623
+ local totalLength=${#string}
1624
+ local actualStart=$startIndex
1625
+ local actualLength=$subStringLength
1626
+
1627
+ # Bash 3.2 cannot use a negative substring length. Convert the
1628
+ # documented negative start/end offsets to nonnegative bounds first.
1629
+ if ((actualStart < 0)); then
1630
+ actualStart=$((totalLength + actualStart))
1631
+ fi
1632
+ if ((actualLength < 0)); then
1633
+ actualLength=$((totalLength + actualLength - actualStart))
1634
+ fi
1635
+ # Out-of-range starts produce an empty result (as in modern Bash).
1636
+ if ((actualStart < 0 || actualStart > totalLength)); then
1637
+ printf '\n'
1638
+ return 0
1639
+ fi
1640
+ if ((actualLength < 0)); then
1641
+ return 1
1642
+ fi
1643
+ printf '%s\n' "${string:$actualStart:$actualLength}"
1385
1644
  }
1386
1645
 
1387
1646
  # @NAME
@@ -1395,7 +1654,7 @@ function string_sub() {
1395
1654
  # string_match 'name;+' "name;name;"
1396
1655
  # @SEE_ALSO
1397
1656
  # string_index_first
1398
- function string_match() {
1657
+ string_match() {
1399
1658
  local value regExp
1400
1659
  regExp=${1}
1401
1660
  string="${2-$(cat)}"
@@ -1413,10 +1672,14 @@ function string_match() {
1413
1672
  # escape_sed 'a$'
1414
1673
  # @SEE_ALSO
1415
1674
  # string_replace
1416
- function escape_sed() {
1417
- echo "${1}" | sed -e 's/\//\\\//g' -e 's/\&/\\\&/g' -e 's/\./\\\./g' -e 's/\^/\\\^/g' -e 's/\[/\\\[/g' -e 's/\$/\\\$/g'
1675
+ escape_sed() {
1676
+ if [[ -n "${ZSH_VERSION:-}" ]]; then
1677
+ printf '%s\n' "$1"
1678
+ else
1679
+ # Preserve historical Bash echo semantics.
1680
+ echo "$1"
1681
+ fi | sed -e 's/\//\\\//g' -e 's/\&/\\\&/g' -e 's/\./\\\./g' -e 's/\^/\\\^/g' -e 's/\[/\\\[/g' -e 's/\$/\\\$/g'
1418
1682
  }
1419
- export -f escape_sed >/dev/null
1420
1683
 
1421
1684
  # @NAME
1422
1685
  # string_replace -- replace literally the token string to new string, not support regular expression
@@ -1432,11 +1695,16 @@ export -f escape_sed >/dev/null
1432
1695
  # string_replace '\*' 'b' 'a*a' ==> 'aba'
1433
1696
  # @SEE_ALSO
1434
1697
  # escape_sed, string_replace_regex
1435
- function string_replace() {
1698
+ string_replace() {
1436
1699
  local tokenString newString
1437
1700
  tokenString=$(escape_sed "${1}")
1438
1701
  newString=$(escape_sed "${2}")
1439
- echo "${3-$(cat)}" | sed -e "s/${tokenString}/${newString}/g"
1702
+ if [[ -n "${ZSH_VERSION:-}" ]]; then
1703
+ printf '%s\n' "${3-$(cat)}" | sed -e "s/${tokenString}/${newString}/g"
1704
+ else
1705
+ # Preserve historical Bash echo semantics.
1706
+ echo "${3-$(cat)}" | sed -e "s/${tokenString}/${newString}/g"
1707
+ fi
1440
1708
  }
1441
1709
 
1442
1710
  # @NAME
@@ -1453,7 +1721,7 @@ function string_replace() {
1453
1721
  # string_replace_regex '*' 'b' 'a*a' ==> 'aba'
1454
1722
  # @SEE_ALSO
1455
1723
  # string_replace
1456
- function string_replace_regex() {
1724
+ string_replace_regex() {
1457
1725
  echo "${3-$(cat)}" | sed -E -e "s/$1/$2/g"
1458
1726
  }
1459
1727
 
@@ -1470,11 +1738,11 @@ function string_replace_regex() {
1470
1738
  # echo " add " | string_index_first "token"
1471
1739
  # @SEE_ALSO
1472
1740
  # string_before_first, string_after_first
1473
- function string_index_first() {
1741
+ string_index_first() {
1474
1742
  local tokenString=$1
1475
1743
  local string="${2-$(cat)}"
1476
1744
  local prefix="${string%%${tokenString}*}"
1477
- [ "${string}" == "${prefix}" ] && echo -1 || echo ${#prefix}
1745
+ [[ "${string}" == "${prefix}" ]] && printf '%s\n' -1 || printf '%s\n' "${#prefix}"
1478
1746
  }
1479
1747
 
1480
1748
  # @NAME
@@ -1490,7 +1758,7 @@ function string_index_first() {
1490
1758
  # echo " add " | string_before_first "dd"
1491
1759
  # @SEE_ALSO
1492
1760
  # string_index_first, string_after_first
1493
- function string_before_first() {
1761
+ string_before_first() {
1494
1762
  local tokenString=$1
1495
1763
  local string="${2-$(cat)}"
1496
1764
  echo "${string%%${tokenString}*}" # Remove the first - and everything following it
@@ -1509,7 +1777,7 @@ function string_before_first() {
1509
1777
  # echo " add " | string_after_first "dd"
1510
1778
  # @SEE_ALSO
1511
1779
  # string_index_first, string_before_first
1512
- function string_after_first() {
1780
+ string_after_first() {
1513
1781
  local tokenString=$1
1514
1782
  local string="${2-$(cat)}"
1515
1783
  echo "${string#*${tokenString}}" # Remove everything up to and including first -
@@ -1531,7 +1799,7 @@ function string_after_first() {
1531
1799
  # string_split_to_array $'\n' branchesToSelectArray "${branchesToSelectString}"
1532
1800
  # @SEE_ALSO
1533
1801
  # array_join, array_describe, array_from_describe, string_pick_to_array
1534
- function string_split_to_array() {
1802
+ string_split_to_array() {
1535
1803
  local tokenString="$1"
1536
1804
  local newArrayVarName="$2"
1537
1805
  local string="${3-$(cat)}"
@@ -1569,7 +1837,7 @@ function string_split_to_array() {
1569
1837
  # string_pick_to_array '{age:' '}' newArray "$str"
1570
1838
  # @SEE_ALSO
1571
1839
  # array_join, array_describe, array_from_describe, string_split_to_array
1572
- function string_pick_to_array() {
1840
+ string_pick_to_array() {
1573
1841
  local startTokenString="$1"
1574
1842
  local endTokenString="$2"
1575
1843
  local newArrayVarName="$3"
@@ -1605,7 +1873,7 @@ function string_pick_to_array() {
1605
1873
  # stop_if_failed "ERROR: can't delete the directory '${destProjectPath}' !"
1606
1874
  # @SEE_ALSO
1607
1875
  # confirm_to_continue
1608
- function stop_if_failed() {
1876
+ stop_if_failed() {
1609
1877
  if [[ $? -ne 0 ]]; then
1610
1878
  print_error "$*"
1611
1879
  exit 1
@@ -1624,19 +1892,25 @@ function stop_if_failed() {
1624
1892
  # confirm_to_continue a b
1625
1893
  # @SEE_ALSO
1626
1894
  # print_args, stop_if_failed
1627
- function confirm_to_continue() {
1895
+ confirm_to_continue() {
1628
1896
  local response
1629
1897
  print_args "$@"
1630
- if ! [ "${modeQuiet}" == true ]; then
1631
- read -r -p "Continue ? [y/N] " response
1632
1898
 
1633
- case "${response}" in
1634
- [yY][eE][sS] | [yY])
1635
- echo -e "Continue..."
1636
- sleep 1s
1899
+ if [[ "${modeQuiet:-false}" != true ]]; then
1900
+ # Bash `read -p` and zsh `read "name?prompt"` are shell-specific.
1901
+ # Match Bash read -p behavior by showing the prompt only for a TTY.
1902
+ if [[ -t 0 ]]; then
1903
+ printf '%s' 'Continue ? [y/N] ' >&2
1904
+ fi
1905
+ read -r response || response=""
1906
+
1907
+ case "$response" in
1908
+ [yY] | [yY][eE][sS])
1909
+ printf '%s\n' 'Continue...'
1910
+ sleep 1
1637
1911
  ;;
1638
1912
  *)
1639
- echo -e "Exiting..."
1913
+ printf '%s\n' 'Exiting...'
1640
1914
  exit 1
1641
1915
  ;;
1642
1916
  esac
@@ -1655,7 +1929,7 @@ function confirm_to_continue() {
1655
1929
  # wait_for 'test -f /tmp/output.txt' 'file existed' 3
1656
1930
  # @SEE_ALSO
1657
1931
  # confirm_to_continue, stop_if_failed
1658
- function wait_for() {
1932
+ wait_for() {
1659
1933
  local predicate="${1}"
1660
1934
  local subject="${2-${predicate}}"
1661
1935
  local interval=${3-3}
@@ -1681,6 +1955,16 @@ function wait_for() {
1681
1955
  # record2
1682
1956
  # EOF
1683
1957
  # @SEE_ALSO
1684
- function declare_heredoc() {
1685
- eval "$1='$(cat)'"
1958
+ declare_heredoc() {
1959
+ local _bash_base_heredoc_target="${1-}"
1960
+ local _bash_base_heredoc_content
1961
+
1962
+ # Only identifier names may appear as the left-hand side of an eval.
1963
+ case "$_bash_base_heredoc_target" in
1964
+ '' | [!a-zA-Z_]* | *[!a-zA-Z0-9_]*) return 2 ;;
1965
+ esac
1966
+
1967
+ _bash_base_heredoc_content="$(cat)"
1968
+ # Expand the content as a variable value, never parse it as shell code.
1969
+ eval "${_bash_base_heredoc_target}=\"\$_bash_base_heredoc_content\""
1686
1970
  }