A broken operand inside [[ ]] used to abandon the whole test, so an || that should have rescued it never ran. Plus numeric literals: huck now reads them the way bash does, wrapping instead of refusing.
A readonly variable could be shadowed inside a function, and could not have an attribute added — huck had both halves of the rule backwards. Plus four diagnostics that now read the way bash writes them.
huck's `local` and `declare` wrote onto the variable they were supposed to shadow, so a function-local could inherit an outer variable's type and even its contents. Plus two arithmetic fixes: quoted operands no longer quietly evaluate.
Four fixes to huck's arithmetic. Quoting a value no longer stops it being evaluated, an unusable value in an integer variable is reported instead of silently becoming zero, and failed expressions now say which part failed.
A script with `[ -f x ] && do_it` inside an if statement quietly exited early under set -e. huck was judging the same failure twice — once where it happened, and once again on the way out.
huck now colours the command line as you type it: strings, variables, globs and comments each get their own colour, brackets match under the cursor — and the only loud signal is a command that will not run.
huck's new syntax highlighting stopped at the first line of a multi-line command — and so, it turned out, did tab completion. Both for the same reason: a continuation line does not mean anything on its own.
When a script ends in the middle of something, the shell tells you which bracket it was still waiting for. huck was naming the wrong one in 78 different situations — and sometimes inventing a line number that didn't contain any bracket at all.
Seven fixes about a shell that spotted your mistake and then sent you somewhere else to find it. A syntax error blamed the line a block started on, an unterminated quote blamed the end of the file, and set -x invented quotes you never typed.
Eleven fixes about a shell that was too willing to answer. set -u stopped catching typos the moment you asked for a length, a nonsense expansion returned a plausible value, and a transform quietly applied to only the first argument.
A cascade of eighteen bug-fix rounds, starting from a redirection that reported an error and then quietly wrote your data into the file it was supposed to close.
Ten more fixes, led by a closed file descriptor that reopened itself because the shell saved another one on top of it — and a trap that fired twice depending on how many exclamation marks you typed.
A flaky test turned out to be two real bugs. The interesting one sent a command's output to a completely different place than bash would — same exit status, no error message, nothing to notice.
A round of small fixes to what builtins do with their arguments. Each one was a rule huck had invented, or a rule of bash's it hadn't noticed — and two of them hid because they were half-right.
mapfile -u now works, and so does its callback option. Getting the callback right meant three separate wrong guesses about how bash calls it — none of which the documentation settles.
After giving every builtin one option parser, the interesting cases were the ones that couldn't use it. pushd -Q was reporting itself as cd, complete was rejecting arguments bash accepts, and a typo could take the shell down.
readonly -pa didn't work, but export -pn did. Not because anyone decided that — because every builtin parsed its own options, and half of them were written by someone having a different day.
We went looking for copy-pasted code and found a bug you can type. Four builtins reject bundled short options that bash accepts — including one that rejects the exact spelling its own usage message documents.
`cmd || handler` protected a failing command — unless you wrapped it in parentheses, and then huck died anyway. The exemption was reaching the child process correctly and being thrown away on arrival.
A script that guarded a failing step with `|| handler` died instead of running the handler. huck had one switch controlling two different ideas about when a failure should be ignored — and it was missing from the case people write most.
A round of DEBUG-trap fixes where every issue mis-described its own bug — and each real cause turned out to be simpler than the report, and fixed by deleting a check rather than adding one.
huck had two dozen places each deciding, alone, whether an error should kill the shell. The results weren't inconsistent so much as uncorrelated with bash — wrong in both directions, sometimes in adjacent lines.
huck had five separate mechanisms for 'abandon this command', each with its own storage and its own idea of what outranks what. Unifying them changed no behaviour at all — which is the entire point, and the reason it was worth doing.
`trap 'exit 1' ERR` is how a careful script aborts on error. huck ran the handler and kept going — in all five kinds of trap. Fixing it also fixed two bugs nobody had reported, both hiding in code that had been copy-pasted.
A substitution that silently did nothing: ${x/#/prefix} — the idiomatic way to prepend to a value, or to every element of an array — returned the value untouched. Found by rebuilding a three-month-old huck to write a blog post.
A round of fixes to huck's RETURN and ERR traps: per-function cleanup handlers that silently did nothing, an ERR handler that quietly overwrote $?, and inherited traps that fired one time too many.
Three months of design docs, commits, pull requests and issues, read as evidence rather than memory: the process inverted itself, the same two bug shapes keep recurring, and 'done' turned out to need a definition.
A routine 'run the tests' turned up three defects with nothing wrong in the shell itself: assertions that only held on Linux, a suite racing itself for file descriptors, and a test quietly writing into the repo. Plus a build cache that reported failures for code it never compiled.
Fixing how `kill` handles a negative process ID turned up a neighbour, which turned up two more. A day later ten pull requests had merged — and the interesting part is the rule that decided when to stop.
huck tracked output twice — the real file descriptors, and a software 'sink' the interpreter passed around. Every leak, loss and mis-ordering for months lived where the two disagreed. Deleting one of them ended the whole bug class.
bash ships a test suite. Pointing it at huck gave a scoreboard that can't be argued with — 5 categories passing at first run, 39 a month later — and forced fixes nobody would have thought to look for, like the order an associative array iterates in.
Every external command huck ran cost a tenth of a second doing nothing at all. Finding it meant admitting that five 'flaky timeouts' in the test suite were one performance bug wearing a disguise.
Thirty iterations replaced huck's lexer and parser with a parser-driven mode stack. The whole point was that you can't tell from the outside — and why a rewrite nobody notices was worth a month.
Two weeks that added the features scripts reach for when they get clever — and split huck into crates so the interpreter could be used as a library, with no terminal attached.
Control flow made huck a language. It still couldn't declare an array, scope a variable, or set a shell option — the unglamorous surface that every real script depends on. Two weeks of filling it in.
Two weeks in, huck could pipe, redirect, expand variables and substitute commands — and had never heard of a for loop. What the first forty iterations built, in the order they built it.