Commit 5abb59b
committed
new: Fix test failures for Bash < 4.2.32
The previous arguments to `@go.test_compgen` should have ended with a
trailing slash; as it stood, it was returning the directory itself, with
a trailing slash appended. After some experimentation, it emerged that
removing the directory prefix (with trailing slash) from the expected
results (e.g. `$TESTS_GO_SCRIPTS_DIR/lib/`) resulted in passing the
empty string as the sole argument to `assert_success` through Bash
4.2.31, and in passing no arguments at all to `assert_success` from Bash
4.2.32 on.
Again using the methodology described in the log message for commit
99ab780, I built different Bash
versions and performed a binary search using the following command until
I pinpointed 4.2.32 as the inflection point, replacing `BASH_VERSION` in
`PATH` at each iteration:
$ PATH=/usr/local/bash-builds/BASH_VERSION/bin:$PATH \
TEST_FILTER='tab complete --internal' gos test new
I then added the following line to `assert_success` from
`lib/bats/assertions`:
printf 'ARGC: %d\n' "$#" >&2
and added `return 1` after the `assert_success` call in the `tab
complete --internal` test case to force an error under 4.2.32, which
produced the following results (edited for clarity):
$ PATH=/usr/local/bash-builds/4.2.31/bin:$PATH \
TEST_FILTER='tab complete --internal' gos test new
✗ new: tab complete --internal
(in test file tests/new.bats, line 96)
`assert_success "${expected[@]#$TEST_GO_SCRIPTS_DIR/lib/}"' failed
ARGC: 1
ARGC: 1
output not equal to expected value:
expected: ''
actual: 'bar
baz
foo'
$ PATH=/usr/local/bash-builds/4.2.32/bin:$PATH \
TEST_FILTER='tab complete --internal' gos test new
✗ new: tab complete --internal
(in test file tests/new.bats, line 96)
`assert_success "${expected[@]#$TEST_GO_SCRIPTS_DIR/lib/}"' failed
ARGC: 1
ARGC: 0
Later I reproduced this behavior using a standalone script,
`expand-empty-list-test.bash`:
#! /usr/bin/env bash
print_argc() {
printf 'ARGC: %d\n' "$#"
for arg in "$@"; do
printf 'ARG: "%s"\n' "$arg"
done
}
ARGV=('foo')
print_argc "${ARGV[@]#foo}"
The last line can also be replaced with the following to achieve the
same effect:
set - "${ARGV[@]#foo}"
print_argc "$@"
Running either version of this script produces the output:
$ /usr/local/bash-builds/4.2.31/bin/bash expand-empty-list-test.bash
ARGC: 1
ARG: ""
$ /usr/local/bash-builds/4.2.32/bin/bash expand-empty-list-test.bash
ARGC: 0
Also, this behavior _only_ manifests when an array containing a single
value is expanded such that all of its characters are removed. Various
other values for `ARGV` (including the empty list) that result in more
than one value (even more than one empty string) do not exhibit this
behavior. Any form of expansion that removes all the characters from
every list item (including `##*`, `%%*`, and `//*/`) only exhibit this
behavior when `ARGV` contains a single string.
This is the patch and bug report for Bash 4.2.32:
https://mirrors.ocf.berkeley.edu/gnu/bash/bash-4.2-patches/bash42-032
http://lists.gnu.org/archive/html/bug-bash/2012-05/msg00010.html
Which appears to correspond to the following from
https://tiswww.case.edu/php/chet/bash/CHANGES:
This document details the changes between this version,
bash-4.3-alpha, and the previous version, bash-4.2-release.
ggg. Fixed a bug that causes the shell to delete DEL characters in the
expanded value of variables used in the same quoted string as
variables that expand to nothing.
This doesn't speak directly to the "single item array expansion to empty
string" behavior difference, but the patch contains the following change
to `expand_word_internal` from `subst.c`:
*** ../bash-20120427/subst.c 2012-04-22 16:19:10.000000000 -0400
--- subst.c 2012-05-07 16:06:35.000000000 -0400
***************
*** 8152,8155 ****
--- 8152,8163 ----
dispose_word_desc (tword);
+ /* Kill quoted nulls; we will add them back at the end of
+ expand_word_internal if nothing else in the string */
+ if (had_quoted_null && temp && QUOTED_NULL (temp))
+ {
+ FREE (temp);
+ temp = (char *)NULL;
+ }
+
goto add_string;
break;
`QUOTED_NULL` is defined in `subst.h` as:
/* Is the first character of STRING a quoted NULL character? */
#define QUOTED_NULL(string) ((string)[0] == CTLNUL && (string)[1] == '\0')
and `CTLNUL` is defined in `shell.h` as:
#define CTLNUL '\177'
Back to `expand_word_internal`, the `add_string:` label following the
`Kill quoted nulls` block is followed by:
add_string:
if (temp)
{
istring = sub_append_string (...);
temp = (char *)0;
}
break;
Finally, the end of the function contains the following comment and
block of code:
```
finished_with_string:
/* ... */
/* If we expand to nothing and there were no single or double quotes
in the word, we throw it away. Otherwise, we return a NULL word.
The single exception is for $@ surrounded by double quotes when
there are no positional parameters. In that case, we also throw
the word away. */
if (*istring == '\0')
{
if (quoted_dollar_at == 0 && ...)
{
/* ... */
}
/* According to sh, ksh, and Posix.2, if a word expands into nothing
and a double-quoted "$@" appears anywhere in it, then the entire
word is removed. */
else if (quoted_state == UNQUOTED || quoted_dollar_at)
list = (WORD_LIST *)NULL;
```
As best I can tell, the 4.2.32 patch that kills `QUOTED_NULL` values
prevented the assignment to `istring` (under the `add_string:` label)
that was still performed as of 4.2.31. Somehow expanding a single-item
array such that it results in the empty string produces a `QUOTED_NULL`,
hence the difference in behavior betweeen the two versions.
With this change to the test, the bugs in the tests themselves are
fixed, and the tests all pass under both versions. That said, this is a
rather pernicious kind of bug that seems difficult to detect and defend
against, given that it only manifests when single-item arrays are
expanded as a single empty string. The interface to `assert_success` and
`assert_failure` isn't wrong; it's intended to make it easy to check
command output without requiring two different assertions when a
straight equality check will do—especially when there should be no
output at all. I'll have to think on whether there's an effective
mechanism or idiom that can guard against it; but for now, my guard is
up, at least.1 parent 84d10bf commit 5abb59b
1 file changed
+3
-3
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | | - | |
| 115 | + | |
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| |||
0 commit comments