From a2d9103f8c2bb715e733ed8281a94b62e3e1dd2c Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 7 Sep 2026 14:02:38 +0200 Subject: [PATCH 1/2] fix(filter): preserve commas in exclusions `--exclude-filter` is repeatable, so each argument is one filter. Keep CLI values in an array to avoid colliding with the comma-separated BASHUNIT_EXCLUDE_FILTER configuration. Closes #1340 --- CHANGELOG.md | 1 + docs/command-line.md | 7 +++++++ docs/configuration.md | 6 ++++-- src/helper/discovery.sh | 20 +++++++++++++------ src/main/test.sh | 12 ++++++----- .../bashunit_exclude_filter_test.sh | 15 ++++++++++++++ 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4a09cd..4b928862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - Coverage propagates execution counts across multiline array literals, quoted strings, heredocs and backslash continuations, including when Bash 3.x records an array assignment on its closing `)`. Parent statement hits no longer mark commands inside command or process substitutions as covered, including within quotes or arrays (#1338) +- `--exclude-filter` treats a comma in one flag value as part of the test function name. Repeat the flag to exclude several names; `BASHUNIT_EXCLUDE_FILTER` remains comma-separated (#1340) ## [0.50.1](https://github.com/TypedDevs/bashunit/compare/0.50.0...0.50.1) - 2026-08-22 diff --git a/docs/command-line.md b/docs/command-line.md index ba94620b..44a1e2e4 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -224,6 +224,13 @@ Matching is identical to `--filter`, the flag is repeatable (a test is skipped if it matches **any** value), and exclusion wins when a name matches both — the same precedence `--exclude-tag` has over `--tag`. +Each flag value is one filter, so a comma in a function name remains literal. +Repeat the flag to exclude several names: + +```bash +bashunit test tests/ --exclude-filter 'test_a,{b}' --exclude-filter test_c +``` + Excluded tests are **not** reported as skipped: they are never selected, so they do not appear in the header count either. diff --git a/docs/configuration.md b/docs/configuration.md index a7a51c02..d37b7b52 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -389,9 +389,11 @@ Similar as using `--order-by` option on the [command line](/command-line#order-b ## Exclude filter -> `BASHUNIT_EXCLUDE_FILTER=name` +> `BASHUNIT_EXCLUDE_FILTER=name[,name...]` -Skip tests whose name matches. Empty by default. It wins over a `--filter` match. +Skip tests whose name matches any comma-separated value. Empty by default. It +wins over a `--filter` match. Unlike the repeatable command-line option, this +setting cannot target a function name that contains a comma. Similar as using `--exclude-filter` option on the [command line](/command-line#exclude-filter). diff --git a/src/helper/discovery.sh b/src/helper/discovery.sh index 5bdc9881..59257669 100644 --- a/src/helper/discovery.sh +++ b/src/helper/discovery.sh @@ -86,9 +86,10 @@ EOF ## # Whether a function name matches any --exclude-filter value. # -# The value is read from BASHUNIT_EXCLUDE_FILTER rather than passed in, so the -# header count (which reaches get_functions_to_run from a subshell) and the -# runner cannot end up applying different selections. +# Configured values come from comma-separated BASHUNIT_EXCLUDE_FILTER. Repeated +# CLI values live in a dynamically scoped array so each argument stays literal. +# Both are inherited by the header-count subshell, keeping it aligned with the +# runner without threading another argument through every call site. # # Locals are `__bu_`-prefixed (bash-style.md, PR #672). The only caller is # get_functions_to_run, and this runs inside its `for fn in ...` loop, so plain @@ -101,13 +102,21 @@ function bashunit::helper::name_matches_exclude_filter() { local __bu_prefix=$1 local __bu_fn=$2 - if [ -z "${BASHUNIT_EXCLUDE_FILTER:-}" ]; then + if [ -z "${BASHUNIT_EXCLUDE_FILTER:-}" ] && + [ -z "${_BASHUNIT_CLI_EXCLUDE_FILTERS[*]:-}" ]; then return 1 fi local IFS=',' local __bu_excl - for __bu_excl in $BASHUNIT_EXCLUDE_FILTER; do + for __bu_excl in ${BASHUNIT_EXCLUDE_FILTER:-}; do + __bu_excl=${__bu_excl/test_/} + if [ -n "$__bu_excl" ]; then + case "$__bu_fn" in ${__bu_prefix}_*${__bu_excl}*) return 0 ;; esac + fi + done + + for __bu_excl in ${_BASHUNIT_CLI_EXCLUDE_FILTERS[@]+"${_BASHUNIT_CLI_EXCLUDE_FILTERS[@]}"}; do __bu_excl=${__bu_excl/test_/} if [ -n "$__bu_excl" ]; then case "$__bu_fn" in ${__bu_prefix}_*${__bu_excl}*) return 0 ;; esac @@ -409,4 +418,3 @@ _BASHUNIT_TAGS_MAP_SCRIPT="" _BASHUNIT_TAGS_MAP_FNS=() _BASHUNIT_TAGS_MAP_TAGS=() _BASHUNIT_TAGS_OUT="" - diff --git a/src/main/test.sh b/src/main/test.sh index 5ec0e94b..17d9d5fe 100644 --- a/src/main/test.sh +++ b/src/main/test.sh @@ -102,6 +102,8 @@ function bashunit::main::cmd_test() { local filter="" local tag_filter="" local exclude_tag_filter="" + local -a _BASHUNIT_CLI_EXCLUDE_FILTERS=() + local _bashunit_cli_exclude_filter_count=0 local IFS=$' \t\n' local -a raw_args=() local raw_args_count=0 @@ -147,11 +149,11 @@ function bashunit::main::cmd_test() { shift ;; --exclude-filter) - if [ -z "$BASHUNIT_EXCLUDE_FILTER" ]; then - BASHUNIT_EXCLUDE_FILTER="$2" - else - BASHUNIT_EXCLUDE_FILTER="$BASHUNIT_EXCLUDE_FILTER,$2" - fi + # One flag is one filter. Keep CLI values in an array so a comma in a + # function name stays literal; BASHUNIT_EXCLUDE_FILTER keeps its existing + # comma-separated configuration format (#1340). + _BASHUNIT_CLI_EXCLUDE_FILTERS[_bashunit_cli_exclude_filter_count]="$2" + _bashunit_cli_exclude_filter_count=$((_bashunit_cli_exclude_filter_count + 1)) # export -n like every other flag (#839): find_total_tests reads this # from a plain subshell, which inherits it without exporting, and a real # export would leak into nested ./bashunit runs. diff --git a/tests/acceptance/bashunit_exclude_filter_test.sh b/tests/acceptance/bashunit_exclude_filter_test.sh index 901fad40..a13a239d 100644 --- a/tests/acceptance/bashunit_exclude_filter_test.sh +++ b/tests/acceptance/bashunit_exclude_filter_test.sh @@ -29,6 +29,21 @@ function test_repeated_exclude_filters_are_or_ed() { assert_same "$FIXTURE::test_user_list" "$output" } +function test_a_comma_in_one_exclude_filter_stays_literal() { + local fixture + fixture="$(bashunit::temp_file comma_filter).sh" + cat >"$fixture" <<'TEST' +function test_a() { assert_same 1 1; } +function test_a,{b}() { assert_same 1 1; } +TEST + + local output + output=$(./bashunit --list --filter test_a \ + --exclude-filter 'test_a,{b}' "$fixture" 2>/dev/null) + + assert_same "$fixture::test_a" "$output" +} + function test_exclude_filter_wins_when_a_name_matches_both() { local output output=$(./bashunit --list --filter admin --exclude-filter admin "$FIXTURE" 2>/dev/null) From fc59a3977e8b0b77bee75b550a993c22f41f39f6 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Mon, 7 Sep 2026 14:09:51 +0200 Subject: [PATCH 2/2] docs(filter): clarify comma limitation --- CHANGELOG.md | 2 +- docs/configuration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b928862..e0a5ad0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed - Coverage propagates execution counts across multiline array literals, quoted strings, heredocs and backslash continuations, including when Bash 3.x records an array assignment on its closing `)`. Parent statement hits no longer mark commands inside command or process substitutions as covered, including within quotes or arrays (#1338) -- `--exclude-filter` treats a comma in one flag value as part of the test function name. Repeat the flag to exclude several names; `BASHUNIT_EXCLUDE_FILTER` remains comma-separated (#1340) +- `--exclude-filter` treats a comma in one flag value as part of the test function name. Repeat the flag to exclude several names; the comma-separated `BASHUNIT_EXCLUDE_FILTER` cannot express a literal comma within one configured filter (#1340) ## [0.50.1](https://github.com/TypedDevs/bashunit/compare/0.50.0...0.50.1) - 2026-08-22 diff --git a/docs/configuration.md b/docs/configuration.md index d37b7b52..3bdfc52a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -393,7 +393,7 @@ Similar as using `--order-by` option on the [command line](/command-line#order-b Skip tests whose name matches any comma-separated value. Empty by default. It wins over a `--filter` match. Unlike the repeatable command-line option, this -setting cannot target a function name that contains a comma. +setting cannot express a literal comma within one configured filter. Similar as using `--exclude-filter` option on the [command line](/command-line#exclude-filter).