Originally found at http://www.faqs.org/faqs/unix-faq/shell/csh-whynot/.
From: Tom Christiansen <tchrist@mox.perl.com> Newsgroups: comp.unix.shell, comp.unix.questions, comp.unix.programmer, comp.infosystems.www.authoring.cgi Subject: Csh Programming Considered Harmful Date: 6 Oct 1996 14:03:18 GMT Message-ID: <538e76$8uq$1@csnews.cs.colorado.edu> Archive-name: unix-faq/shell/csh-whynot Version: $Id: csh-faq,v 1.7 95/09/28 12:52:17 tchrist Exp Locker: tchrist $
The following periodic article answers in excruciating detail the frequently asked question “Why shouldn't I program in csh?”. It is available for anon FTP from perl.com in /pub/perl/versus/csh.whynot.gz [This address is no longer valid. —Ed.]
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 BourneShell 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 is chosen and a csh script is written. Sadly, this is a lost cause, and the programmer seldom even realizes it, even when they find that many simple things they wish to do range from cumbersome to impossible in the csh.
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.
In the BourneShell, you can open or dup arbitrary file descriptors. For example,
exec 2>errs.out
means that from then on, 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
(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.
echo "$0: cannot find $file" 1>&2
sh -c 'echo "$0: cannot find $file" 1>&2'
exec 3<file1 exec 4<file2
read some_var 0<&3 read another_var 0<&4
exec 5<&0 # save old STDIN exec 0<&3; read some_var exec 0<&4; read another_var exec 0<&5 # restore it
In the BourneShell, you can close file descriptors you don't want open, like 2>&-, which isn't the same as redirecting it to /dev/null.
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 its FDs. We send STDERR to SED, and then put it back out 2.
A | B | C
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;
The csh has also been known to close all open file descriptors besides the ones it knows about, making it unsuitable for applications that intend to inherit open file descriptors.
% time | echo
Reset tty pgrp from 9341 to 26678
% 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. Just make sure you have another window handy. Or try
% history | more
on some systems.
% alias lu 'ls -u' % lu HISTORY News bin fortran lib lyrics misc tex Mail TEX dehnung hpview logs mbox netlib % repeat 3 lu lu: Command not found. lu: Command not found. lu: Command not found.
% time lu lu: Command not found.
who | while read line; do echo "gotta $line" done
You can't combine multiline constructs in a csh using semicolons. There's no easy way to do this
alias cmd 'if (foo) then bar; else snark; endif'
if ( { grep vt100 /etc/termcap > /dev/null } ) echo ok
if ( { grep vt100 /etc/termcap | sed 's/$/###' } ) echo ok
if grep vt100 /etc/termcap > /dev/null ; then echo ok; fi
if grep vt100 /etc/termcap | sed 's/$/###/' ; then echo ok; fi
if ( { command1 | command2 } ) then ... endif
The output of command1 won't go into the input of command2. You will get the output of both commands on standard output. No error is raised. In the BourneShell or its clones, you would say
if command1 | command2 ; then ... fi
% kill -1 `cat foo` `cat foo`: Ambiguous.
% /bin/kill -1 `cat foo`
[2] Stopped rlogin globhost
You should be able to kill it with
% kill %?glob kill: No match
but
% fg %?glob
works.
if(expr)
may fail on some versions of csh, while
if (expr)
works! Your vendor may have attempted to fix this bug, but odds are good that their csh still won't be able to handle
if(0) then !if(1) then echo A: got here else echo B: got here endif echo We should never execute this statement endif
$ trap 'rm -f /usr/adm/tmp/i$$ ; echo "ERROR: abnormal exit"; exit' 1 2 3 15
$ trap 'rm tmp.$$' 0 # on program exit
set foo = "Bill asked, \"How's tricks?\""
cd /mnt; /usr/ucb/finger -m -s `ls \`u\``
Dollar signs cannot be escaped in double quotes 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 ".
echo 'This is some text that contains several newlines.'
% mail adec23!alberta!pixel.Convex.COM!tchrist alberta!pixel.Convex.COM!tchri: Event not found.
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 the BourneShell, this
VAR=foo cmds args
is the same as
(export VAR; VAR=foo; cmd args)
or csh's
(setenv VAR; cmd args)
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, something you might like to do if you're starting up several jobs in the background. In the BourneShell, the pid of the last command put in the background is available in $!.
% setenv TERM '`/bin/ls -l / > /dev/tty`' % csh -f
And watch the fun!
if ($?MANPAGER) setenv PAGER $MANPAGER
MANPAGER: Undefined variable.
if ($?MANPAGER) then setenv PAGER $MANPAGER endif
if ($?X && $X == 'foo') echo ok X: Undefined variable
if (p && p->member)
Undefined variables are not fatal errors in the BourneShell, so this issue does not arise there.
While the csh does have built-in expression handling, it's not what you might think. In fact, it's space sensitive. This is an error
@ a = 4/2
but this is ok
@ a = 4 / 2
% alias foo 'echo hi' ; foo foo: Command not found. % foo hi
exit (i)
Of course, they really meant
exit (1)
or just
exit 1
#!/bin/csh -fn if (1) then exit (i) endif
#!/bin/sh -n if (1) then exit (i) endif
/tmp/x: syntax error at line 3: `(' unexpected
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.
% repeat 3 echo "/vmu*" /vmu* /vmunix /vmunix
What???
% mkdir tst % cd tst % touch 'foobar' % foreach var ( * ) > echo "File named $var" > end foreach: No match.
While some vendors have fixed some of the csh's bugs (the tcsh(1) also does much better here), many have added new ones. Most of its problems can never be solved because they're not actually bugs per se, but rather the direct consequences of braindead design decisions. It's inherently flawed.
Do yourself a favor, and if you have to write a shell script, do it in the BourneShell. It's on every UNIX system out there. However, behavior can vary.
There are other possibilities.
The Korn shell is the preferred programming shell by many sh addicts, but it still suffers from inherent problems in the BourneShell's design, such as parsing and evaluation horrors. The Korn shell or its public-domain clones and supersets (like bash) aren't quite so ubiquitous as sh, so it probably wouldn't be wise to write a sharchive in them that you post to the net. When 1003.2 becomes a real standard that companies are forced to adhere to, then we'll be in much better shape. Until then, we'll be stuck with bug-incompatible versions of the sh lying about.
The Plan 9 shell, rc, is much cleaner in its parsing and evaluation; it is not widely available, so you'd be significantly sacrificing portability. No vendor is shipping it yet.
If you don't have to use a shell, but just want an interpreted language, many other free possibilities present themselves, like Perl, REXX?, TCL, Scheme, or Python. Of these, Perl is probably the most widely available on UNIX (and many other) systems and certainly comes with the most extensive UNIX interface. Increasing numbers vendors ship Perl with their standard systems. (See the comp.lang.perl FAQ for a list.)
If you have a problem that would ordinarily use SED or AWK or sh, but it exceeds their capabilities or must run a little faster, and you don't want to write the silly thing in C, then Perl may be for you. You can get at networking functions, binary data, and most of the C library. There are also translators to turn your SED and AWK scripts into Perl scripts, as well as a symbolic debugger. Tchrist's personal rule of thumb is that if it's the size that fits in a Makefile, it gets written in the BourneShell, but anything bigger gets written in Perl.
See the comp.lang.{perl,rexx,tcl} newsgroups for details about these languages (including FAQs), or David Muir Sharnoff's comparison of freely available languages and tools in comp.lang.misc and news.answers.
NOTE: Doug Hamilton has a program that he sells for profit for little toy non-UNIX systems. He calls it 'csh' or the 'hamilton csh', but it's not a csh as it's neither bug nor feature compatible with the real csh. Actually, he's fixed a great deal, but in doing so, has created a totally different shell.
3 pages link to CshProgrammingConsideredHarmful: