First light: a shell that runs commands but can't run a script
- #history
- #parser
- #control-flow
Two weeks after the first commit, huck could do this:
# huck, 20 May
$ printf 'echo hello | tr a-z A-Z\n' | huck
HELLO
$ printf 'ls /nonexistent 2>/dev/null || echo fallback\n' | huck
fallback
$ printf 'echo "user=$USER" $(echo nested) $((2 + 3 * 4)) ${x:-default} ~/docs\n' | huck
user=john nested 14 default /home/john/docsPipes, redirection, ||, quoting, command substitution, arithmetic,
parameter defaults, tilde expansion. That's most of what people actually
type at a prompt.
It could not do this:
# huck, 20 May
$ printf 'for i in 1 2 3; do echo $i; done\n' | huck
huck: command not found: for
huck: command not found: do
huck: command not found: done
$ printf 'greet() { echo "hello, $1"; }\n' | huck
huck: command not found: greet()
huck: command not found: }Not "syntax error" — command not found. To that shell, for was just a
word, and a word in command position is a program to look up on $PATH. The
loop wasn't broken, it didn't exist; the parser had no concept of a compound
command at all. Same for functions: greet() was a program name, and } was
another one.
What the order says
The order things got built wasn't an accident. A shell is really two systems
wearing one coat: an expansion engine that turns text into arguments, and a
language with control flow. huck built the expansion engine first, because
that's the part you can't fake and the part every other feature leans on —
word splitting, quoting rules, $IFS, the sequence expansions happen in. Get
that wrong and every feature built on top inherits the mistake.
The language came second, one construct per iteration, each with its own
design doc, plan and test suite: if, while, for, case, functions,
heredocs. Eight days later:
# huck, 28 May
$ printf 'for i in 1 2 3; do echo $i; done\n' | huck
1
2
3
$ printf 'greet() { echo "hello, $1"; }\ngreet world\n' | huck
hello, world
$ printf 'case abc in a*) echo matched;; esac\n' | huck
matched
$ printf 'cat <<EOF\nheredoc line\nEOF\n' | huck
heredoc lineThe -c you may have noticed
Every example above is piped into huck rather than passed with -c. That's
not stylistic — huck -c 'echo hi' on the 20 May build exits silently with
status 0, and on the 28 May build it hangs waiting on standard input. The
flag arrived later, with script-file mode.
There's something clarifying about a shell that can run a pipeline before it can run a script. The interactive core — the thing you use a hundred times a day — turns out to be the small half. The other half is everything that makes a shell a programming language, and that's where the next few hundred iterations went.