The builtins that don't take options
- #builtins
- #bash-compat
Last week every builtin got a single shared option parser. The follow-ups were more interesting than the main event, because they were all about builtins that can't use it.
pushd was answering to the wrong name
# before
$ pushd -Q
huck: line 1: cd: -Q: invalid option
cd: usage: cd [-L|[-P [-e]] [-@]] [dir]You typed pushd. It complained as cd, and printed cd's usage. What
happened: pushd checks whether its argument is a directory-stack index like
+1, and if not, hands it to the code that changes directory — which is cd
wearing a different hat. A typo'd flag sailed straight through and got answered
by the wrong builtin.
popd had a different version of the same confusion: it checked whether the
directory stack was empty before looking at what you typed, so a bad flag got
reported as an empty stack.
# after
$ pushd -Q
huck: line 1: pushd: -Q: invalid number
pushd: usage: pushd [-n] [+N | -N | dir]
$ popd -Q
huck: line 1: popd: -Q: invalid number
popd: usage: popd [-n] [+N | -N]"Invalid number", not "invalid option" — because that's what bash says, and the
reason is the interesting part. pushd, popd and dirs take arguments like
+1 and -2 to rotate the stack. So -Q isn't a bad flag to them; it's a
malformed number.
That also means they can't use the shared parser at all, and the proof is a detail I'd never have guessed:
# after
$ dirs -cl
huck: line 1: dirs: -cl: invalid number
dirs: usage: dirs [-clpv] [+N] [-N]-c and -l are both real dirs options. Combine them and bash rejects the
whole thing — it reports -cl, not -c. These builtins don't combine flags at
all, because -1 has to stay a number. Any parser that helpfully bundled them
would be wrong.
complete was rejecting things bash accepts
# before
$ complete foo
huck: line 1: complete: nothing to completebash accepts that quite happily — it registers foo with an empty completion
spec. huck had invented a rule that you must specify something, which also
swallowed a couple of stranger forms bash accepts.
The neighbouring bug: huck treated +o as an option to complete. bash
doesn't. complete +o nospace foo registers three completions, named foo,
nospace and +o. Odd, but that's the behaviour, and scripts that do it were
getting an error instead.
Meanwhile compopt, which does take +o, was reporting a bad one with the
wrong sign and no usage line:
# before # after
$ compopt +z $ compopt +z
compopt: -z: invalid option compopt: +z: invalid option
compopt: usage: compopt [-o|+o option] [-DEI] [name ...]Four builtins hand-wrote that same error message, and the copies had drifted apart. Which is precisely the problem the shared parser was built to fix, reappearing one floor up in the code the parser doesn't cover.
And a crash
The option parser had a "this can't happen" branch at twenty-six places. It
could happen. A colon marks "this option takes a value", and was also being
accepted as an option itself, so hash -: reached the impossible branch and
took the whole shell down — exit code 101, everything after it in your script
skipped.
Fixed, but the general shape was the real bug: a shell should never abort on something you typed. All twenty-six now print an error and carry on.
The recurring lesson
Twice in these rounds a fix started by making a flag parse that huck doesn't
actually implement. Both times that's worse than rejecting it: the flag is
accepted, quietly ignored, and you get wrong results with no error. pushd -n
is the current example — bash inserts the directory below the top without
changing directory, huck doesn't implement that, and so it says so out loud
rather than pretending.