Archive-name: csh-whynot-jp Version: $Id: csh-whynot.euc,v 1.3 1997/09/28 14:22:53 hiroki Exp $ この記事は、comp.unix.shell などに定期的に投稿されている「なぜ csh で プログラムを書くのが良くないのか」(原題 "Csh Programming Considered Harmful," 原著者 Tom Christiansen)という記事を日本語に翻訳したものです。 原文は、perl.com の /pub/perl/versus/csh-whynot.gz から anonymous FTP で入手できます。この日本語訳は、次のバージョンに基づいてなされました。 csh-faq,v 1.7 95/09/28 12:52:17 tchrist Exp Locker: tchrist 日本語訳は、http://www.aso.ecei.tohoku.ac.jp/~hiroki/csh-whynot.euc か ら入手することもできます。誤訳、不明瞭な点などのご指摘は、NetNews およ び email でお受けします。なお、翻訳にあたっては 田仲@ケイケン様 鈴木@OTSL様 日下部陽一様 からご助言をいただきました。ここに記して感謝します。 *** 有害な csh プログラミング *** 結論: csh はプログラミングにはまったく向かないツールであり、 そのような目的に使うことは厳しく禁じられるべきである! 何かのテストやインストールスクリプト、さまざまなハッキングなどに csh を使う人々を見て愕然とすることが私にはよくあります。Bourne シェルに十 分習熟していないと /etc/rc や .cronrc といったファイルでミスをするとい うことが知られています。これらのファイルは Bourne シェル言語で書かなけ ればならないので、問題なのです。 csh に魅力を感じるのは、条件文がより C ライクなせいです。それで、障害 の一番少ない道ということで csh でスクリプトを書くことになります。悲し いかな、これは見込み違いの選択で、しかもそのプログラマはめったにそのこ とに気がつきさえしません。それは、やらせたいと思う単純な作業の多くが csh ではやっかいだったり、時には不可能だったりするということがわかった としても同じなのです。 1. ファイル記述子 csh プログラミングにおいて直面する最もありがちな問題は、ファイル記述子 (訳注1)の操作ができないことです。できるのは、標準入力または標準出力を リダイレクトすること、または標準エラーを標準出力へ複製することだけです。 Bourne 互換シェルを使えば、もっと変化に富んだことが可能です。 1a. ファイル書き込み Bourne シェルでは、任意のファイル記述子をオープンまたは複製できます。 たとえば、 exec 2>errs.out というのは、これ以降標準エラーが errs.out ファイルに書き出されることを 意味します。 それとも、ただ単に標準エラーを捨てて標準出力だけをそのままにしたいとい うのならどうしましょうか? とても簡単な操作ですよね。 cmd 2>/dev/null これは Bourne シェルで動作します。csh では、次のような情けないやり方し かありません。 (cmd > /dev/tty) >& /dev/null しかし、標準出力が /dev/tty だと決めつけて良いのでしょうか。だから、こ れは間違いです。この単純な操作は、csh には*不可能*なのです。 同様に、csh スクリプトではエラーメッセージを適切に標準エラーに出力する ことができません。Bourne シェルなら、こんなふうに書けるでしょう。 echo "$0: $file が見つかりません" 1>&2 しかし、csh では標準出力を標準エラーにリダイレクトすることはできません。 だから、結局次のようなばかばかしいものを書くはめになるのです。 sh -c 'echo "$0: $file が見つかりません" 1>&2' 1b. ファイル読み込み csh においては、端末からの入力を得るには一行入力 $< を使うしかありませ ん。もしここで標準入力をリダイレクトしたとしたら? おあいにくさま、やっ ぱり端末からの入力になってしまいます。これは、この場合実際にはリダイレ クトはできないということです。さて、Bourne シェルのread 文を使うと、標 準入力から読みこみ、しかもリダイレクションも取りこむことができます。こ れは次のようなことができるということです。 exec 3&- のようにして、開いておきたくないファイル記述子 を閉じることができます。これは /dev/null へリダイレクトするのと同じで はありません。 1d. さらに込み入った組み合わせ 標準エラーをあるコマンドにパイプで流し、標準出力はそのままにしたいとい うことがあるでしょう。そんなに難しくないと思いますが、どうでしょうか。 1a で述べたように、これは csh ではできません。Bourne シェルでは、この ようにしてできます。 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 通常の出力は影響を受けないはずです。ファイル記述子を閉じる >&- がある のは、何かがそのファイル記述子すべてに構っているかもしれないからです。 ここでは標準エラーを sed に送り、それから 2 に戻しています。 次のパイプラインを考えてみましょう。 A | B | C C が返すステータス値を知りたいとします。そう、これは簡単で、$? か、あ るいは csh なら $status に入っています。でも、もしステータス値を A か ら得たいのなら、ついてませんね。まあ、csh を使っていれば、ですが。 Bourne シェルでならできるのですが、それをするのは少しトリッキーです。 次のものは、私が dd の records in/out ノイズを除くためにエラー出力を grep -v にパイプするが、戻り値として grep のではなく dd のステータスを 返さなければならなかったときのものです。 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; csh はまた、既知・未知を問わずすべてのファイル記述子を閉じてしまい、開 かれたファイル記述子を継承することを意図したアプリケーションには不向き であることでも知られています。 2. コマンドの直交性 2a. 組み込みコマンド csh は組み込みコマンドについてはひどいできそこないです。それらは合理的 な方法で組み合わせられないことが多いのです。次のような単純なものでさえ そうです。 % time | echo ばかばかしいのですが、こんなメッセージを出さないで欲しいものです。 Reset tty pgrp from 9341 to 26678 もっとおもしろいのもあります。 % sleep 1 | while while: Too few arguments. [5] 9402 % jobs [5] 9402 Done sleep | ものによっては、シェルをハングさせてしまうかもしれません。何かを source している間に ^Z をタイプしたり、source コマンドをリダイレクトし たりしてみましょう。ただ、手元に別のウィンドーを出しておくことを忘れず に。 % history | more とすると、何かが起こるシステムもあるので試してみましょう。 エイリアスは、いつもそれが評価されて欲しい所で評価されるわけではありま せん。 % 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. 2b. 流れ制御 次のように、流れ制御とコマンドを混在させることはできません。 who | while read line; do echo "$line を読んだ" done csh での複数行の構造をセミコロンを用いてまとめることはできません。次の ことを簡単に実現する方法はありません。 alias cmd 'if (foo) then bar; else snark; endif' ステータス値を得るだけのために if 文でリダイレクトをすることはできません。 if ( { grep vt100 /etc/termcap > /dev/null } ) echo ok また、パイプさえも使えません。 if ( { grep vt100 /etc/termcap | sed 's/$/###' } ) echo ok しかし、これらは Bourne シェルでならうまく動作します。 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 command1 の出力は command2 の入力へは行きません。両方のコマンドの出力 が標準出力に出てきます。エラーは何も起こりません。 Bourne シェルやその クローンでは、こうすることでしょう。 if command1 | command2 ; then ... fi 2c. 構文解析のばからしいバグ 合理的であるにもかかわらず動作しないものがあります。たとえば、次がそう です。 % kill -1 `cat foo` `cat foo`: Ambiguous. しかし、これだと大丈夫です。 % /bin/kill -1 `cat foo` もし次のようにして止めたジョブがあるなら、 [2] Stopped rlogin globhost 次のようにして kill できるはずです(訳注:が、実際にはできない)。 % kill %?glob kill: No match しかし、 % fg %?glob だと動きます。 スペースが問題となることがあります。 if(expr) は csh のバージョンによってはうまくいきません。ところが if (expr) は大丈夫! あなたのマシンのベンダはこのバグを直そうとしたかもしれませ んが、その csh でも次のものはやはり扱えない公算が大です。 if(0) then if(1) then echo A: ここを通過 else echo B: ここを通過 endif echo この文は実行されないはずなのだが・・・ endif 3. シグナル csh では、SIGINT をトラップすることができるだけです。 Bourne シェルで はどんなシグナルでも、あるいは end-of-program 終了でもトラップすること ができます。たとえば、いろいろなシグナルに応じて中間ファイルを消去する には、このようにします。 $ trap 'rm -f /usr/adm/tmp/i$$ ; echo "ERROR: abnormal exit"; exit' 1 2 3 15 $ trap 'rm tmp.$$' 0 # on program exit 4. クオート csh ではまともにクオートをすることができません。 set foo = "Bill asked, \"How's tricks?\"" これは動作しません。これでは、引用符が混在した文字列を構成するのはとて も難しいことです。Bourne シェルではまったく大丈夫。実は、こんなものも 大丈夫なのです。 cd /mnt; /usr/ucb/finger -m -s `ls \`u\`` csh では二重引用符の中でドル記号をエスケープすることができません。ふう。 set foo = "クオートした \$dollar と、していない $HOME" dollar: Undefined variable. 改行をクオートするためにはバックスラッシュを使う必要があり、文字列に含 めるのは本当に難しいことです。 set foo = "あれ \ これ"; echo $foo あれ これ echo "$foo" Unmatched ". 何だこりゃ? Bourne シェルではこんな問題はありません。Bourne シェルで は次のように書くこともまったく平気です。 echo 'これは 改行を何個か含んだ テキストです。' UUCP 形式のメールアドレスをクオートするのもやりがいがあります。次の例 を考えてみましょう。 % mail adec23!alberta!pixel.Convex.COM!tchrist alberta!pixel.Convex.COM!tchri: Event not found. 5. 変数の文法 大域変数(環境変数)と局所変数(シェル変数)との間には大きな違いがあります。 csh ではそれらをセットするのにまったく異なった文法を使います。 Bourne シェルでは、 VAR=foo cmds args は (export VAR; VAR=foo; cmd args) と、あるいは csh の (setenv VAR; cmd args) と同じです。 環境変数に対して :t, :h などを使うことはできません。次を見てください。 echo やってみよう。 $SHELL:t PAGER がセットされていればそれを使い、そうでなければ more を使うという ことを、 ${PAGER-more} あるいは FOO=${BAR:-${BAZ}} のように書けたら実に良いのですが、csh ではできません。もっと冗長になっ てしまいます。 最新のバックグラウンドコマンドのプロセス番号を csh から得ることはでき ません。何個ものジョブをバックグラウンドで起動させているときには、その ようにしたいことがあるかもしれません。Bourne シェルでは、最後にバック グラウンドで投入したコマンドの pid を $! で参照できます。 csh は環境変数を局所変数(シェル変数)にインポートするときの動作について も怪しいです。特に HOME, USER, PATH, TERM を扱う場合はそうです。次を考 えてみましょう。 % setenv TERM '`/bin/ls -l / > /dev/tty`' % csh -f どうなるかはお楽しみ。 6. 式の評価 csh における次の文を考えてみましょう。 if ($?MANPAGER) setenv PAGER $MANPAGER ただ単に PAGER をセットしたいだけなのに、csh はこんなふうに中断してし まいます。 MANPAGER: Undefined variable. これは、csh が常に行全体を解析し、また*評価する*からです。これはこのよ うに書かなければなりません。 if ($?MANPAGER) then setenv PAGER $MANPAGER endif 次も同様に問題です。 if ($?X && $X == 'foo') echo ok X: Undefined variable このため、入れ子の if 文を2,3行書くことを強いられます。これは、このよ うな状況における「短絡」論理式を使えなくするので非常に望ましくないこと です。もし csh がもっと C に近かったなら、この種の論理式を問題なく使え たはずです。次の一般的な C の構文を考えてみましょう。 if (p && p->member) Bourne シェルでは未定義変数は決定的なエラーとはならないため、この問題 は起きません。 csh は組み込みの数式処理機能を持ってはいるのですが、それはあなたが思う ようなものではありません。実は、スペース依存なのです。次は誤りです。 @ a = 4/2 しかし、これなら OK です。 @ a = 4 / 2 csh が用いるアドホックな構文解析は、他の所でも同様にじゃまをします。次 の例を考えてみましょう。 % alias foo 'echo こんにちは' ; foo foo: Command not found. % foo こんにちは 7. エラー処理 スクリプト中の誤りを、走らせる前に知ることができたら良いと思いませんか? -n フラグはそのためにあります。文法を見てみましょう。特にこれは、あまり ありそうもないコード片の正当性を確かめてくれる点でとても良いです。それ なのに、これは csh の実装では動作しません。次の文を考えてみましょう。 exit (i) もちろん、本当はこのようにしたかったのです。 exit (1) または、単に exit 1 どちらのシェルもこれには警告を出してくれます。しかし、次のように、この 部分が if 節の中に隠れていると、csh はこのスクリプトに誤りはないと告げ ます。一方、Bourne シェルにおける等価な構文はこのように教えてくれます。 #!/bin/sh -n if (1) then exit (i) endif /tmp/x: syntax error at line 3: `(' unexpected さまざまなバグ まず1つ。 fg %?string ^Z kill %?string No match. ありゃ。もう1つ。 !%s%x%s これはコアをダンプ、あるいはゴミを返します。 バッククオートを含むエイリアスがあり、それを別のエイリアス中のバックク オートの中で使うと、コアダンプします。 次のように入力してみましょう。 % repeat 3 echo "/vmu*" /vmu* /vmunix /vmunix 何だ? もう1つあります。 % mkdir tst % cd tst % touch '[foo]bar' % foreach var ( * ) > echo "$var というファイル" > end foreach: No match. 8. まとめ ベンダの中には csh のバグをいくつか直した(tcsh ではもっと良く直ってい る)ところもありますが、新たなバグを付加したところも多いのです。それら の問題のほとんどは決して解決されません。なぜならそれは、本来それらが実 際にバグだからではなく、「脳死」した設計を採用したことによる当然の帰結 だからです。そもそもが欠陥品なのです。 何かをしたくて、しかもシェルスクリプトを書く*必要*があるのなら、Bourne シェルで書きましょう。その辺のすべての UNIX システムに載っています。し かし、その動作はまちまちかもしれません。 他にも選択肢はあります。 Korn シェルは多くの sh 常用者に好まれているプログラミングシェルですが、 それはまだ構文解析や式評価がひどいといった Bourne シェルの設計に元々あ る問題に悩まされています。Korn シェル、あるいはそのパブリック・ドメイ ンなクローンおよびスーパーセット(たとえば bash)は sh ほどありふれては いないので、ネットに投稿するシェルアーカイブをそれらで書くのは賢明とは 言えないかもしれません。1003.2 が企業に対して強制力のある真の標準になっ たとき、状況ははるかに良くなることでしょう。そのときまで、我々はそこら へんにあるバグあり互換性なしバージョンの sh で悩まされることでしょう。 Plan 9 のシェルである rc の構文解析と式の評価ははるかにきれいなのです が、それはまだ広く使えるわけではないので、可搬性は大きく損なわれること でしょう。まだ rc を出荷しているベンダはありません。 もしシェルを使う必要はなくて、単にインタプリタ型言語が欲しいだけなら、 他の多くのフリーソフト、たとえば Perl, REXX, TCL, Scheme, Python のよ うなものを使える可能性が出てきます。とりわけ、Perl は UNIX において、 そして他の多くの処理系において、おそらく最も広く使うことができます。 Perlを標準システムと共に出荷するベンダも増えています。(comp.lang.perl FAQ のリストを見てください。) もし普通だったら sed や awk や sh を使うのだけれども、それらの能力を超 えているような、またはもう少し速く走らせたいような問題があり、そんなも のを C で書くのはばかばかしいというのだったら、Perl が役に立つことでしょ う。ネットワーク関数、バイナリデータ処理やほとんどの C ライブラリ関数 を使うことができます。また sed や awk で書いたスクリプトから Perl スク リプトへの変換プログラムもありますし、シンボリック・デバッガもあります。 tchrist(著者)の経験則は、Makefile の大きさにおさまるようなものは Bourne シェルで書き、それより大きいようなものは Perl で書くというもの です。 これらの言語についての詳細(FAQを含む)については comp.lang.{perl,rexx,tcl} を見るか、comp.lang.misc と news.answers に ある David Muir Sharnoff によるフリーで入手可能な言語とツールの比較を 見てください。 注意: Doug Hamilton による商用の小さな非UNIXシステ ム用のプログラムがあります。彼はそれを `csh' あるいは `hamilton csh' と呼んでいますが、それは本当の csh に対してバグの点でも仕様の点でも互 換ではないので csh ではありません。実際、彼は大きく修正をしたのですが、 そうしたことによって、まったく異なったシェルを作ってしまったのです。 (訳注1:ファイル記述子とは、プロセスがオペレーティングシステムを介して 入出力操作を行うとき、ファイル(またはパイプ)に付与される整数。特別なファ イル記述子として、ユーザがログインすると自動的に作成される 0 標準入力 1 標準出力 2 標準エラー出力 がある。詳細は、たとえば S.R. Bourne, "The UNIX System," Addison-Wesley などを参照。)