Our conventions are not inevitable
bash in an xterm is not fundamental
What’s the real interface to a computer? Behind all that graphical glitz is a command prompt, right?
This is an attitude that is ubiquitous among the programmers around me. Old timers who keyed startup sequences in via the front panel will disagree, but we are at a point where Unix is older than most of the programmers working today. For them, bash in an xterm is the “real” interface to the computer. Yet most of them have never seen a teletype, much less used one as a terminal. What accounts for the staying power of this interface?
Inertia counts for some part of it. I have put in many years typing
bash commands into an xterm and I can get a lot done with very little
thought. I also regularly and loudly swear when I use an editor or IDE
without configuring the custom keybindings from my .emacs
file. But there were third party command shells in teletype emulators
for classic MacOS, a fundamentally graphical system. Microsoft recently
added PowerShell, very much a bash-in-an-xterm interface to its (again,
fundamentally graphical) operating systems. There is something more than
inertia at work here.
I don’t think that “something more” is in the bash part of bash-in-an-xterm. The idea of having a separate scheduling language dates back to OS/360. Fred Brooks has since decided this was a mistake. You don’t want a separate, organically grown language for your scheduling. You want scheduling primitives in a well designed language.
bash
is not a well designed language. It is an organic
growth from the Bourne shell, which grew from the Korn shell, which grew
from sh
. In parallel you have the C shell and is spawn,
which were even less well
designed.
This is not necessarily the case. The Lisp machines used Lisp as their command language. Emacs still does. Smalltalk machines use Smalltalk. Even on less pure systems, there are languages like Rebol and Rexx which were designed for both interactive use and programming. The Scheme shell embeds the scheduling facilities of the Unix shell in a Scheme interpreter.
Why do I think this part is largely inertia? Microsoft designed
PowerShell to be familiar to users accustomed to organically grown
languages like bash
.
Yet the design of a language that needs to be used interactively differs from a language used to create a program text to be executed at once. Python’s rigid use of whitespace is a boon when working on a codebase, and a nuisance interactively. Lisp’s parentheses impose an overhead of typing characters far from the center of the keyboard, with the shift key held down, for every command, but that pales next to the overhead of defining a program in C or Java to be executed. Further, programming languages are designed to use libraries of functions. On systems like Unix where the functionality is exposed through lots of small programs instead of directly via functions, a simple way to run programs as well as functions is almost a necessity, and imposes a strange, awkward burden on language design.
The other aspect, and the one where I believe there is something important, is the xterm, the interface of executing one command after another, with a log of what you have done scrolling away behind.
This, too, is not inevitable. Emacs and Smalltalk let you execute the code at the cursor in any text frame, as does Wirth’s Oberon and the Acme and wily editors. In Oberon, acme, and wily, this is used to create all the menus and other parephrenalia of a window: a one line text frame above the editor begins with the default contents of (in Acme)
Newcol Kill Putall Dump Exit
all of which are commands. To add another command, type it in. If you want a custom menu of commands you run regularly, type them into a text buffer and click on the one you want when you want it.
However, these all dance around the edges. Commands in all these environments are used to perform actions that have immediate, visible, local consequences. Emacs commands typically affect the current buffer, or a buffer explicitly associated with it. Smalltalk commands run a calculation or pop up a window. In either environment, if there is something more to be done, a different tool is used (the code browser in Smalltalk, and normal programs represented as texts in Emacs). Acme commands do something immediate to the editor. Even in Oberon, where the command language is not used in a structured way like Smalltalk does in its code browser, but is used for changes to the system with no immediate, visible result, it is usually used in a facsimile of a teletype, with a series of commands written in an editor to be executed strictly in order.
The environments which have most divorced themselves from this are the notebook interfaces to computer algebra systems such as Maple and Mathematica. Both began their lives as command interpreters on teletypes. Maple will still print out ASCII versions of graphs if you ask it in just the right way. The modern interface, however, consists of blocks of content which can be evaluated or reevaluated in any order. Those blocks can define functions and variables that other blocks depend on. After a few such changes, it is nearly impossible to track the state of the system. The most used command in such notebook interfaces is “reevaluate the whole notebook”.
For similar reasons, the authors of PLT Racket deviated from the traditional approach in Lisp of executing code on top of whatever running state was already there. When you run a module in Racket, it completely resets your interpreter to a clean state, then loads the module.
Apparently that log trailing away behind you is vital to track the invisible state you are manipulating with a command language.
So does this mean that any attempt to escape the teletype emulator is
doomed to failure? Not necessarily, but we must change the language we
use to schedule executions on the system in certain ways. In particular,
all commands in the language must be idempotent and commute. That is, if
I have a command foo
, then running foo
twice
is the same as running foo
once; and if I have two commands
foo
and bar
, then running foo
followed by bar
must be the same as running
bar
followed by foo
. This implies a very
different form for commands. We don’t say “create the file
X
”, we say “ensure that X
exists”. Shell
programmers will recognize that this is what they use in most cases
anyway, and this is exactly what tools like cfengine, Puppet, and Chef
are designed to do.
But there is an obvious problem. The commands “ensure X
exists” and “ensure X
does not exist” do not commute. So
what are we to do?
The best idea I have so far is to make the scheduling language a predicate calculus. You have a set of assertions. When you “execute” a command, you are really telling the system to adjust itself so that it is true. Meanwhile other predicates in view may become false. So imagine I have
(ensure-file-exists "foobar.txt")
(not (ensure-file-exists "foobar.txt"))
I assert the first command, and it turns green. The second command turns red, indicating that it no longer holds. I assert the second command and it turns green, and the first one turns red. Like a spreadsheet, everything is reevaluated all the time. Of course, some assertions are going to be expensive to check. We’ll have to have a way to tell the system “only check if this is true or false when I tell you; grey it out the rest of the time.”
This is the only way that I have figured out that a command interface could truly liberate itself from the flow of a teletype. I have seen the command language of the future, and it is: Prolog.