Skip to content

Commit 00cc4c9

Browse files
committed
fix(compat): use grep -c with $GREP for Bash 3.0 set -e pipeline bug
Bash 3.0 has a bug where piped `grep -q` always returns success under `set -e`. Replace all piped `grep -q` with `grep -c` based checks. Also save the real grep binary path as $GREP so framework internals work correctly when tests spy on grep.
1 parent b7a119f commit 00cc4c9

10 files changed

Lines changed: 51 additions & 47 deletions

File tree

src/assert.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ function bashunit::run_command_or_eval() {
8888
eval "${cmd#eval }" &>/dev/null
8989
;;
9090
*)
91-
if command -v "$cmd" | grep -qE '^alias'; then
91+
if [ "$(command -v "$cmd" | "$GREP" -cE '^alias' || true)" -gt 0 ]; then
9292
eval "$cmd" &>/dev/null
9393
else
9494
"$cmd" &>/dev/null
@@ -322,9 +322,9 @@ function assert_matches() {
322322
local actual
323323
actual=$(printf '%s\n' "${actual_arr[@]}")
324324

325-
if ! printf '%s' "$actual" | grep -qE "$expected"; then
325+
if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -eq 0 ]; then
326326
# Retry with newlines collapsed for cross-line patterns
327-
if ! printf '%s' "$actual" | tr '\n' ' ' | grep -qE "$expected"; then
327+
if [ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -eq 0 ]; then
328328
local test_fn
329329
test_fn="$(bashunit::helper::find_test_function_name)"
330330
local label
@@ -349,8 +349,8 @@ function assert_not_matches() {
349349
actual=$(printf '%s\n' "${actual_arr[@]}")
350350

351351
# Check both line-by-line and with newlines collapsed for cross-line patterns
352-
if printf '%s' "$actual" | grep -qE "$expected" ||
353-
printf '%s' "$actual" | tr '\n' ' ' | grep -qE "$expected"; then
352+
if [ "$(printf '%s' "$actual" | "$GREP" -cE "$expected" || true)" -gt 0 ] ||
353+
[ "$(printf '%s' "$actual" | tr '\n' ' ' | "$GREP" -cE "$expected" || true)" -gt 0 ]; then
354354
local test_fn
355355
test_fn="$(bashunit::helper::find_test_function_name)"
356356
local label
@@ -805,7 +805,7 @@ function assert_string_matches_format() {
805805
local regex
806806
regex="$(bashunit::format_to_regex "$format")"
807807

808-
if ! printf '%s' "$actual" | grep -qE "$regex"; then
808+
if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -eq 0 ]; then
809809
local test_fn
810810
test_fn="$(bashunit::helper::find_test_function_name)"
811811
local label
@@ -827,7 +827,7 @@ function assert_string_not_matches_format() {
827827
local regex
828828
regex="$(bashunit::format_to_regex "$format")"
829829

830-
if printf '%s' "$actual" | grep -qE "$regex"; then
830+
if [ "$(printf '%s' "$actual" | "$GREP" -cE "$regex" || true)" -gt 0 ]; then
831831
local test_fn
832832
test_fn="$(bashunit::helper::find_test_function_name)"
833833
local label

src/clock.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function bashunit::clock::_choose_impl() {
2222
if ! bashunit::check_os::is_macos && ! bashunit::check_os::is_alpine; then
2323
local result
2424
result=$(date +%s%N 2>/dev/null)
25-
if echo "$result" | grep -qv 'N' && echo "$result" | grep -qE '^[0-9]+$'; then
25+
if [ "$(echo "$result" | "$GREP" -cv 'N' || true)" -gt 0 ] \
26+
&& [ "$(echo "$result" | "$GREP" -cE '^[0-9]+$' || true)" -gt 0 ]; then
2627
_BASHUNIT_CLOCK_NOW_IMPL="date"
2728
return 0
2829
fi

src/coverage.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -438,22 +438,22 @@ function bashunit::coverage::is_executable_line() {
438438
[ -z "${line// /}" ] && return 1
439439

440440
# Skip comment-only lines (including shebang)
441-
echo "$line" | grep -qE '^[[:space:]]*#' && return 1
441+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*#' || true)" -gt 0 ] && return 1
442442

443443
# Skip function declaration lines (but not single-line functions with body)
444-
echo "$line" | grep -qE "$_BASHUNIT_COVERAGE_FUNC_PATTERN" && return 1
444+
[ "$(echo "$line" | "$GREP" -cE "$_BASHUNIT_COVERAGE_FUNC_PATTERN" || true)" -gt 0 ] && return 1
445445

446446
# Skip lines with only braces
447-
echo "$line" | grep -qE '^[[:space:]]*[\{\}][[:space:]]*$' && return 1
447+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*[\{\}][[:space:]]*$' || true)" -gt 0 ] && return 1
448448

449449
# Skip control flow keywords (then, else, fi, do, done, esac, in, ;;, ;&, ;;&)
450-
echo "$line" | grep -qE '^[[:space:]]*(then|else|fi|do|done|esac|in|;;|;;&|;&)[[:space:]]*(#.*)?$' && return 1
450+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*(then|else|fi|do|done|esac|in|;;|;;&|;&)[[:space:]]*(#.*)?$' || true)" -gt 0 ] && return 1
451451

452452
# Skip case patterns like "--option)" or "*)"
453-
echo "$line" | grep -qE '^[[:space:]]*[^\)]+\)[[:space:]]*$' && return 1
453+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*[^\)]+\)[[:space:]]*$' || true)" -gt 0 ] && return 1
454454

455455
# Skip standalone ) for arrays/subshells
456-
echo "$line" | grep -qE '^[[:space:]]*\)[[:space:]]*(#.*)?$' && return 1
456+
[ "$(echo "$line" | "$GREP" -cE '^[[:space:]]*\)[[:space:]]*(#.*)?$' || true)" -gt 0 ] && return 1
457457

458458
return 0
459459
}
@@ -515,7 +515,7 @@ function bashunit::coverage::get_line_hits() {
515515
fi
516516

517517
local count
518-
count=$(grep -c "^${file}:${lineno}$" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null) || count=0
518+
count=$("$GREP" -c "^${file}:${lineno}$" "$_BASHUNIT_COVERAGE_DATA_FILE" 2>/dev/null) || count=0
519519
echo "$count"
520520
}
521521

@@ -593,7 +593,7 @@ function bashunit::coverage::extract_functions() {
593593
brace_count=$((brace_count + ${#open_braces} - ${#close_braces}))
594594

595595
# Single-line function
596-
if [ "$brace_count" -eq 0 ] && echo "$line" | grep -q '\{' && echo "$line" | grep -q '\}'; then
596+
if [ "$brace_count" -eq 0 ] && [ "$(echo "$line" | "$GREP" -c '\{' || true)" -gt 0 ] && [ "$(echo "$line" | "$GREP" -c '\}' || true)" -gt 0 ]; then
597597
echo "${current_fn}:${fn_start}:${lineno}"
598598
in_function=0
599599
current_fn=""

src/env.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ TEMP_DIR_PARALLEL_TEST_SUITE="${TMPDIR:-/tmp}/bashunit/parallel/${_BASHUNIT_OS:-
272272
TEMP_FILE_PARALLEL_STOP_ON_FAILURE="$TEMP_DIR_PARALLEL_TEST_SUITE/.stop-on-failure"
273273
TERMINAL_WIDTH="$(bashunit::env::find_terminal_width)"
274274
CAT="$(command -v cat)"
275+
GREP="$(command -v grep)"
275276
MKTEMP="$(command -v mktemp)"
276277
FAILURES_OUTPUT_PATH=$("$MKTEMP")
277278
SKIPPED_OUTPUT_PATH=$("$MKTEMP")

src/helpers.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function bashunit::helper::find_test_function_name() {
2020
local _re='^test[A-Z]'
2121
local _is_test=false
2222
case "$fn" in test_*) _is_test=true ;; esac
23-
if [ "$_is_test" = true ] || echo "$fn" | grep -qE "$_re"; then
23+
if [ "$_is_test" = true ] || [ "$(echo "$fn" | "$GREP" -cE "$_re" || true)" -gt 0 ]; then
2424
echo "$fn"
2525
return
2626
fi
@@ -243,7 +243,7 @@ function bashunit::helper::find_files_recursive() {
243243
local _re='\[tT\]est\.sh$'
244244
local _pattern_match=false
245245
case "$pattern" in *test.sh) _pattern_match=true ;; esac
246-
if [ "$_pattern_match" = true ] || echo "$pattern" | grep -qE "$_re"; then
246+
if [ "$_pattern_match" = true ] || [ "$(echo "$pattern" | "$GREP" -cE "$_re" || true)" -gt 0 ]; then
247247
alt_pattern="${pattern%.sh}.bash"
248248
fi
249249

@@ -273,7 +273,7 @@ function bashunit::helper::normalize_variable_name() {
273273
normalized_string="${input_string//[^a-zA-Z0-9_]/_}"
274274

275275
local _re='^[a-zA-Z_]'
276-
if ! echo "$normalized_string" | grep -qE "$_re"; then
276+
if [ "$(echo "$normalized_string" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then
277277
normalized_string="_$normalized_string"
278278
fi
279279

@@ -471,7 +471,7 @@ function bashunit::helper::parse_file_path_filter() {
471471
*)
472472
# Check for :number syntax (line number filter)
473473
local _re='^(.+):([0-9]+)$'
474-
if echo "$input" | grep -qE "$_re"; then
474+
if [ "$(echo "$input" | "$GREP" -cE "$_re" || true)" -gt 0 ]; then
475475
file_path=$(echo "$input" | sed -nE 's/^(.+):([0-9]+)$/\1/p')
476476
local line_number
477477
line_number=$(echo "$input" | sed -nE 's/^(.+):([0-9]+)$/\2/p')
@@ -559,7 +559,7 @@ function bashunit::helper::get_tags_for_function() {
559559
local content
560560
content=$(sed -n "${check_line}p" "$script")
561561
local _re='^[[:space:]]*#[[:space:]]*@tag[[:space:]]'
562-
if echo "$content" | grep -qE "$_re"; then
562+
if [ "$(echo "$content" | "$GREP" -cE "$_re" || true)" -gt 0 ]; then
563563
local tag_name
564564
tag_name=$(echo "$content" | sed -nE 's/^[[:space:]]*#[[:space:]]*@tag[[:space:]]+//p')
565565
if [ -n "$tag_name" ]; then
@@ -569,10 +569,10 @@ function bashunit::helper::get_tags_for_function() {
569569
tags="$tags,$tag_name"
570570
fi
571571
fi
572-
elif echo "$content" | grep -qE '^[[:space:]]*#'; then
572+
elif [ "$(echo "$content" | "$GREP" -cE '^[[:space:]]*#' || true)" -gt 0 ]; then
573573
# Other comment line, keep walking
574574
:
575-
elif echo "$content" | grep -qE '^[[:space:]]*$'; then
575+
elif [ "$(echo "$content" | "$GREP" -cE '^[[:space:]]*$' || true)" -gt 0 ]; then
576576
# Empty line, stop looking
577577
break
578578
else

src/learn.sh

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function bashunit::learn::mark_completed() {
105105
##
106106
function bashunit::learn::is_completed() {
107107
local lesson=$1
108-
[ -f "$LEARN_PROGRESS_FILE" ] && grep -q "^$lesson$" "$LEARN_PROGRESS_FILE"
108+
[ -f "$LEARN_PROGRESS_FILE" ] && [ "$("$GREP" -c "^$lesson$" "$LEARN_PROGRESS_FILE" || true)" -gt 0 ]
109109
}
110110

111111
##
@@ -253,7 +253,7 @@ function test_bashunit_works() {
253253
fi
254254

255255
# Check if file contains assert_same
256-
if ! grep -q "assert_same" "$test_file"; then
256+
if [ "$("$GREP" -c "assert_same" "$test_file" || true)" -eq 0 ]; then
257257
echo "${_BASHUNIT_COLOR_FAILED}Your test should use assert_same${_BASHUNIT_COLOR_DEFAULT}"
258258
read -p "Press Enter to continue..." -r
259259
return 1
@@ -334,9 +334,9 @@ function test_multiple_assertions() {
334334
return 1
335335
fi
336336

337-
if ! grep -q "assert_contains" "$test_file" ||
338-
! grep -q "assert_matches" "$test_file" ||
339-
! grep -q "assert_not_empty" "$test_file"; then
337+
if [ "$("$GREP" -c "assert_contains" "$test_file" || true)" -eq 0 ] ||
338+
[ "$("$GREP" -c "assert_matches" "$test_file" || true)" -eq 0 ] ||
339+
[ "$("$GREP" -c "assert_not_empty" "$test_file" || true)" -eq 0 ]; then
340340
echo "${_BASHUNIT_COLOR_FAILED}Your test should use all three assertion types${_BASHUNIT_COLOR_DEFAULT}"
341341
read -p "Press Enter to continue..." -r
342342
return 1
@@ -427,8 +427,8 @@ function test_file_has_content() {
427427
return 1
428428
fi
429429

430-
if ! grep -q "function set_up()" "$test_file" ||
431-
! grep -q "function tear_down()" "$test_file"; then
430+
if [ "$("$GREP" -c "function set_up()" "$test_file" || true)" -eq 0 ] ||
431+
[ "$("$GREP" -c "function tear_down()" "$test_file" || true)" -eq 0 ]; then
432432
echo "${_BASHUNIT_COLOR_FAILED}Your test should define set_up and tear_down functions${_BASHUNIT_COLOR_DEFAULT}"
433433
read -p "Press Enter to continue..." -r
434434
return 1
@@ -521,7 +521,7 @@ function test_add_negative_numbers() {
521521
return 1
522522
fi
523523

524-
if ! grep -q "source" "$test_file"; then
524+
if [ "$("$GREP" -c "source" "$test_file" || true)" -eq 0 ]; then
525525
echo "${_BASHUNIT_COLOR_FAILED}Your test should source the calculator.sh file${_BASHUNIT_COLOR_DEFAULT}"
526526
read -p "Press Enter to continue..." -r
527527
return 1
@@ -710,7 +710,7 @@ function test_system_info_on_macos() {
710710
return 1
711711
fi
712712

713-
if ! grep -q "mock" "$test_file"; then
713+
if [ "$("$GREP" -c "mock" "$test_file" || true)" -eq 0 ]; then
714714
echo "${_BASHUNIT_COLOR_FAILED}Your test should use mock${_BASHUNIT_COLOR_DEFAULT}"
715715
read -p "Press Enter to continue..." -r
716716
return 1
@@ -827,7 +827,7 @@ function test_deploy_calls_docker_twice() {
827827
return 1
828828
fi
829829

830-
if ! grep -q "spy" "$test_file"; then
830+
if [ "$("$GREP" -c "spy" "$test_file" || true)" -eq 0 ]; then
831831
echo "${_BASHUNIT_COLOR_FAILED}Your test should use spy${_BASHUNIT_COLOR_DEFAULT}"
832832
read -p "Press Enter to continue..." -r
833833
return 1
@@ -860,7 +860,7 @@ File: validator.sh
860860
861861
function is_valid_email() {
862862
local email_pattern='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
863-
echo "$1" | grep -qE "$email_pattern"
863+
[ "$(echo "$1" | "$GREP" -cE "$email_pattern" || true)" -gt 0 ]
864864
}
865865
───────────────────────────────────────────────────────────────
866866
@@ -941,7 +941,7 @@ function test_invalid_emails() {
941941
return 1
942942
fi
943943

944-
if ! grep -q "function data_provider_" "$test_file"; then
944+
if [ "$("$GREP" -c "function data_provider_" "$test_file" || true)" -eq 0 ]; then
945945
echo "${_BASHUNIT_COLOR_FAILED}Your test should define data provider functions${_BASHUNIT_COLOR_DEFAULT}"
946946
read -p "Press Enter to continue..." -r
947947
return 1
@@ -1057,7 +1057,8 @@ function test_missing_file_returns_127() {
10571057
return 1
10581058
fi
10591059

1060-
if ! grep -q "assert_successful_code\|assert_exit_code\|assert_general_error" "$test_file"; then
1060+
local _exit_assert_pattern="assert_successful_code\|assert_exit_code\|assert_general_error"
1061+
if [ "$("$GREP" -c "$_exit_assert_pattern" "$test_file" || true)" -eq 0 ]; then
10611062
echo "${_BASHUNIT_COLOR_FAILED}Your test should use exit code assertions${_BASHUNIT_COLOR_DEFAULT}"
10621063
read -p "Press Enter to continue..." -r
10631064
return 1
@@ -1162,12 +1163,12 @@ function test_backup_failure_when_source_missing() {
11621163
local -a missing_components=()
11631164
local missing_components_count=0
11641165

1165-
if ! grep -q "function set_up()" "$test_file"; then
1166+
if [ "$("$GREP" -c "function set_up()" "$test_file" || true)" -eq 0 ]; then
11661167
missing_components[missing_components_count]="set_up function"
11671168
missing_components_count=$((missing_components_count + 1))
11681169
fi
11691170

1170-
if ! grep -q "function tear_down()" "$test_file"; then
1171+
if [ "$("$GREP" -c "function tear_down()" "$test_file" || true)" -eq 0 ]; then
11711172
missing_components[missing_components_count]="tear_down function"
11721173
missing_components_count=$((missing_components_count + 1))
11731174
fi

src/main.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -849,10 +849,10 @@ function bashunit::main::handle_assert_exit_code() {
849849
output=$(eval "$cmd" 2>&1 || echo "inner_exit_code:$?")
850850
local last_line
851851
last_line=$(echo "$output" | tail -n 1)
852-
if echo "$last_line" | grep -q 'inner_exit_code:[0-9]*'; then
852+
if [ "$(echo "$last_line" | "$GREP" -c 'inner_exit_code:[0-9]*' || true)" -gt 0 ]; then
853853
inner_exit_code=$(echo "$last_line" | grep -o 'inner_exit_code:[0-9]*' | cut -d':' -f2)
854854
local _re='^[0-9]+$'
855-
if ! echo "$inner_exit_code" | grep -qE "$_re"; then
855+
if [ "$(echo "$inner_exit_code" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then
856856
inner_exit_code=1
857857
fi
858858
output=$(echo "$output" | sed '$d')

src/runner.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ function bashunit::runner::parse_data_provider_args() {
263263
local has_metachar=false
264264
local _re1='[^\\][\|\&\;\*]'
265265
local _re2='^[\|\&\;\*]'
266-
if echo "$input" | grep -qE "$_re1" || echo "$input" | grep -qE "$_re2"; then
266+
if [ "$(echo "$input" | "$GREP" -cE "$_re1" || true)" -gt 0 ] \
267+
|| [ "$(echo "$input" | "$GREP" -cE "$_re2" || true)" -gt 0 ]; then
267268
has_metachar=true
268269
fi
269270

@@ -967,7 +968,7 @@ function bashunit::runner::parse_result_sync() {
967968

968969
# Extract values using sed instead of BASH_REMATCH for Bash 3.0+ compatibility
969970
# shellcheck disable=SC2001
970-
if echo "$result_line" | grep -qE 'ASSERTIONS_FAILED=[0-9]*##ASSERTIONS_PASSED=[0-9]*'; then
971+
if [ "$(echo "$result_line" | "$GREP" -cE 'ASSERTIONS_FAILED=[0-9]*##ASSERTIONS_PASSED=[0-9]*' || true)" -gt 0 ]; then
971972
assertions_failed=$(echo "$result_line" | sed 's/.*ASSERTIONS_FAILED=\([0-9]*\)##.*/\1/')
972973
assertions_passed=$(echo "$result_line" | sed 's/.*ASSERTIONS_PASSED=\([0-9]*\)##.*/\1/')
973974
assertions_skipped=$(echo "$result_line" | sed 's/.*ASSERTIONS_SKIPPED=\([0-9]*\)##.*/\1/')
@@ -1036,7 +1037,7 @@ function bashunit::runner::get_failure_source_context() {
10361037
while [ "$line_num" -le "$end_line" ]; do
10371038
line_text=$(sed -n "${line_num}p" "$file")
10381039
# Stop at the closing brace of the function
1039-
if echo "$line_text" | grep -qE '^[[:space:]]*\}[[:space:]]*$'; then
1040+
if [ "$(echo "$line_text" | "$GREP" -cE '^[[:space:]]*\}[[:space:]]*$' || true)" -gt 0 ]; then
10401041
break
10411042
fi
10421043
# Collect lines containing assert calls

src/state.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22

33
# Cache base64 -w flag support (Alpine needs -w 0, macOS does not support -w)
4-
if base64 --help 2>&1 | grep -q -- "-w"; then
4+
if [ "$(base64 --help 2>&1 | "$GREP" -c -- "-w" || true)" -gt 0 ]; then
55
_BASHUNIT_BASE64_WRAP_FLAG=true
66
else
77
_BASHUNIT_BASE64_WRAP_FLAG=false
@@ -346,8 +346,8 @@ function bashunit::state::print_tap_line() {
346346
while IFS= read -r detail_line; do
347347
detail_line=$(printf "%s" "$detail_line" | sed 's/\x1B\[[0-9;]*[mK]//g')
348348
if [ -n "$detail_line" ] \
349-
&& ! echo "$detail_line" | grep -qF "Failed:" \
350-
&& ! echo "$detail_line" | grep -qF "Error:"; then
349+
&& [ "$(echo "$detail_line" | "$GREP" -cF "Failed:" || true)" -eq 0 ] \
350+
&& [ "$(echo "$detail_line" | "$GREP" -cF "Error:" || true)" -eq 0 ]; then
351351
local trimmed="${detail_line#"${detail_line%%[![:space:]]*}"}"
352352
printf " %s\n" "$trimmed"
353353
fi

src/test_doubles.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function assert_have_been_called_with() {
101101
shift
102102

103103
local index=""
104-
if echo "${!#}" | grep -qE '^[0-9]+$'; then
104+
if [ "$(echo "${!#}" | "$GREP" -cE '^[0-9]+$' || true)" -gt 0 ]; then
105105
index=${!#}
106106
set -- "${@:1:$#-1}"
107107
fi

0 commit comments

Comments
 (0)