The plus sign that moved your output
- #redirection
- #bash-compat
- #flaky-tests
This started as a flaky test. One harness in the suite failed a full run, passed on its own, and had been written off twice as "load flakiness" on a slow machine. It wasn't. Underneath were two separate bugs, and the one nobody was looking for is the kind you cannot see happening.
Writing to somewhere else entirely
In bash, >&WORD only means "duplicate a file descriptor" when the word is
made purely of digits. >&2 is a descriptor. >&report.txt is a file — it's
the same thing as &>report.txt, sending both stdout and stderr there.
So what is >&+7? Not digits — there's a +. bash makes a file called +7.
huck was deciding "is this a descriptor?" with a numeric parse that happily
accepts a leading sign. It read +7 as descriptor 7. If descriptor 7 happened
to be open, the output went there:
# before
$ exec 7>fd7.out; x=+7; echo PAYLOAD >&"$x"; echo "rc=$?"
rc=0
FILE fd7.out=[PAYLOAD]# after (and what bash does)
$ exec 7>fd7.out; x=+7; echo PAYLOAD >&"$x"; echo "rc=$?"
rc=0
FILE +7=[PAYLOAD]
FILE fd7.out=[]Note what is not different: the exit status is 0 both times, and neither run prints a warning. The only evidence you were given is which file your text ended up in. A script that logs somewhere unexpected, with no error and a successful return, is about the least fun thing to debug.
The empty descriptor
The second bug is what the flaky test was actually tripping over. When a descriptor word expands to nothing, bash calls it a bad descriptor. huck had two different wrong answers depending on which way the arrow pointed — one of them tried to open a file with an empty name, and the other used a message bash has never printed for a redirection:
# before
$ x=; echo hi >&"$x"; echo "rc=$?"
line 1: : No such file or directory
rc=1
$ x=; read r <&"$x"; echo "rc=$?"
line 1: bad fd: "$x"
rc=1# after (matching bash)
$ x=; echo hi >&"$x"; echo "rc=$?"
"$x": Bad file descriptor
rc=1
$ x=; read r <&"$x"; echo "rc=$?"
"$x": Bad file descriptor
rc=1An empty word isn't a contrived case. It's exactly what you get from
${MYP[0]} after a coprocess has finished and the shell has cleared the array
out from under you — which is precisely the state the flaky harness kept
landing in. Since huck's message differed from bash's, the comparison failed.
Related, a non-numeric word going the input direction has no filename meaning at all, so bash calls it ambiguous:
# before
$ x=abc; read r <&"$x"
line 1: bad fd: "$x"# after
$ x=abc; read r <&"$x"
abc: ambiguous redirectThe rule is fussier than it looks
Getting the error text right meant working out which thing bash names when it
complains, and the answer took several attempts. A first version of this fix
looked correct against a dozen hand-picked cases and still broke 4>&77,
because bash has three behaviours here, not one: a literal number names itself
whatever descriptor you aimed at (4>&77 says 77, and 4>&007 says 7),
quoting that same number changes the answer to 4, and a redirection on its
own default descriptor names the word you wrote rather than the number.
That is not something worth reasoning about from first principles. It's
something to measure, which is what the new harness does — 47 cases, run
through both shells, compared byte for byte, including which files ended up on
disk. That last part matters: the +7 bug produces identical text and
identical exit codes in both shells, so any harness comparing only output would
have called it a pass.
Still open
Fixing this changes the flaky harness's symptom without removing it. The
underlying race is #185: huck
tears a finished coprocess down sooner than bash does, so ${MYP[0]} goes
empty earlier. Measured on an idle machine, bash lost that race 0 times in 150
runs and huck lost it 5 — so it was never really about load, and it reproduces
standalone if you run it enough times. The descriptor cleanup itself is
unchanged here.