Shells, features and interaction
Rob Pike
rob at alice.UucP
Mon Nov 18 05:44:22 AEST 1985
The recent shell discussion prompted by Arnold Robbins's announcement has
missed an important point: effective programmability in an interactive
system can obviate many supposed `features'.
Those who claim shell functions are the way to do aliasing are right, but
the only implementation that works properly for this purpose is in the
8th edition shell. The problem is that unless functions can be exported,
they are unavailable to subshells and shell files. In the 8th edition shell,
functions are literally exported, and passed in the environment as
shell input text. This sounds expensive, but on most current systems,
argument and environment passing through exec(2) is so efficient that a few
Kbytes of extra environment are probably cheaper than adding a directory
to PATH. Shells like csh or ksh that read function (or equivalent) definitions
from a file at start up have peculiar semantics, can't provide an export
facility that works properly, and are probably no more efficient.
Functions can be used for much more than providing aliases. For the
novitiate:
f(){
cmd
}
is the shell syntax for functions. 'cmd' is any text, however many lines
you'd like, exactly as it would appear in a shell file. The only difference
is that you 'return' from a function instead of 'exit.' After f is defined,
it is invoked as any command, and 'cmd' may refer to the function arguments
as $1, $2, $#, etc. So aliases are obviously trivial. Now some v8isms:
builtin cmd ...
runs cmd directly, but looks cmd up in the list of builtin functions first.
This allows functions to be evaluated first and override builtin functions:
cd(){
echo my cd
builtin cd $1
}
Function names may be anything printable, but may not contain = or ( to
simplify parsing. Thus:
ls-l(){
ls -l $*
}
and so on.
Because functions are evaluated locally, you can use them to do things
impossible in shell files. For example, if the shell writes its input
text to a file with a known name, say $HISTORY, you can invest an arbitrary
amount of effort in the history manipulating program, but have it merely
print the resulting command. A function can then collect the output
and eval it, so history works for any command even though the code for
supporting it is outside the shell.
People want to build things in to the shell for efficiency,
but every builtin is one less thing you can change (although functions clearly
mitigate this somewhat). test is often built in to avoid the costly fork and
exec. But most tests are doing string comparisons, and in fact the shell
semantics for string comparison is rather more powerful but has a peculiar
syntax. What people say is:
if test "$1" = "$2"
then cmd
fi
which is easy to read, but slow. So they build in test. More efficient
is the case statement:
case "$1" in
"$2") cmd
esac
But it's weird. Functions to the rescue:
equal(){
case "$1" in
"$2") return 0 ;;
*) return 1
esac
}
if equal "$1" "$2"
then ....
Obviously what's needed is a function library, as in any programming language.
Similar thought applied to things like expr(1) can show why expr needn't be
built in - when expr is in the inner loop it's usually doing something simple
like incrementing a variable. Why not provide a looping primitive, and rewrite
the loop as something like:
for i in `loop 1 100`
do foo $i
done
?
The next time someone hands you a shell with nice interactive features
or builtin commands, ask whether the implementer understands where the
power of the shell should lie. The Bourne shell is the favorite for
shell commands, the csh for interaction. But by addressing interaction
as a programmability problem, the interactive and programming shells
can be the same without sacrificing grace in either environment.
One final note:
text data bss dec hex
27648 1024 2568 31240 7a08 /bin/sh 8th edition shell
67584 2048 5740 75372 1266c /bin/csh C shell
81920 3072 9224 94216 17008 /usr/lbin/ksh Korn shell
Rob Pike
More information about the Comp.unix
mailing list