Repository navigation
Description
| Q | A |
|---|---|
| OS | Linux |
| Shell & version | bash 5.2.21(1)-release |
| bashunit version | 0.50.1 |
Summary
A multi-line array assignment inside a function that a test executes is reported as uncovered on
every line it spans. #722 fixed this for statements split with a backslash, but a statement that
spans lines because its parentheses are still open is not covered by that propagation, so an array
literal costs one uncovered line per element.
Current behavior
Bash's DEBUG trap reports the array assignment on the line of its closing ), not on the line
where the statement starts:
#!/usr/bin/env bash
set -T
trap 'printf "DEBUG trap fired with LINENO=%s\n" "$LINENO"' DEBUG
demo() { # line 5
local commands=( # line 6
"start" # line 7
"stop" # line 8
) # line 9
printf 'after array\n' # line 10
}
demo # line 13DEBUG trap fired with LINENO=13
DEBUG trap fired with LINENO=5
DEBUG trap fired with LINENO=9
DEBUG trap fired with LINENO=10
after array
The only hit for the assignment is on line 9, a )-only line, which is classified as non-executable
and so is discarded when hits are matched against executable lines. The result is that the hit
disappears completely: the opening line and every element line are reported as uncovered, even
though the statement ran.
A multi-line quoted string is affected by the same gap but degrades differently — the hit does land
on the first line, so the first line is reported as covered and only the interior lines are
reported as uncovered.
How to reproduce
src/example.sh:
#!/usr/bin/env bash
multi_line_array() {
local commands=(
"start"
"stop"
"status"
)
printf '%s\n' "${commands[@]}"
}
backslash_continuation() {
printf '%s\n' \
"start" \
"stop" \
"status"
}
multi_line_string() {
printf '%s' '{
"index": "spend",
"alias": "filters"
}'
}tests/example_test.sh:
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/../src/example.sh"
function test_multi_line_array() {
assert_contains "start" "$(multi_line_array)"
}
function test_backslash_continuation() {
assert_contains "start" "$(backslash_continuation)"
}
function test_multi_line_string() {
assert_contains "spend" "$(multi_line_string)"
}BASHUNIT_COVERAGE_SHOW_UNCOVERED=true BASHUNIT_COVERAGE_SHOW_LINE_HITS=true \
bashunit --coverage --coverage-paths src --no-coverage-report testsAll three tests pass, so all three functions ran, and the report is:
Coverage Report
---------------
src/example.sh 6/ 13 lines ( 46%)
---------------
Total: 6/13 (46%)
Uncovered Lines
---------------
src/example.sh:4-7,22-24
Line Hits
---------
src/example.sh: 10:1 14:1 15:1 16:1 17:1 21:1
- Lines 4-7, the whole
local commands=(...)statement, are reported uncovered. - Lines 14-17, the backslash-continued statement, are all covered, which is the Line continuations not counted as covered in report #722 propagation
working as intended. - Lines 22-24, the interior of the multi-line string, are reported uncovered while its first line
(21) is covered.
Expected behavior
A statement that ran should count as covered on every physical line it spans, however it happens to
span them — the same guarantee #722 established for a backslash chain. Alternatively the lines that
carry no statement of their own (array elements, the interior of a multi-line string) could be
treated as non-executable so they leave the denominator. Either way the array in the repro should
not contribute four uncovered lines after its function has run.
The scale of this in real code: in a repository of 13 shell scripts with 548 executable lines,
83 of the 188 lines reported as uncovered are array elements or interior lines of a multi-line
string, so roughly 15% of the denominator can never be covered by any test. Command allowlists such
as available_commands=("start" "stop" ...) written one entry per line make it worse the more
commands a script grows.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
Chemaclass commentedon Sep 7, 2026
Completed by #1339, with scanner correctness follow-ups in #1341 and #1342. Coverage now propagates hits across multiline statement spans, including Bash 3.x array attribution, without passing parent hits to commands inside command or process substitutions.