The Common Lisp Cookbook – ファイルとディレクトリ

Table of Contents

The Common Lisp Cookbook – ファイルとディレクトリ

📢 🎓 ⭐ Learn Common Lisp efficiently in videos, by the Cookbook's main contributor. Learn more.

🖊️ Discover a new Common Lisp and Coalton editor for beginners: mine and a new VSCode extension for Common Lisp: OLIVE.

ここでは 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-namenil を返すことがあります。代わりに 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/")

これは foobarbaz を作成するかもしれません。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 を含めない場合、otherpathprojects は file と見なされるため、otherpathprojects を含む 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 には openclose 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))

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-lineread-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-stringwith-open-filemake-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-linenil を返します。

(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-charread-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-charwhitespace 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-lineread-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-filestr:to-file はどちらも、file creation を control する cl:open と同じ keyword argument、つまり :if-existsif-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 directoriespathnames などの操作ができます。

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 を使います。

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 として次を取ります。

directory が与えられると、collectp がその directory に対して true を返した場合、その directory に対して collector function を call し、recursep が true を返す各 subdirectory を recurse します。

したがって、この function により filesystem hierarchy を traverse でき、cl-fad:walk-directory の機能を置き換えられます。

symlink が存在する場合の behavior は portable ではありません。そのような状況を扱うには IOlib を使います。

例:

(defparameter *dirs* nil "All recursive directories.")

(uiop:collect-sub*directories "~/cl-cookbook"
    (constantly t)
    (constantly t)
    (lambda (it) (push it *dirs*)))
(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-fad:walk-directory "./"
  (lambda (name)
     (format t "~A~%" name))
   :directories t)
(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

T
O
C