倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Groovy JDK? It's GDK! (File 編7) traverse() 再訪

今回は、以前の記事で簡単に見た traverse() メソッドをもう少しきちんと見て行きます(目次)。

参考 URL

オーバーロードされた3つの traverse()

まずは traverse() メソッドのオーバーロードされているシグニチャを見てみましょう:

void traverse(Map options)
void traverse(Closure closure)
void traverse(Map options, Closure closure) 

Map のみを引数にとる traverse() は、何の処理もなく何の値も返さないので一見無意味そうですが、あとで見るようにオプションを表す引数の Map に preDir, postDir というキーで Closure による処理を渡せます。

Closure のみを引数にとる traverse() はすべてのファイル・ディレクトリを走査して、引数の Closure の処理を行います。 走査する順序は深さ優先 (in a depth-first fashion) だそうです。 ただし、ちょっと試してみたところ

  • サブディレクトリよりも親ディレクトリの方が先に走査
  • 1つのディレクトリ下ではファイルよりもディレクトリが先に走査

といった感じです。

Map と Closure を引数にとる traverse() はオプションを表す Map によって指定された走査方法に基づいてファイル・ディレクトリを列挙し、それぞれに対して引数の Closure の処理を実行します。

オプション

次は引数の Map オブジェクトに指定できる項目を見ていきましょう。 これらはディレクトリ階層を走査する方法を指定するオプションという位置づけです。

名前 値の型 デフォルト値 説明
type FileType ANY 走査するファイルの種類
preDir Closure ディレクトリを走査する際の前処理
preRoot boolean false preDir の処理をルートディレクトリに対しても行うかどうか
postDir Closure ディレクトリを走査する際の後処理
postRoot boolean false postDir の処理をルートディレクトリに対しても行うかどうか
visitRoot boolean false Closure の処理をルートディレクトリに対しても行うかどうか
maxDepth int -1(infinite) 再帰を行うディレクトリ階層の深さ
filter Object 走査するファイルに対するフィルター
nameFilter Object 走査するファイルの名前に対するフィルター
excludeFilter Object 走査しないファイルに対するフィルター
excludeNameFilter Object 走査しないファイルの名前に対するフィルター
sort Closure 同一ディレクトリ内のファイル、ディレクトリを操作する順序
  • FileType は groovy.io.FileType です
  • type を指定しない場合は、ファイル、ディレクトリの両方が走査対象になります
  • preDir, postDir に渡す Closure は groovy.io.FileVisitResult オブジェクトを返すことによって、走査を終了 (FileVisitResult.TERMINATE) したり、サブディレクトリの走査をスキップ (FileVisitResult.SKIP_SUBTREE) したり、同ディレクトリ内の走査をスキップ (FileVisitResult.SKIP_SIBLINGS) したりすることができます*1
  • filter, nameFilter, excludeFilter, excludeNameilter の型は Object になっていますが、isCase() メソッドが呼べる(switch 文の case に指定できる・・・って感じ)ものであることが想定されています

groovy.io.FileType, groovy.io.FileVisitResult には、以下のような定数が定義されています:

package groovy.io

enum FileType{
    FILES,
    DIRECTORIES,
    ANY
}

enum FileVisitResult{
    CONTINUE,
    SKIP_SIBLINGS,
    SKIP_SUBTREE,
    TERMINATE
}

FileVisitResult は、java.nio.file にもほぼ同じ列挙型が定義されてますね。 まぁ、Visitor パターンの実装だから特に不思議でもないですが。

サンプルコード

以上を踏まえていくつかサンプルコードを見ていきましょう。 以前に java.nio の記事で扱ったサンプルを焼き直します(元ネタは Java のチュートリアルですが)。 まずは適当にディレクトリ階層を作っておきます。 もしサンプルを動かしたい場合はどうぞ:

// ***** 準備 *****
def ant = new AntBuilder()
ant.with{
    mkdir dir :'src/org/sample'
    mkdir dir :'src/org/sample/child1'
    mkdir dir :'src/org/sample/child2'
    mkdir dir :'src/org/sample/child2/grandchild2'

    touch file:'src/org/sample/Main.java'
    touch file:'src/org/sample/Sub.java'
    touch file:'src/org/sample/child1/Child11.java'
    touch file:'src/org/sample/child1/Child12.java'
    touch file:'src/org/sample/child1/Child1.groovy'
    touch file:'src/org/sample/child2/Child21.java'
    touch file:'src/org/sample/child2/Child22.java'
    touch file:'src/org/sample/child2/Child2.groovy'
    touch file:'src/org/sample/child2/grandchild2/Child22.groovy'
}

ファイルの内容は全て空です。 もうちょっとスマートに書けるでしょうけど、まぁあまり気にしないで下さい。 生成されるディレクトリ階層とファイルは以下のようになります:

.
  +src/
    +org/
      +sample/
        +Main.java
        +Sub.java
        +child1/
          +Child11.java
          +Child12.java
          +Child1.groovy
        +child2/
          +Child21.java
          +Child22.java
          +Child2.groovy
          +grandchild2/
            +Child22.groovy

これを踏まえて、いざサンプルコードへ。

traverse(Closure)
オプション指定なしの traverse() :

// find
new File('src').traverse{ println "[FIND] $it" }

出力結果は以下のようになります:

[FIND] src\org
[FIND] src\org\sample
[FIND] src\org\sample\child1
[FIND] src\org\sample\child1\Child1.groovy
[FIND] src\org\sample\child1\Child11.java
[FIND] src\org\sample\child1\Child12.java
[FIND] src\org\sample\child2
[FIND] src\org\sample\child2\Child2.groovy
[FIND] src\org\sample\child2\Child21.java
[FIND] src\org\sample\child2\Child22.java
[FIND] src\org\sample\child2\grandchild2
[FIND] src\org\sample\child2\grandchild2\Child22.groovy
[FIND] src\org\sample\Main.java
[FIND] src\org\sample\Sub.java
  • 同一ディレクトリ内なら、ファイルよりもサブディレクトリの方が先に走査されています
  • 深さ優先 (depth-first) で走査

filter
次は filter オプション:

// find
new File('src').traverse(filter:{ it.isDirectory() }){ println "[FIND] $it" }

「type:DIRECTORIES」 を指定すればいいじゃん、っていう指摘は無視w flter の値に指定する Closure には File オブジェクトが渡されます。 結果は以下の通り:

[FIND] src\org
[FIND] src\org\sample
[FIND] src\org\sample\child1
[FIND] src\org\sample\child2
[FIND] src\org\sample\child2\grandchild2

なんてことないですね。

nameFilter
次は filter に似た nameFilter オプション。 filter との違いは、ファイル名(ディレクトリ名)の String に対するフィルターだというところ:

// find
new File('src').traverse(type:FILES, nameFilter:~/.*\.java/){ println "[FIND] $it" }

ここでは正規表現を用いて「.java」という拡張子をもったファイルのみ走査しています。 出力結果は以下の通り:

[FIND] src\org\sample\child1\Child11.java
[FIND] src\org\sample\child1\Child12.java
[FIND] src\org\sample\child2\Child21.java
[FIND] src\org\sample\child2\Child22.java
[FIND] src\org\sample\Main.java
[FIND] src\org\sample\Sub.java

preDir, preRoot
preDir オプションは各ディレクトリを走査する際に行う前処理を指定します。 preRoot オプションはルートディレクトリに対して同じ前処理を行うかどうかを boolean 値によって指定します。 デフォルトは false なので注意。 サンプルでは、src ディレクトリ下の内容を dest ディレクトリにコピーしています:

def toDest = { dir -> new File(dir.toString().replace('src', 'dest')) }    // コピー先の File オブジェクトを返す

// copy
new File('src').traverse(type:FILES, preDir:{ dir -> toDest(dir).mkdir() }, preRoot:true){ file ->
    def copy = toDest(file)
    copy.createNewFile()
    println "[COPY] $copy"
    //copy.bytes = file.bytes    // ファイルに内容がないので意味がないよう
}

traverse() の引数の Closure ではファイルのみをコピーするようにして、ディレクトリの作成は preDir オプションによって行っています(ファイルコピーのにディレクトリ作成しないといけないので)。 ルートディレクトリも作成する必要があるので「preRoot:true」を指定しています。 traverse() の引数の Closure でファイルかディレクトリかを場合分けするよりもスマートに書けてるんじゃないかと。 出力結果は以下の通り:

[COPY] dest\org\sample\child1\Child1.groovy
[COPY] dest\org\sample\child1\Child11.java
[COPY] dest\org\sample\child1\Child12.java
[COPY] dest\org\sample\child2\Child2.groovy
[COPY] dest\org\sample\child2\Child21.java
[COPY] dest\org\sample\child2\Child22.java
[COPY] dest\org\sample\child2\grandchild2\Child22.groovy
[COPY] dest\org\sample\Main.java
[COPY] dest\org\sample\Sub.java

postDir, postRoot
今度は各ディレクトリを走査する祭の後処理を行う postDir, postRoot。 使い方は preDir, preRoot と同じです。 ここでは先ほど作成したコピー(dest ディレクトリ下)を削除しています:

// delete
new File('dest').traverse(type:FILES, postDir:{ dir -> dir.delete() }, postRoot:true){ file ->
    file.delete()
    println "[DELETE] $file.canonicalPath"
}

File.delete() はディレクトリの場合にはサブファイルやサブディレクトリがあると削除が失敗するので、まずそれらのサブファイル、サブディレクトリを削除した後、postDir でディレクトリの後処理としてそのディレクトリを削除しています。 ルートディレクトリも削除したいので「postRoot:true」を指定しています。 出力結果は以下の通り:

[DELETE] dest\org\sample\child1\Child1.groovy
[DELETE] dest\org\sample\child1\Child11.java
[DELETE] dest\org\sample\child1\Child12.java
[DELETE] dest\org\sample\child2\Child2.groovy
[DELETE] dest\org\sample\child2\Child21.java
[DELETE] dest\org\sample\child2\Child22.java
[DELETE] dest\org\sample\child2\grandchild2\Child22.groovy
[DELETE] dest\org\sample\Main.java
[DELETE] dest\org\sample\Sub.java

visitRoot
visitRoot オプションはルートディレクトリを走査するかどうかを boolean 値によって指定します:

new File('src').traverse(visitRoot:true){ println "[FIND] $it" }

出力結果は以下のようになります:

[FIND] src\org
[FIND] src\org\sample
[FIND] src\org\sample\child1
[FIND] src\org\sample\child1\Child1.groovy
[FIND] src\org\sample\child1\Child11.java
[FIND] src\org\sample\child1\Child12.java
[FIND] src\org\sample\child2
[FIND] src\org\sample\child2\Child2.groovy
[FIND] src\org\sample\child2\Child21.java
[FIND] src\org\sample\child2\Child22.java
[FIND] src\org\sample\child2\grandchild2
[FIND] src\org\sample\child2\grandchild2\Child22.groovy
[FIND] src\org\sample\Main.java
[FIND] src\org\sample\Sub.java
[FIND] src

ルートディレクトリの走査はなぜか一番最後になってますね。

maxDepth
maxDepth は走査するディレクトリ階層の深さを指定します:

// find
new File('src').traverse(maxDepth:2){ println "[FIND] $it" }

ルートから数えて、指定した値のサブフォルダ、サブディレクトリまで走査されるようです:

[FIND] src\org
[FIND] src\org\sample
[FIND] src\org\sample\child1
[FIND] src\org\sample\child2
[FIND] src\org\sample\Main.java
[FIND] src\org\sample\Sub.java

maxDepth に0を指定すると再帰走査を行わない(ルートディレクトリのサブディレクトリ下を走査しない)という指定と同じになります。 GDK が File クラスに追加しているメソッド eachFile(), eachFileMatch() などはこれと同じだと思えます(実装は違うでしょうけど)。

あとは excludeFilter, excludeNameFilter, sort が残ってますが

  • excludeFilter, excludeNameFilter は filter, nameFilter と使い方は同じ(除外するファイル、もしくはファイル名を指定する)
  • sort はサンプルが「Groovy JDK」 の File#traverse(Map, Closure) に載っているので、興味のある方はそちらをどうぞ

という感じで。 traverse() 使えると結構融通が利きますが、知らないとコードを書くのも読むのも大変そう。

これでとりあえず GDK の File 編は終了。

プログラミングGROOVY

プログラミングGROOVY

  • 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/07/06
  • メディア: 単行本(ソフトカバー)
  • 購入: 6人 クリック: 392回
  • この商品を含むブログ (155件) を見る
Groovyイン・アクション

Groovyイン・アクション

  • 作者: Dierk Konig,Andrew Glover,Paul King,Guillaume Laforge,Jon Skeet,杉浦孝,櫻井正樹,須江信洋,関谷和愛,佐野徹郎,寺沢尚史
  • 出版社/メーカー: 毎日コミュニケーションズ
  • 発売日: 2008/09/27
  • メディア: 単行本(ソフトカバー)
  • 購入: 5人 クリック: 146回
  • この商品を含むブログ (121件) を見る

*1:これは traverse() の引数に渡せる Closure についても同じ。