Skip to content

assert_matches passes a pattern that starts with a dash to grep as an option #1398

Closed

Description

@Chemaclass

Problem

assert_matches and assert_not_matches cannot take a pattern that starts with -. The pattern reaches grep as an option, and the outcome depends on the Bash version: a vacuous pass on Bash 5, an Error on Bash 3.2.

Reproduced on main 776997e9, macOS arm64:

function test_pattern_with_leading_dash_should_pass() {
  assert_matches "-[0-9]+" "-5"
}
function test_pattern_with_leading_dash_should_fail() {
  assert_matches "-x" "bar"
}
function test_negated_pattern_with_leading_dash_should_fail() {
  assert_not_matches "-5" "-5"
}

On Bash 5.3 all three tests pass: the two that should fail pass without matching anything. On Bash 3.2 all three are reported as Error with grep's usage text and [: : integer expression expected in the output, including the one that should pass.

Where

src/assert/core.sh:564, :566, :595, :596: "$GREP" -cE "$expected" with no -- or -e. grep rejects the pattern as an option and prints nothing, so $(... || true) is empty. [ "" -eq 0 ] and [ "" -gt 0 ] exit 2, which the if reads as false: the failure branch is skipped and the assertion counts as passed. The runtime error classifier catches the shell diagnostic on Bash 3.2 only.

assert_file_contains already handles this case with -e "$string" -- (#1108); the regex assertions were not covered.

Why it matters

Negative numbers, CLI flags and diff output all start with -. A test that asserts --help output matches -v, --verbose passes without checking anything on Bash 5.

Suggested change

Pass the pattern with -e "$expected" (or after --) in all four grep calls, and compare the count only when grep exited 0 or 1, so a grep usage error becomes an assertion failure instead of a pass.

Acceptance criteria

  • assert_matches "-[0-9]+" "-5" passes on Bash 3.2 and Bash 5.
  • assert_matches "-x" "bar" and assert_not_matches "-5" "-5" fail with the standard "to match" / "to not match" block, on both versions.
  • A test covers the three cases and fails on the current code.

Proposed fix

In assert_matches (src/assert/core.sh:550) and assert_not_matches (:579), pass the pattern as an operand with -e and fail closed when grep cannot run. Replace the four "$GREP" -cE "$expected" calls with a small helper that leaves the count in a slot and returns 1 on a grep usage error (exit code 2), so the callers never compare an empty string. Public behaviour that must not change: ERE semantics, the newline-collapsed retry, and the to match / to not match failure blocks.

_BASHUNIT_ASSERT_GREP_COUNT_OUT=0
# Count of lines of $2 matching ERE $1; returns 1 when grep refused the pattern.
function bashunit::assert::_grep_count_to_slot() {
  local count rc=0
  count=$(printf '%s' "$2" | "$GREP" -cE -e "$1") || rc=$?
  [ "$rc" -le 1 ] || return 1
  _BASHUNIT_ASSERT_GREP_COUNT_OUT=${count:-0}
}

In assert_matches: if the helper returns 1 for either form, fail with fail_with "" "${actual}" "to match" "${expected}" and return; otherwise keep the current logic on the slot. In assert_not_matches: a helper failure also fails the assertion (a pattern grep cannot read is not a proven non-match). assert_string_matches_format (:1357) already prefixes ^, so it is unaffected but may take -e for consistency. Bash 3.0: plain $(), [ ], no [[ =~ ]]. Docs: one sentence under assert_matches in docs/assertions.md that a pattern may start with -. CHANGELOG: one line under Unreleased, Fixed.

Agent brief

  • Branch: fix/1398-matches-leading-dash from fresh origin/main. Commit: fix(assert): accept a pattern that starts with a dash in assert_matches, body Closes #1398.
  • Files: src/assert/core.sh; tests in tests/unit/assert/string_test.sh next to the existing assert_matches tests (search function test_.*assert_matches), copying their assert_same "$(bashunit::console_results::print_failed_test ...)" "$(assert_matches ...)" shape.
  • RED first:
function test_successful_assert_matches_with_leading_dash_pattern() {
  assert_empty "$(assert_matches "-[0-9]+" "-5")"
}

function test_unsuccessful_assert_matches_with_leading_dash_pattern() {
  assert_same \
    "$(bashunit::console_results::print_failed_test \
      "Unsuccessful assert matches with leading dash pattern" "bar" "to match" "-x")" \
    "$(assert_matches "-x" "bar")"
}

function test_unsuccessful_assert_not_matches_with_leading_dash_pattern() {
  assert_same \
    "$(bashunit::console_results::print_failed_test \
      "Unsuccessful assert not matches with leading dash pattern" "-5" "to not match" "-5")" \
    "$(assert_not_matches "-5" "-5")"
}

Run only that file: ./bashunit tests/unit/assert/string_test.sh. Check both shells: /bin/bash ./bashunit tests/unit/assert/string_test.sh and the Homebrew Bash 5.

  • Non-goals: no change to assert_file_contains (already fixed in Bug in assert_file_contains and assert_equals #1108), to assert_contains, or to the format assertions' regex builder.
  • Gate, once, when the change is complete: ./bashunit tests/, ./bashunit --parallel --simple --strict tests/, make sa, make lint. make lint is the formatting authority; never shfmt -w; 2-space indent.
  • Done when: every acceptance box is checked, the gate is green, CHANGELOG and docs are updated as listed above, and a PR is open with the pr skill.

Activity

  1. added
    p1Data loss, a failure hidden behind exit 0, or a vacuous pass
    area:assertCode under src or scripts for assert
    agent-readyScoped, reproduced and testable; an agent can take it without more questions
    on Oct 28, 2026
  2. self-assigned this
    on Oct 29, 2026
  3. added a commit that references this issue on Oct 30, 2026
    ebeb277
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

agent-readyScoped, reproduced and testable; an agent can take it without more questionsarea:assertCode under src or scripts for assertbugSomething isn't workinggood first issueGood for newcomersp1Data loss, a failure hidden behind exit 0, or a vacuous pass

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions