Guile色々/ディレクトリ階層の探索:nftw,ftw,scandir

変更履歴

概 要

目 次

参考資料

モジュール

以下に説明する手続きを利用するためには,(ice-9 ftw) モジュールをロードする必要があります.
(use-modules (ice-9 ftw))

nftw 手続き

nftw 手続きの形式

(nftw path proc)
(nftw path proc option ... )

path によって指定されたディレクトリ下のすべてのファイルやディレクトリ(以下,ノード)を再帰的に探索します. さらに,探索の途中で,各ノードに対して proc を適用します.path は探索の起点となるディレクトリ名(文字列)です. 絶対パスと相対パスのどちらでも指定できます.procは各ノードに適用する手続き,optionは nftw の振る舞いを制御するための引数です. これらと返り値については後述します.

path にファイルを指定することもできます. その場合,そのファイルだけが探索の対象になります.

必須引数

proc は次のようなプロトタイプの手続きです.
(proc node-name st flag base level)
nftw 手続きは,各ノードに proc を適用するとき,上記の各引数に次のような値を渡します.

node-name には, ノードの名前(文字列)を渡します. これには探索起点の path も含まれます. 従って,path に(カレントディレクトリを起点とする)相対パスを指定したときには node-name も相対パスになり,path に絶対パスを指定したときには node-name も絶対パスになります.

st には, ノードの属性値からなるベクトル(stat手続きまたはlstat手続きをノードに適用したときの返り値)を渡します.

flag には,ノードに関する基礎情報として, 下記のシンボルのうちの1つを渡します.
シンボル 意味
'regular ノードが通常ファイルである(ディレクトリでない)ことを示します. これにはスペシャルデバイスやパイプなどの特殊なファイルも含まれます.
'directory ノードがディレクトリであることを示します.
'directory-processed node-name がディレクトリであって, その子孫ノードのすべてに対してprocが適用されたことを示します.このシンボルは後述する 'depth オプションを指定した場合に, 上記の 'directory の代わりに渡されます.
'invalid-stat ノードに対するstat手続きがエラーを起こした(#fを返した)ことを示します.
'directory-not-readable ノードがディレクトリで,かつ,読み取れなかったことを示します.
'stale-symlink node-name がdanglingなシンボリックリンクであることを示します.nftw は,新たに見つけたノードがdanglingなシンボリックリンクだった場合,node-name にそのシンボリックリンクの名前を渡し,flag に 'stale-symlink を渡します.
'symlink ノードがシンボリックリンクであり,リンク先のノードが存在することを示します.nftw は,後述する 'physical オプションが指定されたとき,シンボリックリンクを辿らずに,シンボリックリンク自体の名前を node-name に渡し,flag に 'symlink を渡します.

base は,文字列としてのノード名(node-name)におけるベース部が始まる文字位置(整数)が渡されます.この引数は,ノード名のディレクトリ部とベース部を分離しやすくするためのものです.特に,後述する 'chdir オプションを指定して nftw を実行したときに役立つものと思います.

level は,path を根とするディレクトリ階層におけるノードのレベル(整数)が渡されます. 根(path)のレベルは 0 です.

proc の返り値が #t かそれ以外かによって nftw 手続きの振る舞いが変化します.proc の返り値が #t だった場合,proc の処理が成功したものと見なして,探索を継続します.一方,#t 以外だった場合,proc の実行中に何らかのエラーが発生したものと見なして,探索を終了します.さらにその場合,nftw は proc の返り値をそのまま返します.

▹(注意) Guileのマニュアル[7.12 File Tree Walk] は,proc に関して次のように注意しています(注:「ftw」となっているところを「nftw」に訂正).
In the current implementation, returning non-#t from proc is the only valid way to terminate nftw. proc must not use throw or similar to escape.

オプション引数

option は,nftw の振る舞いを変えるためのもので,次のような引数を指定します.これらは幾つでも指定できます.
引数 意味
'chdir これを指定すると,proc をノードに適用する直前で, ノードを含むディレクトリをカレントディレクトリに設定します. 従って,新たなノードを探索するたびにカレントディレクトリが変化します.ただし,nftwが終了するときにカレントディレクトリを元々のディレクトリ(nftw を開始した時点のカレントディレクトリ)に戻します. カレントディレクトリを変更したときの注意すべき点として,path が相対パスの場合,procの内部で正しいノード名を取得するために,node-namebase を使ってベース部を抽出する必要があります.path が絶対パスのときには node-name も絶対パスになるのでそのまま利用できます.
'depth ・これを指定すると,proc をpost-order(後行順;帰りがけ順)に適用します. つまり,現在のノードがディレクトリのとき, その子孫ノードのすべてに proc を適用し終えたあとで現在のノードに proc を適用します.
・これを指定しなかった場合,pre-order(先行順;行きがけ順)に proc を適用します.
・このオプションについては,後述する補足事項も参照して下さい.
'hash-size size 探索のために利用するハッシュ表の大きさ(スロット数)を指定します.size はスロット数を表す整数です.既定値は 211 スロットです.nftw は, 各ノードに対して proc を2度以上適用することはなく, 各シンボリックリンクを2度以上辿ることもありません. これらを実現するためにハッシュ表を使っています. ちなみに,これによって,巡回的なシンボリックリンクがあったとしても, それらを無限に辿ることはありません.
'mount これを指定すると,マウントポイントをまたがって探索しません.つまり,path と同じファイルシステムだけを探索します.
'physical ・これを指定すると,(danglingでない)シンボリックリンクを辿りません.つまり, (danglingでない)シンボリックリンクのノードに proc を適用しようとするとき,nftw は node-name にシンボリックリンクの名前を渡し,flag に 'symlink を渡します.さらに,procst 引数にはシンボリックリンク自体の属性値を渡します.
・これを指定しなかった場合,nftw は,シンボリックリンクを辿った先のファイルやディレクトリに proc を適用し,(danglingでない)シンボリックリンク自体に proc を適用することはありません.
・なお,danglingなシンボリックリンクは,その先のファイルやディレクトリに辿ることはできないので,常に proc の適用対象になります.

nftw 手続きの返り値

▹nftw 手続きは,すべてのノードに対して proc が #t を返してきた場合,#t を返します.これは探索が問題なく成功したことを意味します. 一方,あるノードに対して proc が #t 以外の値を返してきた場合,その時点で探索を終了し,proc の返り値をそのまま返します.

実行例

▹以下では,次のようなディレクトリに対して nftw 手続きを適用します.
$ ls -lR /home/algo/temp
/home/algo/temp:
drwxr-xr-x 2 algo algo 4096  2月 15 16:40 dir-aaa/
drwxr-xr-x 2 algo algo 4096  2月 15 16:40 dir-bbb/
lrwxrwxrwx 1 algo algo   12  2月 15 16:45 somefile.lnk -> somefile.txt
-rw-r--r-- 1 algo algo   22  2月 15 16:44 somefile.txt

/home/algo/temp/dir-aaa:
lrwxrwxrwx 1 algo algo  7  2月 15 16:40 aaa.lnk -> aaa.txt
-rw-r--r-- 1 algo algo 22  2月 15 16:44 aaa.txt

/home/algo/temp/dir-bbb:
-rw-r--r-- 1 algo algo 22  2月 15 16:44 bbb.txt
$ 

▹まず nftw 手続きを使って,/home/algo/temp ディレクトリ下のすべてのノードを表示してみます. 以下の disp 手続きは, 第1引数の name(ノード名)を表示するだけのものです. 他の引数は無視しています. それから,nftw に探索を続けさせるために #t を返すようにしています.
$ guile
GNU Guile 3.0.5
      ...... 起動メッセージ ......
guile> (define (disp name st flag base level) (display name) (newline) #t)
guile> (use-modules (ice-9 ftw))
guile> (nftw "/home/algo/temp" disp)
/home/algo/temp
/home/algo/temp/dir-aaa
/home/algo/temp/dir-aaa/aaa.txt
/home/algo/temp/dir-bbb
/home/algo/temp/dir-bbb/bbb.txt
/home/algo/temp/somefile.txt
$1 = #t
guile> 

▹上の結果を見るとシンボリックリンクが表示されていません. これは,nftw は(デフォルトでは)シンボリックリンクを辿った先のファイルやディレクトリに disp を適用するためです. そこで,'physical オプションを指定して nftw 手続きを実行してみます.
guile> (nftw "/home/algo/temp" disp 'physical)
/home/algo/temp
/home/algo/temp/dir-aaa
/home/algo/temp/dir-aaa/aaa.txt
/home/algo/temp/dir-aaa/aaa.lnk
/home/algo/temp/dir-bbb
/home/algo/temp/dir-bbb/bbb.txt
/home/algo/temp/somefile.txt
/home/algo/temp/somefile.lnk
$2 = #t
guile> 
今度はシンボリックリンクが表示されます.nftw は,'physical が指定されたとき, シンボリックリンクを辿らずに,シンボリックリンク自体を1つのノードとして扱います.

▹次に,'depth オプションを指定して実行してみます.
guile> (nftw "/home/algo/temp" disp 'physical 'depth)
/home/algo/temp/dir-aaa/aaa.txt
/home/algo/temp/dir-aaa/aaa.lnk
/home/algo/temp/dir-aaa
/home/algo/temp/dir-bbb/bbb.txt
/home/algo/temp/dir-bbb
/home/algo/temp/somefile.txt
/home/algo/temp/somefile.lnk
/home/algo/temp
$3 = #t
guile> 
nftw は,'depth が指定されると,post-order(後行順;帰りがけ順)に disp を適用します.そのため,下位のノードから順に表示されることになります.proc の適用順序については,後述する補足事項も参照して下さい.

具体例

▹ディレクトリ階層を字下げをしながら階層的に表示するスクリプトを作ってみます
#!/usr/bin/guile \
-e main -s
!#

;; show-ft.scm

(use-modules (ice-9 ftw))

(define (main args)
  (let ((dir-name (cadr args)))
    (nftw dir-name disp-node 'physical)))

(define (disp-node name st flag base level)
  (define (indent-space level) (make-string (* level 3) #\space))
  (let ((base-name (substring name base)))
    (display (indent-space level))
    (display base-name)
    (cond 
     ((eq? flag 'directory)
      (display "/"))
     ((eq? flag 'symlink)
      (display " -> ") (display (readlink name)))
     ((eq? flag 'invalid-stat)
      (display " -- stat?"))
     ((eq? flag 'directory-not-readable)
      (display "-- not-readable?"))
     ((eq? flag 'stale-symlink)
      (display " -- dangling symlink")))
    (newline))
  #t
  )

▹下記の /home/algo/temp は先に示したディレクトリです.
$ ./show-ft.scm /home/algo/temp
/home/algo/temp/
   dir-aaa/
      aaa.txt
      aaa.lnk -> aaa.txt
   dir-bbb/
      bbb.txt
   somefile.txt
   somefile.lnk -> somefile.txt
▹次のようなやや病的なディレクトリにも適用してみます.
$ ls -lR /home/algo/temp2
/home/algo/temp2:
drwxr-xr-x 2 algo algo 4096  2月 19 14:45 dir-aaa/
drwxr-xr-x 2 algo algo 4096  2月 19 14:45 dir-bbb/
lrwxrwxrwx 1 algo algo    7  2月 19 14:50 ppp.lnk -> ppp.txt
lrwxrwxrwx 1 algo algo    7  2月 19 14:50 qqq.lnk -> rrr.lnk
lrwxrwxrwx 1 algo algo    7  2月 19 14:50 rrr.lnk -> qqq.lnk

/home/algo/temp2/dir-aaa:
-rw-r--r-- 1 algo algo  0  2月 19 14:44 aaa.txt
lrwxrwxrwx 1 algo algo 10  2月 19 14:45 bbb.lnk -> ../dir-bbb/

/home/algo/temp2/dir-bbb:
lrwxrwxrwx 1 algo algo 10  2月 19 14:45 aaa.lnk -> ../dir-aaa/
-rw-r--r-- 1 algo algo  0  2月 19 14:45 bbb.txt
$ ./show-ft.scm /home/algo/temp2
/home/algo/temp2/
   dir-aaa/
      aaa.txt
      bbb.lnk -> ../dir-bbb
   ppp.lnk -- dangling symlink
   rrr.lnk -- dangling symlink
   qqq.lnk -- dangling symlink
   dir-bbb/
      bbb.txt
      aaa.lnk -> ../dir-aaa

補足

▹Guileのハッシュ表は, スロット数に対するエントリー数の割合が,あるしきい値の上限を超えたり下限を下回ったりしたら,自動的にハッシュ表を拡大・縮小して, すべてのエントリーを再ハッシュします(つまり,余計な仕事をします). 従って,ハッシュ表のスロット数がある程度予測できるのであれば,'hash-size を指定したほうがよいでしょう.

▹nftw はディレクトリ階層の各ノードを素直に再帰的に探索します. つまり,探索処理を大まかに見ると,nftw は次のような go 関数を path に対して実行していると言えます. なお,下記の v はディレクトリ階層内のノードを表します.
   go(v)
      apply proc to v if 'depth is not specified.
      Children ← if v is a directory 
                     then the set of children of v
                     else the empty set
      for each w in Children: go(w) 
      apply proc to v if 'depth is specified.
これは,nftw の中で定義されている go 手続きを抽象化したものです. これを見ると,各ノードの訪問順序は,'depth オプションを指定してもしなくても変わりません.'depth オプションは,訪問順序を制御するのではなく,proc をどのタイミングで適用するかを制御します.'depth を指定しなかったときには pre-oder(先行順;行きがけ順)に適用し,'depth を指定したときには post-order(後行順;帰りがけ順)に適用します.

▹nftw や下記の ftw は,POSIXにおいてライブラリ関数として定義されていて,glibc もそれらを実装しています.しかし,Guile の nftw や ftw は,glibc のライブラリ関数を使わずに独自に実装しています. ただし,ライブラリ関数の仕様を可能な限り忠実に再現しようとしています. 例えば,proc の適用は pre-order か post-order のいずれかを排他的に選ばなければなりません.せっかく独自に実装しているのですから,pre-order と post-order を同時に指定できるようにしても良かったのではないかと感じます. さらに,その排他性のため,pre-order のときに proc に渡される'directory フラグと,post-order のときに渡される 'directory-processed フラグはどちらもノードがディレクトリであることを示しているだけにすぎません. つまり,それらを分離する必要はなく,仕様に不備があるように感じてしまいます. でも,排他性についても分離についても, ライブラリ関数の仕様がそうなっているため, その仕様を忠実に再現しているものと思われます.

ftw 手続き

(ftw path proc)
(ftw path proc 'hash-size size)

path によって指定されたディレクトリ下のすべてのファイルやディレクト(以下,ノード)を再帰的に探索します. さらに,探索の途中で,各ノードに対して proc を適用します.path は探索の起点となるディレクトリ名を表す文字列,proc は手続き(後述),size は探索のために利用するハッシュ表の大きさ(整数),シンボルの 'hash-size はその大きさを指定するときのキーワードです.この手続きの返り値については後述します.

proc は次のようなプロトタイプの手続きです.
(proc node-name st flag)
ftw 手続きは,各ノードに proc を適用するとき,上記の各引数に次のような値を渡します.node-name にはノードの名前(文字列)を渡します.これには探索起点の path も含まれます.st にはノードの属性値からなるベクトル(stat手続きをノードに適用したときの返り値)を渡します.flag には下記のシンボルのうちの1つを渡します.
シンボル 意味
'regular ノードが通常ファイルである(ディレクトリでない)ことを示します. これにはスペシャルデバイスやパイプなどの特殊なファイルも含まれます.
'directory ノードがディレクトリであることを示します.
'invalid-stat ノードに対するstat手続きがエラーを起こした(#fを返した)ことを示します.
'directory-not-readable ノードがディレクトリで,かつ,読み取れなかったことを示します.
'symlink ftw の 'symlink の意味は nftw の 'stale-symlink と同じです(とても紛らわしい).つまり,ノードがdanglingなシンボリックリンクであることを示します.ftw は,シンボリックリンクを辿った先のファイルやディレクトリに対して proc を適用しようとします.ただし,新たに見つけたノードがdanglingなシンボリックリンクだった場合,node-name にそのシンボリックリンクの名前を渡し,flag に 'symlink を渡します.

proc の返り値が #t かそれ以外かによって ftw 手続きの振る舞いが変化します.返り値が #t だった場合,proc の処理が成功したものと見なして,探索を継続します.一方,#t 以外だった場合,proc の実行中に何らかのエラーが発生したものと見なして,探索を終了します.さらにその場合,ftw は proc の返り値をそのまま返します.

ftw はシンボリックリンクを辿った先のノードに対してprocを適用します.ただし,各ノードに対してprocは一度しか適用しません.

ftw 手続きは,すべてのノードに対する proc の適用が #t を返してきた場合,#t を返します.これは探索が問題なく成功したことを意味します. 一方,あるノードに対する proc の適用が #t 以外の値を返してきた場合,その時点で探索を終了し,proc の返り値をそのまま返します.

▹(注意) Guileのマニュアル[7.12 File Tree Walk] は,proc に関して次のように注意しています.
In the current implementation, returning non-#t from proc is the only valid way to terminate ftw. proc must not use throw or similar to escape.

▹(記録) nftw 手続きと同様に,Guile は,ftw 手続きもライブラリ関数を使わずに独自に実装しています.ただし,その仕様はライブラリ関数の仕様に極力合わせようとしています. 例えば,上記の 'symlink は 'stale-symlink にすべきだと思うのですが(つまり,nftw と ftw で 'symlink の意味を変えるべきではないと思うのですが), ライブラリ関数の仕様が上のようになっているため,それを忠実に再現しているものと思われます.

ftw 手続きの難点はシンボリックリンクを辿ってしまうことです. シンボリックリンクを辿るか否かを proc の内部で判断したい場合がけっこうあると思います.そういった場合,ftw 手続きは根本的に使えません. ソースコードを見ても効率的に大きな差があるとは感じません. 以上のようなことから,ftw 手続きは,nftw 手続きによって置き換えられると言ってもよいと思います.

元々は,POSIXが定めるライブラリ関数の仕様に沿って実現したものだと思います.おそらく,旧い時代への互換性のために残しているのではないかと推測します. ちなみに,POSIXの仕様書[ftw;APPLICATION USAGE] は次のようなことを述べています.
Applications should use the nftw() function instead of the obsolescent ftw() function.

scandir 手続き

(scandir path)
(scandir path select?)
(scandir path select? compare?)

path によって指定されたディレクトリに含まれるノード名(ファイル名やディレクトリ名)からなるリストを返します.リストは string-locale<? をもとにソートされます.

path はディレクトリ名を表す文字列です.

select? は文字列を引数とする述語です. これを指定した場合,select? が真となるノード名だけが選ばれます.

compare? は2つの文字列を比較する述語です. これを指定した場合,string-locale<? の代わりに compare? をもとにソートします.

▹実行例
$ guile
GNU Guile 3.0.5
      ...... 起動メッセージ ......
guile> (system "ls -l /home/algo/temp")
lrwxrwxrwx 1 algo algo   12  2月 15 16:45 somefile.lnk -> somefile.txt
-rw-r--r-- 1 algo algo   22  2月 15 16:44 somefile.txt
drwxr-xr-x 2 algo algo 4096  2月 15 16:40 temp-aaa
drwxr-xr-x 2 algo algo 4096  2月 15 16:40 temp-bbb
$1 = 0
guile> (use-modules (ice-9 ftw))
guile> (scandir "/home/algo/temp")
$2 = ("." ".." "somefile.lnk" "somefile.txt" "temp-aaa" "temp-bbb")
guile> 
(おしまい)