ここでは file と directory を操作するための function と library をいくつか見ていきます。
この章では主に namestrings を使って specify filenames. file name を指定します。いくつかの recipe では pathnames. も使います。
多くの function は UIOP 由来なので、直接見ておくことを勧めます。
もちろん、次も見逃さないでください。
pathname の構成要素を取得する
file name (directory なし)
pathname から file name を取得するには file-namestring を使います。
(file-namestring #p"/path/to/file.lisp") ;; => "file.lisp"
file extension
file extension は Lisp 用語では “pathname type” と呼ばれます。
(pathname-type "~/foo.org") ;; => "org"
file basename
basename は “pathname name” と呼ばれます。
(pathname-name "~/foo.org") ;; => "foo"
(pathname-name "~/foo") ;; => "foo"
directory pathname に trailing slash がある場合、pathname-name は nil を返すことがあります。代わりに pathname-directory を使います。
(pathname-name "~/foo/") ;; => NIL
(first (last (pathname-directory #P"~/foo/"))) ;; => "foo"
parent directory
(uiop:pathname-parent-directory-pathname #P"/foo/bar/quux/")
;; => #P"/foo/bar/"
file が存在するか test する
function
probe-file
を使います。これは
generalized boolean -
を返します。file が存在しなければ nil、存在するならその
truename
(渡した argument とは異なる場合があります) を返します。
より portable にするには、uiop:probe-file* または uiop:file-exists-p を使います。これらは (存在する場合) file pathname を返します。
file name に *、[、] などの wildcard character が含まれているかもしれない場合は、下を読んでください。
* (probe-file "/etc/passwd")
#p"/etc/passwd"
;; symlink を作成する (shell):
$ ln -s /etc/passwd foo
* (probe-file "foo")
#p"/etc/passwd"
* (probe-file "bar")
NIL
file が存在するか test する (wildcard character に注意)
(make-pathname :name filename-with-wild-chars) の後に probe-file を使うか、SBCL では sb-ext:parse-native-namestring を使います。なぜでしょうか。
* だけでなく [ と ] も wildcard character です。file name の中では、これらは制限付きの wildcard pathnames を作ります。
file name にこれらが含まれている場合、file が存在していても uiop:probe-file* と uiop:file-exists-p は NIL を返します。
“best-of-[2000]-01.mp3” という music file があるとします。
$ touch best-of-\[2000\]-01.mp3
2 つの backslash で character を escape しない限り、probe-file は使えません (これは str:replace-all で行うことになるでしょう)。
(probe-file "best-of-[2000]-01.mp3")
;; => NIL
(probe-file "best-of-\\[2000\\]-01.mp3")
;; => #P"best-of-\\[2000]-01.mp3"
make-pathname に続けて probe-file を使えます。
(probe-file (make-pathname :name "best-of-[2000]-01.mp3"))
;; => #P"/home/me/path/to/best-of-\\[2000]-01.mp3"
SBCL では sb-ext:parse-native-namestring を使えます。
(sb-ext:parse-native-namestring "best-of-[2000]-01.mp3")
;; => #P"best-of-\\[2000]-01.mp3"
uiop:ensure-pathname では :want-non-wild t key parameter を使えます。
tilde (~) を含む file name または directory name を展開する
portability のために uiop:native-namestring を使います。
(uiop:native-namestring "~/.emacs.d/")
"/home/me/.emacs.d/"
存在しない file や directory についても tilde を展開します。
(uiop:native-namestring "~/foo987.txt")
:: "/home/me/foo987.txt"
いくつかの implementation (CCL、ABCL、ECL、CLISP、LispWorks) では、namestring も同様に動作します。SBCL では file または directory が存在しない場合、namestring は path を展開せず、tilde を含む argument を返します。
存在する file には truename も使えます。ただし少なくとも SBCL では、path が存在しない場合 error を返します。
pathname を Windows の directory separator を使う string に変換する
ここでも uiop:native-namestring を使います。
CL-USER> (uiop:native-namestring #p"~/foo/")
"C:\\Users\\You\\foo\\"
逆の operation には uiop:parse-native-namestring も参照してください。
directory を作成する
function ensure-directories-exist は、directory が存在しない場合に作成します。
(ensure-directories-exist "foo/bar/baz/")
これは foo、bar、baz を作成するかもしれません。trailing slash を忘れないでください。
directory を削除する
pathname (#p)、trailing slash、:validate key とともに uiop:delete-directory-tree を使います。
;; mkdir dirtest
(uiop:delete-directory-tree #p"dirtest/" :validate t)
directory を指す string を pathname で包んで使うこともできます。
(defun rmdir (path)
(uiop:delete-directory-tree (pathname path) :validate t))
UIOP には delete-empty-directory もあります。
cl-fad has (fad:delete-directory-and-files "dirtest").
file と directory を merge する
merge-pathnames を使いますが、注意点が 1 つあります。directory を append したい場合、第 2 argument には trailing / が必要です。
いつものように UIOP function を見てください。corner case を修正する uiop:merge-pathnames* 相当があります。
ある directory に別の directory を append する方法は次のとおりです。
(merge-pathnames "otherpath" "/home/vince/projects/")
;; 重要: ^^
;; trailing / は directory を表す
;; => #P"/home/vince/projects/otherpath"
違いを見てください。どちらの path にも trailing slash を含めない場合、otherpath と projects は file と見なされるため、otherpath は projects を含む base directory に append されます。
(merge-pathnames "otherpath" "/home/vince/projects")
;; #P"/home/vince/otherpath"
;; ^^ "projects" はない。file と見なされたため
あるいは、otherpath/ (trailing / 付き) だが projects は file と見なされる場合です。
(merge-pathnames "otherpath/" "/home/vince/projects")
;; #P"/home/vince/otherpath/projects"
;; ^^ ここに挿入される
current working directory (CWD) を取得する
uiop/os:getcwd を使います。
(uiop/os:getcwd)
;; #P"/home/vince/projects/cl-cookbook/"
;; ^ trailing slash 付き。merge-pathnames に便利
Lisp project からの相対 current directory を取得する
asdf:system-relative-pathname system path を使います。
mysystem の中で作業しているとします。これは ASDF system declaration を持ち、その system は Lisp image に load されています。この ASDF file は filesystem のどこかにあり、src/web/ への path が欲しいとします。次のようにします。
(asdf:system-relative-pathname "mysystem" "src/web/")
;; => #P"/home/vince/projects/mysystem/src/web/"
これは system source が別の場所にある他の user の machine でも動作します。
current working directory を設定する
uiop:chdir path を使います。
(uiop:chdir "/bin/")
0
path の trailing slash は optional です。
または、次の operation だけ current directory を設定するには、uiop:with-current-directory を使います。
(let ((dir "/path/to/another/directory/"))
(uiop:with-current-directory (dir)
(directory-files "./")))
file を開く
Common Lisp には
open と
close
function があり、これはおそらく馴染みのある他の programming language の同名の function に似ています。しかし、ほとんど常に macro
with-open-file
を使うことを勧めます。この macro は file を開き、終わったら閉じるだけではありません。code が body から異常に抜けた場合 (
go,
return-from,
または throw の使用など) も処理してくれます。with-open-file の典型的な使い方は次のようになります。
(with-open-file (str <_file-spec_>
:direction <_direction_>
:if-exists <_if-exists_>
:if-does-not-exist <_if-does-not-exist_>)
(your code here))
strは、file を開くことで作成される stream に bind される variable です。<_file-spec_>は truename または pathname になります。<_direction_>は通常:input(file から読みたい場合)、:output(file に書きたい場合)、または:io(同時に read and write する場合) です。default は:inputです。<_if-exists_>は、書き込み用に file を開きたいが同名の file がすでに存在する場合にどうするかを指定します。file から読むだけなら、この option は無視されます。default は:errorで、error が signalled されることを意味します。他に便利な option として、:supersede(新しい file が古いものを置き換える)、:append(content が file に追加される)、nil(stream variable がnilに bind される)、:rename(つまり古い file が rename される) があります。<_if-does-not-exist_>は、開きたい file が存在しない場合にどうするかを指定します。error を signal する:error、空の file を作成する:create、stream variable をnilに bind するnilのいずれかです。default は簡単に言えば、指定した他の option に応じて正しいことをする、というものです。詳細は CLHS を参照してください。
with-open-file にはさらに多くの option がある点に注意してください。詳細は
the CLHS entry for open
を参照してください。下に with-open-file の使い方の例があります。また、既存の file を読むために開くだけなら、通常 keyword argument を指定する必要はありません。
file を読む
file を string または line の list に読む
file の内容に string 形式で access したり、line の list を取得したりする必要はかなりよくあります。
uiop は ASDF に含まれており (追加で install する library や load する system はありません)、次の function を持ちます。
(uiop:read-file-string "file.txt")
and
(uiop:read-file-lines "file.txt")
別の方法として、これは read-line や read-char function でも実現できますが、おそらく最善の解決策ではありません。file が複数行に分かれていないかもしれませんし、1 character ずつ読むと大きな performance 問題を招くかもしれません。この問題を解決するには、特定 size の bucket を使って file を読めます。
(with-output-to-string (out)
(with-open-file (in "/path/to/big/file")
(loop with buffer = (make-array 8192 :element-type 'character)
for n-characters = (read-sequence buffer in)
while (< 0 n-characters)
do (write-sequence buffer out :start 0 :end n-characters)))))
さらに、常に character 型の element を使う代わりに、read/write される data の format を自由に変更できます。たとえば octet として data を読むには、with-output-to-string、with-open-file、make-array function の :element-type type argument を '(unsigned-byte 8) に設定できます。
utf-8 encoding で読む
ASCII stream decoding error を避けるために UTF-8 encoding を指定したい場合があります。
(with-open-file (in "/path/to/big/file"
:external-format :utf-8)
...
SBCL の default encoding format を utf-8 に設定する
library の内部を control できないことがあるため、default encoding を utf-8 に設定しておく方がよいでしょう。~/.sbclrc に次の行を追加します。
(setf sb-impl::*default-external-format* :utf-8)
必要に応じて:
(setf sb-alien::*default-c-string-external-format* :utf-8)
file を 1 line ずつ読む
read-line
は stream から 1 line を読みます (default は
standard input)
です)。line の終端は newline character または file の終端で決まります。この line は trailing newline character なし の string として返ります。(read-line には第 2 戻り値があり、trailing newline がなかった場合、つまり line が file の終端で終わった場合に true になる点に注意してください。) read-line は default では file の終端に到達すると error を signal します。第 2 argument に NIL を渡すとこれを抑制できます。その場合、file の終端に到達すると read-line は nil を返します。
(with-open-file (stream "/etc/passwd")
(do ((line (read-line stream nil)
(read-line stream nil)))
((null line))
(print line)))
file の終端を signal するために nil の代わりに使われる第 3 argument も指定できます。
(with-open-file (stream "/etc/passwd")
(loop for line = (read-line stream nil 'foo)
until (eq line 'foo)
do (print line)))
file を 1 character ずつ読む
read-char
は read-line に似ていますが、1 line ではなく 1 character だけを読みます。もちろん、この function では newline character は他の character と異なる扱いを受けません。
(with-open-file (stream "/etc/passwd")
(do ((char (read-char stream nil)
(read-char stream nil)))
((null char))
(print char)))
1 character 先を見る
stream の次の character を実際には取り除かずに「見る」ことができます。これを行うのが function
peek-char
です。これは最初の (optional) argument に応じて 3 つの異なる目的に使えます (第 2 argument は読む対象の stream です)。最初の argument が nil の場合、peek-char は stream で待っている次の character をそのまま返します。
CL-USER> (with-input-from-string (stream "I'm not amused")
(print (read-char stream))
(print (peek-char nil stream))
(print (read-char stream))
(values))
#\I
#\'
#\'
最初の argument が T の場合、peek-char は
whitespace
character を skip します。つまり stream で待っている次の non-whitespace character を返します。whitespace character は read-char で読まれたかのように stream から消えます。
CL-USER> (with-input-from-string (stream "I'm not amused")
(print (read-char stream))
(print (read-char stream))
(print (read-char stream))
(print (peek-char t stream))
(print (read-char stream))
(print (read-char stream))
(values))
#\I
#\'
#\m
#\n
#\n
#\o
peek-char の最初の argument が character の場合、その特定の character が見つかるまですべての character を skip します。
CL-USER> (with-input-from-string (stream "I'm not amused")
(print (read-char stream))
(print (peek-char #\a stream))
(print (read-char stream))
(print (read-char stream))
(values))
#\I
#\a
#\a
#\m
peek-char には、read-line や read-char と同様に end-of-file 時の behaviour を control する追加の optional argument がある点に注意してください (default では error を signal します)。
CL-USER> (with-input-from-string (stream "I'm not amused")
(print (read-char stream))
(print (peek-char #\d stream))
(print (read-char stream))
(print (peek-char nil stream nil 'the-end))
(values))
#\I
#\d
#\d
THE-END
function
unread-char. You
を使うと、1 character を stream に戻すこともできます。character を読んだ 後で、read-char ではなく peek-char を使うべきだったと判断した場合のように使えます。
CL-USER> (with-input-from-string (stream "I'm not amused")
(let ((c (read-char stream)))
(print c)
(unread-char c stream)
(print (read-char stream))
(values)))
#\I
#\I
stream の先頭は stack のようには振る舞わない点に注意してください。stream に戻せるのは正確に 1 character だけです。また、以前に読んだものと同じ character を戻さなければならず、まだ何も読んでいない場合は character を unread できません。
file への random access
function
file-position
を file への random access に使います。この function を 1 つの argument (stream) で使うと、stream 内の現在 position を返します。2 つの argument で使うと (下を参照)、stream 内の
file position
を実際に変更します。
CL-USER> (with-input-from-string (stream "I'm not amused")
(print (file-position stream))
(print (read-char stream))
(print (file-position stream))
(file-position stream 4)
(print (file-position stream))
(print (read-char stream))
(print (file-position stream))
(values))
0
#\I
1
4
#\n
5
content を file に書く
with-open-file では :direction :output を指定し、内部で write-sequence を使います。
(with-open-file (f <pathname> :direction :output
:if-exists :supersede
:if-does-not-exist :create)
(write-sequence s f))
file が存在する場合、content を :append することもできます。
存在しない場合は :error にできます。詳細は the standard を参照してください。
library を使う
Alexandria library には write-string-into-file という function があります。
(alexandria:write-string-into-file content "file.txt")
また、str library には to-file function があります。
(str:to-file "file.txt" content) ;; optional option 付き
alexandria:write-string-into-file と str:to-file はどちらも、file creation を control する cl:open と同じ keyword argument、つまり :if-exists と if-does-not-exists を取ります。
file attribute (size、access time など) を取得する
Osicat
は Windows を含む POSIX-like system 向けの Common Lisp の軽量 operating system interface です。Osicat を使うと、environment variables の取得と設定 (現在は uiop:getenv でも可能)、files and directories、pathnames などの操作ができます。
file-attributes is a 新しく軽量な OS portability library で、system call (cffi) を使って file attribute を取得することに特化しています。
sb-posix contrib を持つ SBCL も使えます。
file attribute (Osicat)
Osicat を install すると、file attribute を取得できる osicat-posix system も定義されます。
(ql:quickload "osicat")
(let ((stat (osicat-posix:stat #P"./files.md")))
(osicat-posix:stat-size stat)) ;; => 10629
他の attribute は次の method で取得できます。
osicat-posix:stat-dev
osicat-posix:stat-gid
osicat-posix:stat-ino
osicat-posix:stat-uid
osicat-posix:stat-mode
osicat-posix:stat-rdev
osicat-posix:stat-size
osicat-posix:stat-atime
osicat-posix:stat-ctime
osicat-posix:stat-mtime
osicat-posix:stat-nlink
osicat-posix:stat-blocks
osicat-posix:stat-blksize
file attribute (file-attributes)
library は次で install します。
(ql:quickload "file-attributes")
package は org.shirakumo.file-attributes です。function に短く access するために、たとえば package-local nickname を使えます。
(uiop:add-package-local-nickname :file-attributes :org.shirakumo.file-attributes)
あとは単に function を使います。
access-time,modification-time,creation-time。これらはsetfできます。owner,group,attributes。これらの function で使われる value は OS specific です。attribute flag はdecode-attributesとencode-attributesにより standardized form で decode/encode できます。
CL-USER> (file-attributes:decode-attributes
(file-attributes:attributes #p"test.txt"))
(:READ-ONLY NIL :HIDDEN NIL :SYSTEM-FILE NIL :DIRECTORY NIL :ARCHIVED T :DEVICE
NIL :NORMAL NIL :TEMPORARY NIL :SPARSE NIL :LINK NIL :COMPRESSED NIL :OFFLINE
NIL :NOT-INDEXED NIL :ENCRYPTED NIL :INTEGRITY NIL :VIRTUAL NIL :NO-SCRUB NIL
:RECALL NIL)
documentation を参照してください。
file attribute (sb-posix)
この contrib は POSIX system では default で load されます。
まず file の stat object を取得し、それから必要な stat を取得します。
CL-USER> (sb-posix:stat "test.txt")
#<SB-POSIX:STAT {10053FCBE3}>
CL-USER> (sb-posix:stat-mtime *)
1686671405
file と directory を list する
下のいくつかの function は pathname を返すため、次が必要になるかもしれません。
(namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/baz.txt"
(directory-namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/"
(file-namestring #p"/foo/bar/baz.txt") ==> "baz.txt"
directory 内の file を list する
(uiop:directory-files "./")
pathname の list を返します。
(#P"/home/vince/projects/cl-cookbook/.emacs"
#P"/home/vince/projects/cl-cookbook/.gitignore"
#P"/home/vince/projects/cl-cookbook/AppendixA.jpg"
#P"/home/vince/projects/cl-cookbook/AppendixB.jpg"
#P"/home/vince/projects/cl-cookbook/AppendixC.jpg"
#P"/home/vince/projects/cl-cookbook/CHANGELOG"
#P"/home/vince/projects/cl-cookbook/CONTRIBUTING.md"
[…]
sub-directory を list する
(uiop:subdirectories "./")
(#P"/home/vince/projects/cl-cookbook/.git/"
#P"/home/vince/projects/cl-cookbook/.sass-cache/"
#P"/home/vince/projects/cl-cookbook/_includes/"
#P"/home/vince/projects/cl-cookbook/_layouts/"
#P"/home/vince/projects/cl-cookbook/_site/"
#P"/home/vince/projects/cl-cookbook/assets/")
file を iterate する (lazy に)
上の function に加えて、directory を lazy に traverse する解決策にも触れておきます。これらは file の list 全体を load してから返すわけではありません。
Osicat には with-directory-iterator があります。
(with-directory-iterator (next "/")
(loop for entry = (next)
while entry
when (member :group-write (file-permissions entry))
collect entry))
;; => (#P"tmp/")
LispWorks には fast-directory-files function があり、AllegroCL には map-over-directory があります。
directory を recursively に traverse (walk) する
uiop/filesystem:collect-sub*directories を参照してください。これは argument として次を取ります。
- a
directory - a
collectpfunction - a
recursepfunction - a
collectorfunction
directory が与えられると、collectp がその directory に対して true を返した場合、その directory に対して collector function を call し、recursep が true を返す各 subdirectory を recurse します。
したがって、この function により filesystem hierarchy を traverse でき、cl-fad:walk-directory の機能を置き換えられます。
symlink が存在する場合の behavior は portable ではありません。そのような状況を扱うには IOlib を使います。
例:
- subdirectory だけを collect します。
(defparameter *dirs* nil "All recursive directories.")
(uiop:collect-sub*directories "~/cl-cookbook"
(constantly t)
(constantly t)
(lambda (it) (push it *dirs*)))
- file と subdirectory を collect します。
(let ((results))
(uiop:collect-sub*directories
"./"
(constantly t)
(constantly t)
(lambda (subdir)
(setf results
(nconc results
;; 細かい点: pathname ではなく string を返す
(loop for path in (append (uiop:subdirectories subdir)
(uiop:directory-files subdir))
collect (namestring path))))))
results)
cl-fadlibrary でも同じことができます。
(cl-fad:walk-directory "./"
(lambda (name)
(format t "~A~%" name))
:directories t)
- もちろん external tool も使えます。古き良き unix
find、またはより新しいfd(Debian ではfdfind) です。fdはより単純な syntax を持ち、common files and directories の集合を default で filter out します (node_modules、.git など)。
(str:lines (uiop:run-program (list "find" ".") :output :string))
;; または
(str:lines (uiop:run-program (list "fdfind") :output :string))
ここでは str library の助けを借りています。
pattern に match する file を探す
下では単に directory の file を list し、その name が与えられた string を含むかを確認します。
(remove-if-not (lambda (it)
(search "App" (namestring it)))
(uiop:directory-files "./"))
(#P"/home/vince/projects/cl-cookbook/AppendixA.jpg"
#P"/home/vince/projects/cl-cookbook/AppendixB.jpg"
#P"/home/vince/projects/cl-cookbook/AppendixC.jpg")
pathname を string に変換するために namestring を使いました。これにより、search が扱える sequence になります。
wildcard で file を探す
unix wildcard を portable Common Lisp にそのまま移すことはできません。
pathname string では * と ** を wildcard として使えます。これは absolute pathname と relative pathname で動作します。
(directory #P"*.jpg")
(directory #P"**/*.png")
default pathname を変更する
current directory を表す . という概念は portable Common Lisp には存在しません。これは特定の filesystem や特定の implementation には存在するかもしれません。
home directory を表す ~ も存在しません。いくつかの implementation は non-portable extension として認識することがあります。
*default-pathname-defaults* は一部の pathname operation に default を提供します。
(let ((*default-pathname-defaults* (pathname "/bin/")))
(directory "*sh"))
(#P"/bin/zsh" #P"/bin/tcsh" #P"/bin/sh" #P"/bin/ksh" #P"/bin/csh" #P"/bin/bash")
(user-homedir-pathname) も参照してください。
Page source: ja/files.md