Subject: csh programming considered harmful
I am continually shocked and dismayed to see people write test cases,
install scripts, and other random hackery using the csh. Lack of
proficiency in the Bourne shell has been known to cause errors in /etc/rc
and .cronrc files, which is a problem, because you must write these files
in that language.
The csh is seductive because the conditionals are more C-like, so the path
of least resistance if chosen and a csh script is written. sadly, This is
a lost cause, and they don't realize it, even when they find that the very
simple things they wish to do are cumbersome or impossible in the csh.
Let me attempt to spell out some very good reasons why the csh is an
utterly inadequate tool for programming and its use for such purposes
should be strictly banned. I preface this from the tail of the csh man
page on Suns:
Although robust enough for general use, adventures
into the esoteric periphery of the C shell may reveal
unexpected quirks.
This is understatement at its finest, and should be in our man page, too.
So here's a document I wrote a while back elaborating on numerous very
sound reasons why not to use the csh for programming. I suspect that
after reading them you'll agree. I shall put this in swnotes for
safekeeping.
--tom
1. EXPRESSION EVALUATION
Consider this statement in the csh:
if ($?MANPAGER) setenv PAGER $MANPAGER
Despite your attempts to only set PAGER when you want
to, the csh aborts:
MANPAGER: Undefined variable.
That's because it parses the whole line anyway AND EVALUATES IT!
You have to write this:
if ($?MANPAGER) then
setenv PAGER $MANPAGER
endif
That's the same problem you have here:
if ($?X && $X == 'foo') echo ok
X: Undefined variable
This forced to write a couple nested if's. This is gross and stupid
because it renders short-ciruit booleans useless. If this were the
really C-like, you would expect to be able to safely write such becuase
of the common C construct:
if (p && p->member)
Undefined variables are not fatal errors in the Bourne shell, so
this issue does not arise.
3. ERROR HANDLING
Wouldn't it be nice to know you had an error in your script before
you ran it? That's what the -n flag is for: just check the syntax.
This is especially good to make sure seldom taken segments of code
code are correct. Alas, the csh implementation of this doesn't work.
Consider this statement:
exit (i)
Of course, they really meant
exit (1)
or just
exit 1
Either shell will complain about this. But if you hide this in an if
clause, like so:
#!/bin/csh -fn
if (1) then
exit (i)
endif
The csh tells you there's nothing wrong with this script. The equivalent
construct in the Bourne shell, on the other hand, tells you this:
#!/bin/sh -n
if (1) then
exit (i)
endif
/tmp/x: syntax error at line 3: `(' unexpected
3. FILE DESCRIPTORS
The most common problem encountered in csh programming is that
you can't do file-descriptor manipulation. All you are able to
do is redirect stdin, or stdout, or dup stderr into stdout.
Bourne-compatible shells offer you an abundance of more exotic
possibilities.
3a. Writing Files
In the Bourne shell, you can open or dup random file descriptors.
For example,
exec 2>errs.out
means that from then on, all of stderr goes into errs file.
Or what if you just want to throw away stderr and leave stdout
alone? Pretty simple operation, eh?
cmd 2>/dev/null
Works in the Bourne shell. In the csh, you can only make a pitiful
attempt like this:
(cmd > /dev/tty) >& /dev/null
But who said that stdout was my tty? So it's wrong. This simple
operation CANNOT BE DONE in the csh.
3b. Reading Files
In the csh, all you've got is $&-, which isn't the same as redirecting it
to /dev/null.
3d. More Elaborate Combinations
Maybe you want to pipe stderr to a command and leave stdout alone.
Not too hard an idea, right? You can't do this in the csh as I
mentioned in 1a. In a Bourne shell, you can do things like this:
exec 3>&1; grep yyy xxx 2>&1 1>&3 3>&- | sed s/file/foobar/ 1>&2 3>&-
grep: xxx: No such foobar or directory
Normal output would be unaffected. The closes there were in case
something really cared about all it's FDs. We send stderr to the sed,
and then put it back out 2.
Consider the pipeline:
A | B | C
You want to know the status of C, well, that's easy: it's in $?,
or $status in csh. But if you want it from A, you're out of luck --
if you're in the csh. In the Bourne shell, you can get it. Here's
something I had to do where I ran dd's stderr into a grep -v pipe to
get rid of the records in/out noise, but had to return the dd's exit
status, not the grep's:
device=/dev/rmt8
dd_noise='^[0-9]+\+[0-9]+ records (in|out)$'
exec 3>&1
status=`((dd if=$device ibs=64k 2>&1 1>&3 3>&- 4>&-; echo $? >&4) |
egrep -v "$dd_noise" 1>&2 3>&- 4>&-) 4>&1`
exit $status;
4. COMMAND ORTHOGONALITY
4a. Built-ins
The csh is a horrid botch with its built-ins. You can't put them
together in many reasonable way. Even simple little things like this:
% time | echo
which while nonsensical, shouldn't give me this message:
Reset tty pgrp from 9341 to 26678
Others are more fun:
% sleep 1 | while
while: Too few arguments.
[5] 9402
% jobs
[5] 9402 Done sleep |
Some can even hang your shell. Try typing ^Z while you're sourcing
something. Or redirecting a source command.
4b. Flow control
You can't mix flow-control and commands, like this:
who | while read line; do
echo "gotta $line"
done
You can't combine multiline things in a csh using semicolons.
There's no easy way to do this
alias cmd 'if (foo) then bar; else snark; endif'
4c. Stupid non-orthogonal parsing bugs
Certain reasonable things just don't work, like this:
kill -1 `cat foo`
`cat foo`: Ambiguous.
But this is ok:
/bin/kill -1 `cat foo`
There are many more of these.
5. SIGNALS
In the csh, all you can do with signals is trap SIGINT. In the Bourne
shell, you can trap any signal, or the end-of-program exit. For example,
to blow away a tempfile on any of a variety of signals:
trap 'rm -f /usr/adm/tmp/i$$ ;
echo "ERROR: abnormal exit";
exit' 1 2 3 15
trap 'rm tmp.$$' 0 # on program exit
6. QUOTING
You can't quote things reasonably in the csh:
set foo = "Bill asked, \"How's tricks?\""
doesn't work. This makes it really hard to construct strings with
mixed quotes in them. In the Bourne shell, this works just fine.
In fact, so does this:
cd /mnt; /usr/ucb/finger -m -s `ls \`u\``
Dollar signs cannot be escaped in doublequotes in the csh. Ug.
set foo = "this is a \$dollar quoted and this is $HOME not quoted"
dollar: Undefined variable.
You have to use backslashes for newlines, and it's just darn hard to
get them into strings sometimes.
set foo = "this \
and that";
echo $foo
this and that
echo "$foo"
Unmatched ". # say what???
echo $foo:q
You don't have these problems in the Bourne shell, where it's just
fine to write things like this:
echo 'This is
some text that contains
several newlines.'
7. VARIABLES
There's this big difference between global (environment) and local
(shell) variables. In csh, you use a totally different syntax
to set one from the other.
In Bourne shell, this
VAR=foo cmds args
is the same as
(export VAR; VAR=foo; cmd args)
or csh's
(setenv VAR; cmd args)
You can't use :t, :h, etc on envariables. Watch:
echo Try testing with $SHELL:t
It's really nice to be able to say
${PAGER-more}
or
FOO=${BAR:-${BAZ}}
to be able to run the user's PAGER if set, and more otherwise.
You can't do this in the csh. It takes more verbiage.
You can't get the process number of the last background
command from the csh. In the Bourne shell, it's $!.
7. RANDOM BUGS
Here's one:
fg %?string
^Z
kill %?string
No match.
Huh? Here's another
!%s%x%s
Coredump, or garbage.
If you have an alias with backquotes, and use that in backquotes in
another one, you get a coredump.
Try this:
% repeat 3 echo "/vmu*"
/vmu*
/vmunix
/vmunix
What???
There are a lot, probably over 100, of these.
/* End of text from pixel:swnotes */