gaucheでUNIXコマンドプログラミング(catコマンド基礎編)


 SICPばかりやってるんじゃ飽きちゃうよね、ということでUnixコマンドをgaucheで作ってみようという試み。コマンドを作ることによって色々なプログラミングのエッセンスを吸収できるんじゃないのぐへへ、という下心満載でやっていこうと思います。


 第一回目はcatコマンド。これ、実は公式ドキュメントに乗ってます。なので、一番最初にやるのには、コマンドの動作自体は結構簡単だし、例もあるし、ということでうってつけだと。今回、このcatコマンドを作ることによってのねらいは、gaucheにおける

  • 引数の処理
  • 入出力の操作

の2点が大まかなねらいです。なお、今回はプログラムが複雑になるのを防ぐために、

  • エラー処理
  • オプション処理

を加えていません。次回ぐらいにオプションをつけてみたいと思います。また、「本来のcatコマンドの使い方じゃねーよm9(^Д^)」というツッコミは華麗にスルーします。それじゃあ、やってみましょうか。


公式サンプル

; コメントは補足して書いています。
#!/usr/bin/env gosh

(define (main args)   ;entry point …1
  (if (null? (cdr args)) …2
      (copy-port (current-input-port) (current-output-port)) ; …3
      (for-each (lambda (file) ; …4
                  (call-with-input-file file ; …5
                    (lambda (in)
                      (copy-port in (current-output-port)))))
                (cdr args)))
  0)


 1.まず最初にmain手続きがきます。わざわざmain手続きを定義するのは

ファイルが正常にロードされたら、goshは userモジュールに `main' という手続きが定義されているかどうか調べ、定義されていればそれを呼びます。mainには、スクリプトへの引数のリストが唯一の引数として渡されます。リストの最初の要素はスクリプトファイル名です。

Gauche Users’ Reference: Top

ということで、引数が欲しかったり、デバックがしやすいからです。


 2ではファイル名が指定されているかを、手続きnull?で調べています。手続きnull?は、

Function: pair? obj
 [R5RS] objがペアなら#tを、そうでなければ#fを返します。

Gauche Users’ Reference: Top

と定義されています。他にもリストがペアかそうでないかを調べる手続きpair?などがあります。まぁ、リストの状態を調べる手続きとしては、null?が一番活躍しそうかなと予想しています。あとリストの値を取り出す手続きcdrですが、これはschemeの基礎の基礎です。「scheme リスト」とでもググってみれば、詳しい情報が出てくるので、そちらを参考になさってください。


 3の部分では、「ファイル名が指定されていなかった場合」の処理を記述しています。処理の内容は、「標準入力を標準出力に出力する」という内容です。ここでgaucheの入出力の概念でportという概念が出てくるのですが、これはUnixにおけるファイルディスクリプタと同義としてもらって差し支えないと思います(ファイルディスクリプタより抽象化された概念だと思いますが…)。手続きcopy-portは

Function: copy-port src dst &keyword (unit 0)
 srcからEOFまでデータを読みだし、dstへ書き出します。

Gauche Users’ Reference: Top

と定義されていて、手続きcurrent-input-port(ここでは標準入力)の内容を、手続きcurrent-output-port(が返すポート)にコピーしています。実際に実行してみると、


dorayaki@Ubuntu-desktop$ ./cat.scm
abc ←入力してCtrl+D
abc ←出力
という、catコマンドでファイル名を指定しない場合と同じ動作をします。


 次は、「ファイル名が指定された場合」の処理です。やりたい処理として、「指定したファイルの内容を標準出力に出力するです。」これは、手続きcall-with-input-fileで実現されています。この手続きは

Function: call-with-input-file string proc &keyword if-does-not-exist buffering element-type
[R5RS] stringで示されるファイルを入力または出力用にオープンし、作成されたポートを引数として手続きprocを呼び出します。 procが正常終了するか、proc内で捕捉されないエラーが起きた場合にファイルはクローズされます。
キーワード引数はif-exists、buffering, element-type、 if-does-not-existは open-input-file及びopen-output-fileのものと同じ意味を持ちます。 if-existsやif-does-not-existに#fを指定した場合、ファイルがオープンされなかった場合はprocにポートではなく#fが渡されることに注意して下さい。 procが返す値を返します。

Gauche Users’ Reference: Top

とされています。かいつまんで言えば、「ファイルを開いて、渡された手続きを実行するよ」という手続きです。一つの手続きで二つのことができるので、かなり便利と言えます。


 また、4でのforeach文ですが、

Function: for-each proc list1 list2 …
[R5RS] 手続きprocをリストの各エレメントに対して順に適用します。 procの結果は捨てられます。for-eachの戻り値は定義されていません。複数のリストが与えられた場合、一番短いリストが終了した時点でfor-eachは終了します。
gauche.collectionモジュール(gauche.collection - コレクションフレームワーク参照) を使うと、for-eachがリスト以外のコレクション型に対しても動作するようになります。

Gauche Users’ Reference: Top

とあります。自分でリストの個数だけの再帰処理を記述せずに済むので、簡単ですね(SICPの時は封印しないと勉強にはなりませんが…)。


 で、まとめると、ここでは、「for-each構文を使って指定したファイル(cdr args)の数の回数分だけ、手続きcall-with-input-fileを実行する。手続きcall-with-input-fileの第一引数にはファイルポートを、第二引数にはlambda構文を使用したファイルの内容を手続きcurrent-output-portが示すポート(ここでは標準出力)に出力する無名手続きを指定する。」ってなことをしている訳ですな。

まとめ

 次回の課題は「このcatコマンドにオプション、エラー処理を追加」にしたいと思います。その後は、有用そうなコマンドを、gaucheで書いていきたいなあと思っているしだいです。何か要望、ツッコミなどございましたらコメントにお願いしたします。