シェルスクリプト

したいことをファイルあるいはスクリプトに単純に書くだけで そのスクリプトが他のOSのシェルコマンドのように実行できれば、 便利なことはままあります。重厚なプログラムへのインタフェースは スクリプトの形式で用意されることが多く、利用者は独自のスクリプトを 作ったり、既存のものを自分の必要にあわせてカスタマイズすることが よくあります。スクリプトプログラミングはおそらく間違いなく最もよく 行なわれるプログラミングでしょう。多くのユーザにとってはそれが 唯一のプログラミングでしょう。

Unix や DOS (Windows のコマンドラインインタフェース) のような オペレーティングシステムはこうしたスクリプトプログラミングの メカニズムを用意しています。しかし、どちらの場合にもスクリプト プログラミング言語は原始的なものです。多くのスクリプトは、 シェルプロンプトで打込むコマンドの並びあるいはバッチコマンドに すぎません。これのおかげでユーザが同じあるいは似たようなコマンドの 並びを実行する場合、毎回一々シェルのコマンドを全部打ち込むような ことをしなくてすみます。スクリプト言語には条件文やループといった 形式にちょっとしたプログラムの可能性を盛り込んでいるものもありますが それだけであったりします。もちろん、ちょっとしたことをするだけなら これで十分です。しかし、スクリプトも、それに対する要求もどんどん 多きくなりますし、スクリプトも常にそれができるように見え、より完全 なプログラミング言語が必要だと感じることがよくあります。 適切なOSインタフェースをもつ Scheme はスクリプトプログラミングを やさしくし、保守しやすいものにします。

この節では Scheme でのスクリプトプログラムの書かかたを解説します。 Scheme の方言はいろいろあり方法には多くのバリエーションがありますので、 ここでは、MzScheme で具体例をあげることにします。付録のAに 他の方言用の変更点について書いておきます。また、ここでは、UNIX を前提と します。付録Bには DOS での扱いについて書いておきます。

16.1  Hello, World! ふたたび

世界に向けてこんにちはを言うスクリプトを書きましょう。こんにちはを 言うというのはもちろん、伝統的なスクリプトプログラミングができなかった ことではありません。しかし、これを Scheme で記述することはもっと 野心的なスクリプトプログラミングに着手する手始めになります。まず、 Unix の hello スクリプトはファイルで以下のような中身になります。

echo Hello, World! 

これは、シェルのコマンド echo を使っています。 スクリプトには hello という名前をつけることができて、

chmod +x hello 

とすると、実行可能になり、環境変数 PATH にあるディレクトリに 置きます。そうすると、そのあと、だれでも

hello 

とシェルプロンプトから打ち込むとすぐにそっけない挨拶がでます。

Scheme の hello スクリプトは同じ出力を Scheme (1 節のプログラム)を使って出しますが、 オペレーティングシステムにファイルの中のコマンドをデフォルトの シェルスクリプトではなく、Scheme として解釈するように教えるための 何かが必要です。Scheme のスクリプトファイルも、hello という 名前にし、その中身は、以下のようになります。

":"; exec mzscheme -r $0 "$@" 
(begin 
  (display "Hello, World!") 
  (newline)) 

最初の行の後はそのまま Scheme です。しかし、最初の行は これをスクリプトにするための魔法です。ユーザが Unix のプロンプト から hello と入力すると、Unix はこのファイルを通常の スクリプトとして読みます。最初の ":" はシェルの no-op です。; はシェルのコマンドセパレータです。つぎにくるのは シェルコマンドの exec になります。exec は Unix に 現在のスクリプトを捨てて mzscheme -r $0 "$@" を代りに 実行するように指示します。ここで、パラメータ $0 はこの スクリプトの名前で置き換えられます。そして、パラメータ "$@" はユーザがこのスクリプトに与えた引数のリストで置き換えられます。 (この場合にはそのような引数はありません。)

結局、hello というシェルコマンドが別のシェルコマンド

mzscheme -r /whereveritis/hello 

に置き換わったということです。ここで /whereveritis/hellohello のパス名です。

mzscheme は MzScheme の実行ファイルを呼びます。-r オプションは 直後の引数を Scheme ファイルとして、その後につづく引数を argv というベクタに集めてから、ロードします。(ここの例では、 argv はナルベクタになります。)

したがって、この Scheme スクリプトは Scheme ファイルとして 走り、ファイル中の Scheme のフォームはこのスクリプトの元々の 引数にベクタ argv を通じてアクセスします。

こんどは、Scheme がこのスクリプトの最初の行にとりかからなければ なりません。この行は既にみたように、れっきとした 伝統的 シェルスクリプトです。":" は Scheme では自己評価的文字列です。 ので、害はありません。

;’ は Scheme のコメントマークですので、exec ... は無視されます。 ファイルの残りの部分はもちろん Scheme です。そこにある式は順に評価され ます。これらを全部評価したのち Scheme は終了します。

要するに、hello とシェルプロンプトから入力すると

Hello, World! 

が表示され、またシェルプロンプトに戻ります。

16.2  引数をもつスクリプト

Scheme スクリプトは変数 argv を使って、引数を参照します。 たとえば、以下のスクリプトはすべての引数を一行づつ表示します。

":"; exec mzscheme -r $0 "$@"

;argv-count に与えられた引数の数をいれる

(define argv-count (vector-length argv))

(let loop ((i 0))
  (unless (>= i argv-count)
    (display (vector-ref argv i))
    (newline)
    (loop (+ i 1))))

このスクリプト echoall を呼んでみましょう。 echoall 1 2 3 と呼ぶと以下のように表示されます。

1 
2 
3 

このスクリプトの名前("echoall")は引数ベクタには含まれない ことに注意してください。

16.3  例

もっと実質的な問題に取り組みましょう。ファイルを一台のコンピュータから 別のコンピュータに転送する必要があり、運搬媒体としては 3.5インチの フロッピィディスクしかないとしましょう。そこで、1.44M Byte より大きい ファイルをフロッピィのサイズの塊に分割するスクリプト split4floppy が 必要になります。スクリプトファイル split4floppy は次のようになります。

":";exec mzscheme -r $0 "$@"

;floppy-size = 3.5インチフロッピィにおさまるバイト数

(define floppy-size 1440000)

;split は大きなファイル f をフロッピィの容量サイズの小さいサブファイルに
;subfile-prefix.1、subfile-prefix.2 などに分割する。

(define split
  (lambda (f subfile-prefix)
    (call-with-input-file f
      (lambda (i)
        (let loop ((n 1))
          (if (copy-to-floppy-sized-subfile i subfile-prefix n)
              (loop (+ n 1))))))))

;copy-to-floppy-sized-subfile は次の 1.44M Byte (のこりがこれよりも
;少ければ、全部)をその大きいファイルから n 番目のサブファイルに
;コピーする。まだ、残っていれば真を返し、さもなければ、偽を返す。

(define copy-to-floppy-sized-subfile
  (lambda (i subfile-prefix n)
    (let ((nth-subfile (string-append subfile-prefix "."
                                      (number->string n))))
      (if (file-exists? nth-subfile) (delete-file nth-subfile))
      (call-with-output-file nth-subfile
        (lambda (o)
          (let loop ((k 1))
            (let ((c (read-char i)))
              (cond ((eof-object? c) #f)
                    (else
                     (write-char c o)
                     (if (< k floppy-size)
                         (loop (+ k 1))
                         #t))))))))))

;bigfile = スクリプトの第一引数
;        = 分割する必要のあるファイル

(define bigfile (vector-ref argv 0))

;subfile-prefix = スクリプトの第二引数
;               = サブファイルのベース名

(define subfile-prefix (vector-ref argv 1))

;split を呼び、subfile-prefix.{1,2,3,...} を bigfile から生成する。

(split bigfile subfile-prefix)

スクリプト split4floppy は以下のように呼びます。

split4floppy largefile chunk 

これは largefile をサブファイル chunk.1chunk.2、... に分割し、それぞれが、一枚のフロッピィに入るようにします。

chunk.i を相手先のコンピュータに持っていってから、chunk.i を 一つづきにして largefile を復元できます。Unix なら

cat chunk.1 chunk.2 ... > largefile 

DOS なら

copy /b chunk.1+chunk.2+... largefile 

とします。