入出力

Common Lispでの(主にファイルの)入出力関数を試してみた.
概要
ストリームのオープン
パス
出力関数
format
入力
with-open-file
練習clcat

概要

Common Lispでは標準の入出力ストリームが大域変数*standard-input*と*standard-output*に格納されている.これらはデフォルトではターミナルである.
> *standard-input* 
#<IO SYNONYM-STREAM *TERMINAL-IO*>
> *standard-output* 
#<IO SYNONYM-STREAM *TERMINAL-IO*>
任意のストリームを作成するには,関数openを使用する.
open パス &key direction element-type if-exists if-does-not-exist external-format
関数openはストリームを返す.返されたストリームを関数formatの第一引数に渡せばそのストリームに出力が流し込まれる.ストリームを閉じるには関数closeを使用する.
(close ストリーム)
以下はカレントディレクトリにhogeというファイルを作成し,それへのストリームに出力する例である.
> (setf stream (open "hoge" :direction :output))
#<OUTPUT BUFFERED FILE-STREAM CHARACTER #P"hoge" @1>
> (format stream "hoge")
NIL
> (format stream "fuga~%")
NIL
> (format stream "foo")
NIL
> (close stream)
T
実行すると,カレントディレクトリにhogeという名前のファイルが作られ,
hogefuga
foo
と書き込まれる.ファイルへの出力は関数close呼び出しが終るまでには完了するが,formatして即書き込まれるかどうかの保証はない.ストリームからの読み込みには関数read-line等を使用する.以下はhogeと書かれたファイルhogefileの内容を読み込む例である.
> (setf stream (open "hogefile" :direction :input))
#<INPUT BUFFERED FILE-STREAM CHARACTER #P"hogefile" @1>
> (setf line (read-line stream))
"hoge"
> line 
"hoge"
以上のようにCommon Lispでの入出力は他の多くのプログラミング言語同様,ストリームを使用して行なう.

ストリームのオープン

openはストリームを作成して返す関数である.
open パス &key direction element-type if-exists if-does-not-exist external-format
openは多くのキーワード複数をもち,ストリーム作成に関していろいろな指定ができる.openのキーワード引数について以下に示す.
:direction
ストリームの用途を指定する.値として以下のキーワード引数を取る.
:input
入力
:output
出力
:io
入出力
:prove
用途を指定しない.ストリームを作成するが,関数openが返る前にストリームはクローズされる.
:element-type
ファイルストリームタイプの指定.デフォルトでは文字ストリームである.他にバイトストリームに出来たりする.
:if-exists
:directionに:outputを指定した時に,出力ファイルが既存であればどうするかを指定する.値として以下を取る.
:error
file-errorが返る.
:new-version
デフォルト.より大きなバージョン番号が振られたファイルが作成される.
:rename
既存のファイルがリネームされて,指定したファイル名のファイルが新規作成される.
:rename-and-delete
既存のファイルはリネームされてdeleteされるがexpungeされない???そして指定したファイル名のファイルが新規作成される.
:overwrite
既存のファイルに上書きする.open直後,ファイルポインタはファイルの先頭にある.
:append
既存のファイルに追加書き込みする.open直後,ファイルポインタはファイルの末尾にある.
supersede
既存の新ファイルで置き換える.処理系は可能ならばストリームが閉じられるまで,既存ファイルを新ファイルで置き換えない.
nil
ファイルもストリームも作成しない.nilが返る.
:if-does-not-exist
指定したファイル名と同名のファイルが既存でない場合にどうするかを指定する.値として以下を取る.
:error
file-errorが返る.
:create
空ファイルが作成され,ファイルが既存であった場合のように処理が続行される.ただしif-existsで指定したdirectionは実行されない.
nil
ファイルもストリームも作成されない.nilが返る.
:external-format
ファイルの拡張フォーマットを指定する.値として標準化されているものは:defaultのみで,あとは処理系による.

パス

先にストリングでファイル名を指定してopen関数を呼んだが,この方法は可搬性がないらしい.つまり環境によっては正しく動作しない.これに対処するために,make-pathnameという関数を使用してパス名を取得し,これをopenに渡す方法がある.
make-pathname &key host device directory name type version defaults case
多くのキーワード引数があるが,clispでは一部はサポートされていない雰囲気なので一部だけ以下に記述する.
:directory
ディレクトリを指定する.ディレクトリはルート側からディレクトリ名をストリングで列挙する.絶対パスの場合,列挙は第一引数に:absoluteをもつリストで行なう(※).
:name
ファイル名.
:type
指定した値がファイル名のサフィックスとして付加される.
以下にいくつか例を記述しておく.
;; ファイル名"hoge"(相対パス)
> (make-pathname :name "hoge")
#P"hoge"
;; /tmp/hoge
> (make-pathname :name "hoge" :directory '(:absolute "tmp"))
#P"/tmp/hoge"
;; ルートディレクトリ
> (make-pathname :directory '(:absolute))
#P"/"
ここで記述していないキーワードにはhost,device,versionなどがある.Common Lispではlogical-pathnameという概念があって,make-pathnameでは単にパスを表す文字列だけでなくホスト名,デバイス名,バージョンなどの情報を格納してpathname-host,pathname-device,pathname-versionなどの関数で取り出せるようだ.clispではこれらが動作しないように見える.
> (pathname-host (make-pathname :host "hoge"))
NIL
※ 相対パスを指定する方法はよくわからない.仕様では単にストリングを指定するとか,それらをリストで表現することで相対パスになるようだが,clispではそうすると怒られる.単に:nameだけを指定してopenすると,カレントディレクトリにファイルが作成されるようだ.
> (make-pathname :directory '(:absolute "tmp"))
#P"/tmp/"
> (make-pathname :directory '("tmp"))

*** - MAKE-PATHNAME: illegal :DIRECTORY argument ("tmp")
> (make-pathname :directory "tmp")

*** - MAKE-PATHNAME: illegal :DIRECTORY argument "tmp"

出力関数

Common Lispの基本的な関数はprin1,princ,print,pprintである(※).
prin1 object &optional output-stream
princ object &optional output-stream
print object &optional output-stream
pprint object &optional output-stream
前者3つはobjectを返し,pprintは何も返さない.4つともオプション引数としてストリームを取るが,何も指定されない場合は*standard-output*をデフォルト値として使用する.4つの関数を実際に実行して違いを比べてみる.
> (prin1 "hoge")
"hoge"
"hoge"
> (princ "hoge")
hoge
"hoge"
> (print "hoge")

"hoge" 
"hoge"
> (pprint "hoge")

"hoge"

>
出力は微妙に違うが,pprint以外の返り値は皆同じ「"hoge"」であるように見える.これは処理系が関数の返り値をprin1で出力しているためである.4者の違いを以下にまとめる.
prin1
関数readに適した出力,すなわちLispプログラムに適した出力を生成する.
princ
prin1がLispプログラムに適した出力を生成するのに対し,princはユーザの見やすい出力を生成する.エスケープ文字も出力されない.
print
先頭に改行が付加される点を除いてprin1と同じである.
pprint
続くスペースを空行にすることを除いてprintと同じである.
※ これらは汎用出力関数writeの特化したものらしい.
write object &key array base case circle escape gensym
      length level lines miser-width pprint-dispatch pretty
      radix readably right-margin stream
writeはobjectを返す.なんかキーワード引数が一杯あるが,このキーワード引数の組み合わせでよく使用するものをprin1,princ,print,pprintなどとして定義しているらしい.

format

もっとも強力な出力関数はおそらくformatである.
format destination control-string &rest args
第一引数destinationは出力先ストリームである.tなら*standard-output*に出力する.nilならストリングを生成して返す.ファイルストリーム等を指定してもよい.第二引数control-stringは出力フォーマットである.今までも~Aと~%を使用してきた.argsは出力フォーマット中のフォーマット指示子にあてはめる値である.フォーマット指示子は引数を取る.例えば~Rは~とRの間に:や@を引数に取る.主なフォーマット指示子は以下である.
~~
チルダを出力する.~n~でn個のチルダを出力する.
> (format nil "~~")
"~"
> (format nil "~3~")
"~~~"
~%
改行.~n%でn行の改行を出力する.
> (format nil "~%")
"
"
> (format nil "~3%")
"


"
~C
文字.引数に:を指定すると,関数readが認識できる出力を生成する.
> (format nil "~C" #\a)
"a"
> (format nil "~:C" #\a)
"a"
> (format nil "~C" #\Space)
" "
> (format nil "~:C" #\Space)
"Space"
~A
文字列をprincスタイルで出力.~mincol,colinc,minpad,padcharAと4つの引数を取る.
mincol
フィールド幅.
> (format nil "~10A" "hoge")
"hoge      "
;; ~n@Aでフィールド幅をnにして右寄せする.
> (format nil "~10@A" "hoge")
"      hoge"
colinc
出力にminpad個の文字を付加してもmincolに満たない場合に一回あたりに付加する文字数.
minpad
出力に付加する文字の文字数.
padchar
mincol,colinc,mipnadによって出力に付加する文字.
>(format nil "~,,6,'*A" "hoge")
"hoge******"
> (format nil "~15,,6,'*A" "hoge")
"hoge***********"
まとめると出力にminpad個の文字padcharを付加し,さらに出力文字数がmincol以上になるまで,一回につきcolinc個の文字padcharを付加する.
> (format nil "~,,3,'*A" "a")
"a***"
> (format nil "~,,3,'*A" "a")
"a***"
> (format nil "~5,,3,'*A" "a")
"a****"
; aに*(= padchar)を3(= minpad)文字付加したが,
; それが"a***"で4文字であるので(4 < mincol = 5),
; さらに*を3(= colinc)文字付加してa******となる.
> (format nil "~5,3,3,'*A" "a")
"a******"
; colincは出力文字数がmincol以上になるまで
; 「一回あたり」に付加する文字数である.
> (format nil "~8,3,3,'*A" "a")
"a*********"
~S
文字列をprin1スタイルで出力.~Aと同様の引数をとるが,出力は異なる.
> (format nil "~10,4,5,'*A" "hoge")
"hoge*********"
;; "も出力文字列に含まれるので,出力される*の数が~Aとは異なる.
> (format nil "~10,4,5,'*S" "hoge")
"\"hoge\"*****"
~F
浮動小数点.~w,d,k,overflowchar,padcharFと5つの引数をもつ.
w
出力文字数.
> (format nil "~10F" 1.2345)
"    1.2345"
d
小数点以下の表示文字数.出力が丸められるとき,四捨五入するのか切り上げか切捨てかは保証されない.
> (format nil "~10,2F" 1.2345)
"      1.23"
> (format nil "~,3F" 1.2345)
"1.235"
k
10のk乗掛けた出力.
> (format nil "~10,2,5F" 1.2345)
" 123450.00"
> (format nil "~10,2,-1F" 1.2345)
"      0.12"
overflowchar
文字数wでは出力しきれないときに,表示する文字.
> (format nil "~3,2,,'+,'*F" 1.2345)
"+++"
padchar
数字がw文字以下の場合に数字の前に表示する文字.
> (format nil "~10,,,,'*F" 1.2345)
"****1.2345"
~E
正規化して出力する.
> (format nil "~E" 12345)
"1.2345E+4"
~w,d,e,k,overflowchar,padchar,exponentcharEと多くのパラメータを持つ.
w
出力文字数
d
小数点以下の出力文字数
e
指数部分の出力文字数
> (format nil "~,,10,,,,,E" 12345)
"1.2345E+0000000004"
k
整数部分の桁数
> (format nil "~,,,1,,,,E" 12345)
"1.2345E+4"
> (format nil "~,,,2,,,,E" 12345)
"12.345E+3"
> (format nil "~,,,3,,,,E" 12345)
"123.45E+2"
overflowchar
文字列が出力文字数に収まらない時に出力する文字.
padchar
文字列が出力文字数に満たない場合に出力する文字.
exponentchar
Eのかわりに出力する文字列
> (format nil "~,,,,,,'XE" 12345)
"1.2345X+4"
~R
数値の汎用変換指示子.デフォルトは数字の英語表記("one" "two" "three" "four" .....)らしい.~:Rだと("first" "second" "third" "fourth"....).~@Rだとローマ数字.~:@Rだと古いローマ数字(例えば4がIIIIになる).~nRだとn進数.~radix,mincol,padchar,commachar,comma-intervalRと7つの引数をとる.
radix
基数
> (format nil "~10R" 20)
"20"
> (format nil "~2R" 20)
"10100"
> (format nil "~8R" 20)
"24"
> (format nil "~16R" 20)
"14"
mincol
出力文字数
padchar
出力がincolに満たない場合に出力する文字.
commachar
区切り文字
comma-interval
区切り文字間の文字数(:Rとしないと区切られないようだ).
> (format nil "~2,,,' ,4:R" 1000)
"11 1110 1000"
~D
~10Rと同じ.~n,cDと二つの引数を取り,nは出力文字数,cは出力がn文字に満たない場合に出力する文字である.
> (format nil "~D" 123)
"123"
> (format nil "~5D" 123)
"  123"
> (format nil "~5,'*D" 123)
"**123"
~B
2進数
~O
8進数
~X
16進数

入力

主な入力関数はreadとread-lineである.
read &optional input-stream eof-error-p eof-value recursive-p
read-line &optional input-stream
               eof-error-p eof-value recursive-p
read-char &optional input-stream eof-error-p
               eof-value recursive-p
3つとも同様のオプション引数を取るが,readは一つのLisp式を読み込み,解析してLispオブジェクトに変換して返す.入力内容は正しいLispコードでなければならない.read-lineは改行までの文字を読み込む.入力は単なる文字列として処理される.read-charはもっとも基本的な読み込み関数で,ストリームから一文字読み込む(char型の値を返す).以下にオプション引数を示す.
input-stream
入力ストリーム.デフォルトは*standard-input*
eof-error-p
ファイル末尾に到達した時にエラーを吐くかどうか.デフォルトはT.
eof-value
eof-error-pがnilだった場合,エラーを吐くかわりに返す引数.
recursive-p
readあるいはread-line内で再帰的に自身を呼び出すかどうか.デフォルトはnil.入力マクロにreadが使用されている場合,readがそれを読むとそのread内部でreadを呼ぶことになるが,たぶんそれを許可するか否かを指定するのに使用するらしい.
readは一度に1Lisp式しか読み込まないので,ユーザが入力する際に不都合がある場合がある.以下はトップレベルからreadに1 2 3を渡すが,readは1Lisp式つまり1しか読み込まないので,次の入力で2 3が入力されたことになり,困る場合がある.
> (setf in (read))
1 2 3
1
>
2
> in
1
この問題の対処に使用するのによく使用されるのが,read-line,read-from-stringの組み合わせである.
> (setf in (read-line ))
1 2 3
"1 2 3"
> in
"1 2 3"
> (read-from-string in)
1 ;
2
ユーザ入力を1行分もれなく読み込み,その文字列に対してreadする(read-from-string)ので,読み残しが次のトップレベルへの入力になることがない.read-from-stringは値を二つ返す.第一の返り値は読み込んだ文字列に対してreadと同様の処理を施しLispオブジェクトにしたもので,第二の引数は読み込み停止場所である.
read-from-string string &optional eof-error-p eof-value
                 &key start end preserve-whitespace
第一引数は入力文字列で,オプション引数はreadやread-lineなどと同様である.キーワード引数startとendは読み込みの開始位置と終了位置である.eof-error-p,efo-valueを両方指定しないと:startは機能しないようだ.
> (read-from-string "1 2 3") 
1 ;
2
> (read-from-string "1 2 3" :start 2)
1 ;
2
> (read-from-string "1 2 3" t nil :start 2)
2 ;
4
> (read-from-string "1 2 3" nil nil :start 2)
2 ;
4
文字列が:startから:endで指定した範囲がLisp式としてきりが悪ければ(Lisp式として不正ならば),第一の返り値はnilとなる.
> (read-from-string "(+ 1 2) (+ 3 4)")
(+ 1 2) ;
7
> (read-from-string "(+ 1 2) (+ 3 4)" nil nil :start 7)
(+ 3 4) ;
15
> (read-from-string "(+ 1 2) (+ 3 4)" nil nil :start 7 :end 8)
NIL ;
8
> (read-from-string "(+ 1 2) (+ 3 4)" nil nil :start 7 :end 15)
(+ 3 4) ;
15
; '2'から読み込み開始.
> (read-from-string "(+ 1 2) (+ 3 4)" nil nil :start 5)
2 ;
6
; ')'から読み込み開始.
> (read-from-string "(+ 1 2) (+ 3 4)" nil nil :start 6)

*** - READ from #<INPUT STRING-INPUT-STREAM>: an object cannot start with #\)
:preserve-whitespaceはよくわからない.

with-open-file

ファイルストリームをopenしたら必ずcloseしなければならないが,面倒臭いし忘れることもある.with-open-fileを使用すると,その式内でファイルをオープンし,その式から出るときにcloseしてくれるので便利である.
with-open-file (stream filespec options*) 本体部
with-open-fileの返り値は本体の最後の式の返り値である.引数リストのstreamはストリーム,filespecはファイルへのパス,そしてopen関数と同様のオプションを取る.続いて本体部が続く.この本体部ではstreamはopenされていて(option*に応じた)入出力ストリームとして使用できる.以下はwhith-open-fileでhogefileというファイルへの出力ファイルストリームを使用する例である.
> (with-open-file (stream "hogefile" :direction :output)
                     (format stream "hoge~%"))
NIL

練習clcat

練習として,簡易版catのようなものを作成してみた.

(defpackage "CL-SAMPLE"
  (:use "COMMON-LISP")
  (:nicknames "sample")
  (:export "clcat"))

(in-package "CL-SAMPLE")

(defun read-write (stream)
  (multiple-value-bind (line end-flag) (read-line stream)
    (format t "~A~%" line)
    end-flag))

(defun clcat (file path)
  (let ((targetpath
	 (if (listp path)
	     (make-pathname :name file :directory (cons ':absolute path))
	   (make-pathname :name file :directory (list ':absolute path)))))
    (format t "targetpath ~A~%" targetpath)
    (with-open-file
     (stream targetpath :direction :input)
     (do ((end-flag (read-write stream)))
	 ((not (null end-flag)))
       (setf end-flag (read-write stream))))))
  
(unintern 'read-write)
(export 'clcat)

;; �ƥ�����
(defpackage "CL-SAMPLE-TEST"
  (:use "COMMON-LISP" "CL-SAMPLE"))
(in-package "CL-SAMPLE-TEST")

(format t "~%")
(format t "=== now in ~A ===~%" *package*)
(format t "execute : (clcat \"filename\" \"pathname\") ~%")
(format t "  ...or : (clcat \"filename\" '(\"pathname\"+)) ~%")
(eval (read))

このサンプルは関数clcatを定義している.clcatは,第一引数をファイル名(String)としてとる.第二引数はStringのディレクトリか,Stringからなるリストからなる.前者は/hogeというように/か/の一つ下のディレクトリ下のファイルをcatする.後者はそれよりも深いディレクトリ下のファイルをcatする.まず第二引数をStringとして実行してみた.
$> clisp  clcat.lisp
WARNING: *FOREIGN-ENCODING*: reset to ASCII

=== now in #<PACKAGE CL-SAMPLE-TEST> ===
execute : (clcat "filename" "pathname") 
  ...or : (clcat "filename" '("pathname"+)) 
(clcat "services" "etc")
targetpath /etc/services
# Network services, Internet style
#
# Note that it is presently the policy of IANA to assign a single well-known
# port number for both TCP and UDP; hence, officially ports have two entries
# even if the protocol doesn't support UDP operations.
...省略...
*** - READ: input stream
  #<INPUT BUFFERED FILE-STREAM CHARACTER #P"/etc/services" @490>
  has reached its end
もう一例.今度は第二引数をStringのリストにしてみる.
$> clisp  clcat.lisp
WARNING: *FOREIGN-ENCODING*: reset to ASCII

=== now in #<PACKAGE CL-SAMPLE-TEST> ===
execute : (clcat "filename" "pathname") 
  ...or : (clcat "filename" '("pathname"+)) 
(clcat "site.xml" '("home" "matsu" "fp_xml"))
targetpath /home/matsu/fp_xml/site.xml
<?xml version="1.0" encoding="EUC-JP"?>

<!-- $Id: io.xml,v 1.3 2004/10/30 00:24:04 matsu Exp $ -->
<!DOCTYPE site SYSTEM "http://www.h7.dion.ne.jp/~matsu/site.dtd">
...省略...
*** - READ: input stream
  #<INPUT BUFFERED FILE-STREAM CHARACTER #P"/home/matsu/fp_xml/site.xml" @394>
  has reached its end
どちらの実行例でも,ファイルの末尾の検知がうまくいっていないらしく,エラーが出ている.

This article was written by Fujiko