Three ways to say the same command
- #printer
- #declare
- #jobs
- #diagnostics
- #architecture
- #bugfix
A shell has to turn a parsed command back into text more often than you
would think: declare -f prints a function body, an error message names the
word that went wrong, jobs shows what each background job is, and
$BASH_COMMAND hands a trap the command that fired it. huck did this in three
unrelated places. The one behind error messages could not print a compound
command at all, so a $( ) containing an if was reported as nothing:
# before
$ huck -c 'echo hi > $(if true; then echo a; fi; echo b c)'
huck: line 1: $(; echo b c): ambiguous redirect# after
$ huck -c 'echo hi > $(if true; then echo a; fi; echo b c)'
huck: line 1: $(if true; then
echo a;
fi; echo b c): ambiguous redirectThat is bash's exact text, odd line breaks and all. bash rewrites the body of
a $( ) from its parsed form the moment it parses it, so what the message
shows is the printer's output, not what you typed — >&2 becomes 1>&2, and
echo b c loses its extra spaces.
The one behind jobs gave up on anything that was not a simple command:
# before
$ huck -c 'if true; then sleep 1; fi & jobs'
[1]+ Running background job &# after
$ huck -c 'if true; then sleep 1; fi & jobs'
[1]+ Running if true; then
sleep 1;
fi &bash's printer is one function with one switch: are we inside a function
definition? Inside one, every ; ends the line and a { … } group prints
over several lines. Everywhere else — the text of a $( ), the jobs
column — a ; joins commands on one line and a group is { a; b; }, while
if, for and while keep their multi-line shape in both. huck's
declare -f printer only knew the first style. It now has the switch, and the
other two printers are gone; error messages, jobs and $BASH_COMMAND all
ask it. Reading bash's printer also turned up two things declare -f had
been getting wrong: a two-command header (if a; b; then) is indented to
match its if, and a coprocess always prints its name, even the default one
(coproc COPROC cat).
The last piece was an operator that vanished at parse time. huck rewrote
&> file into > file 2>&1 before anything else saw it, so a function
containing one came back changed:
# before
$ huck -c 'f() { true &>log; }; declare -f f'
f ()
{
true > log 2>&1
}# after
$ huck -c 'f() { true &>log; }; declare -f f'
f ()
{
true &> log
}It still runs as > log 2>&1 — that rewrite now happens at the one place
redirections are applied, not where they are parsed — so noclobber, a later
2>/dev/null, exec &>f and every other shape behave exactly as before.
Still open: a $( ) inside a malformed ${…} is quoted verbatim in the
bad-substitution message rather than normalised (that text is unparsed by
design), and a literal newline between commands inside $( ) cannot be
preserved, because the parsed form does not record it. The set -x trace of a
quoted [[ == ]] pattern turned out to be the pattern matcher's escape form
leaking out, not the printer's, and moved to the next hub
(#589).