next up previous index
Next: 10.3 JUNET 利用の手引き Up: 10 付録 Previous: 10.1.2 英語原文   Index


10.2 有害な csh プログラミング

Path: coconuts.jaist!wnoc-tyo-news!newsfeed.btnis.ad.jp!np0.iij.ad.jp!news.iij.ad.jp!rim.or.jp!tamaru-news!kuee-news!kuis-news!news.cs.ritsumei.ac.jp!odins-suita!chiba-ns!sakunami!Makino-Lab.cc.tohoku.ac.jp!not-for-mail
From: hiroki@aso.ecei.tohoku.ac.jp (Hiroki Mori)
Newsgroups: fj.archives.documents
Subject: Csh Programming Considered Harmful (in Japanese)
Supersedes: <5s83cm$rrg$1@dp-cc.cc.tohoku.ac.jp>
Followup-To: fj.unix
Date: 30 Sep 1997 01:50:16 +0900
Organization: Aso Lab., Dept of Comm., Tohoku Univ., Sendai, Japan
Lines: 574
Sender: hiroki@uma.aso.ecei.tohoku.ac.jp
Expires: 31 Dec 1997 23:30:00 +0900
Message-ID: <60om88$c50$1@dp-cc.cc.tohoku.ac.jp>
NNTP-Posting-Host: uma.aso.ecei.tohoku.ac.jp
X-Newsreader: Gnus v5.1
Xref: coconuts.jaist fj.archives.documents:1127

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<file1
    exec 4<file2

こうすると、ファイル記述子 3 から読むことによって file1 の行を得たり、
ファイル記述子 4 から file2 を得ることができるようになります。最近の 
Bourne 系のシェルでは、次のようにすれば良いです。

    read some_var 0<&3
    read another_var 0<&4

read が 0 からしか読めないような古いシェルでは、こうやってごまかします。

    exec 5<&0  # 古い stdin を取っておく
    exec 0<&3; read some_var
    exec 0<&4; read another_var
    exec 0<&5  # 元に戻す


1c. ファイル記述子を閉じる

Bourne シェルでは、2>&- のようにして、開いておきたくないファイル記述子
を閉じることができます。これは /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 <hamilton@bix.com> による商用の小さな非UNIXシステ
ム用のプログラムがあります。彼はそれを `csh' あるいは `hamilton csh' 
と呼んでいますが、それは本当の csh に対してバグの点でも仕様の点でも互
換ではないので csh ではありません。実際、彼は大きく修正をしたのですが、
そうしたことによって、まったく異なったシェルを作ってしまったのです。

(訳注1:ファイル記述子とは、プロセスがオペレーティングシステムを介して
入出力操作を行うとき、ファイル(またはパイプ)に付与される整数。特別なファ
イル記述子として、ユーザがログインすると自動的に作成される
  0  標準入力
  1  標準出力
  2  標準エラー出力
がある。詳細は、たとえば S.R. Bourne, "The UNIX System," Addison-Wesley
などを参照。)
-- 
            東北大学工学部通信工学科阿曽研究室    森  大毅
                          (hiroki@aso.ecei.tohoku.ac.jp)



Tetsuya Makimura 2000-10-06