1. グローブ

グローブ 

sbt 1.3.0では、ファイルシステムクエリを指定するために使用できるGlob型が導入されました。この設計は、シェルのグロブにヒントを得ています。Globには、パスがグロブパターンに一致するかどうかを確認するために使用できる、公開メソッドmatches(java.nio.file.Path)が1つだけあります。

Globの作成 

Globは、明示的に作成することも、/演算子を使用してクエリを拡張するDSLを使用して作成することもできます。提供されているすべての例ではjava.nio.file.Pathを使用していますが、java.io.Fileも使用できます。

最も単純なGlobは単一のパスを表します。単一パスGlobは次のように明示的に作成します。

val glob = Glob(Paths.get("foo/bar"))
println(glob.matches(Paths.get("foo"))) // prints false
println(glob.matches(Paths.get("foo/bar"))) // prints true
println(glob.matches(Paths.get("foo/bar/baz"))) // prints false

グロブDSLを使用して作成することもできます。

val glob = Paths.get("foo/bar").toGlob

2つの特別なGlobオブジェクトがあります。1) AnyPath*の別名)は、名前コンポーネントが1つだけのパスに一致します。2) RecursiveGlob**の別名)はすべてのパスに一致します。

AnyPathを使用して、ディレクトリのすべての子と一致するGlobを明示的に構築できます。

val path = Paths.get("/foo/bar")
val children = Glob(path, AnyPath)
println(children.matches(path)) // prints false
println(children.matches(path.resolve("baz")) // prints true
println(children.matches(path.resolve("baz").resolve("buzz") // prints false

DSLを使用すると、上記は次のようになります。

val children    = Paths.get("/foo/bar").toGlob / AnyPath
val dslChildren = Paths.get("/foo/bar").toGlob / *
// these two definitions have identical results

再帰的なGlobも同様です。

val path = Paths.get("/foo/bar")
val allDescendants = Glob(path, RescursiveGlob)
println(allDescendants.matches(path)) // prints false
println(allDescendants.matches(path.resolve("baz")) // prints true
println(allDescendants.matches(path.resolve("baz").resolve("buzz") // prints true

または

val allDescendants = Paths.get("/foo/bar").toGlob / **

パス名 

Globは、パス名を使用して作成することもできます。次の3つのGlobは同等です。

val pathGlob = Paths.get("foo").resolve("bar")
val glob = Glob("foo/bar")
val altGlob = Glob("foo") / "bar"

Globパスを解析する際、すべての/文字はWindowsでは自動的に\に変換されます。

フィルタ 

Globは、各パスレベルで名前フィルタを適用できます。例えば、

val scalaSources = Paths.get("/foo/bar").toGlob / ** / "src" / "*.scala"

は、親ディレクトリの名前がsrcで、ファイル拡張子がscalaである/foo/barの子孫のすべてを指定します。

より高度なクエリも可能です。

val scalaAndJavaSources =
  Paths.get("/foo/bar").toGlob / ** / "src" / "*.{scala,java}"

深さ 

AnyPath特別なGlobを使用して、クエリの深さを制御できます。例えば、Glob

  val twoDeep = Glob("/foo/bar") / * / * / *

は、親が正確に2つある/foo/barの子孫パスに一致します。例えば、/foo/bar/a/b/c.txtは受け入れられますが、/foo/bar/a/b/foo/bar/a/b/c/d.txtは受け入れられません。

正規表現 

GlobAPIはグロブ構文を使用します(詳細はPathMatcherを参照)。正規表現を使用することもできます。

val digitGlob = Glob("/foo/bar") / ".*-\d{2,3}[.]txt".r
digitGlob.matches(Paths.get("/foo/bar").resolve("foo-1.txt")) // false
digitGlob.matches(Paths.get("/foo/bar").resolve("foo-23.txt")) // true
digitGlob.matches(Paths.get("/foo/bar").resolve("foo-123.txt")) // true

正規表現で複数のパスコンポーネントを指定することもできます。

val multiRegex = Glob("/foo/bar") / "baz-\d/.*/foo.txt"
multiRegex.matches(Paths.get("/foo/bar/baz-1/buzz/foo.txt")) // true
multiRegex.matches(Paths.get("/foo/bar/baz-12/buzz/foo.txt")) // false

再帰的なGlobは、正規表現構文では表現できません。なぜなら、**は正規表現では無効であり、パスはコンポーネントごとにマッチングされるためです(そのため、"foo/.*/foo.txt"は実際にはマッチング目的で3つの正規表現{"foo", ".*", "foo.txt"}に分割されます)。上記のmultiRegexを再帰的にするには、次のように記述できます。

val multiRegex = Glob("/foo/bar") / "baz-\d/".r / ** / "foo.txt"
multiRegex.matches(Paths.get("/foo/bar/baz-1/buzz/foo.txt")) // true
multiRegex.matches(Paths.get("/foo/bar/baz-1/fizz/buzz/foo.txt")) // true

正規表現構文では、\はエスケープ文字であり、パスセパレータとして使用できません。正規表現が複数のパスコンポーネントをカバーする場合、Windowsでもパスセパレータとして/を使用する必要があります。

val multiRegex = Glob("/foo/bar") / "baz-\d/foo\.txt".r
val validRegex = Glob("/foo/bar") / "baz/Foo[.].txt".r
// throws java.util.regex.PatternSyntaxException because \F is not a valid
// regex construct
val invalidRegex = Glob("/foo/bar") / "baz\Foo[.].txt".r

FileTreeViewによるファイルシステムのクエリ 

1つ以上のGlobパターンに一致するファイルをファイルシステムからクエリするには、sbt.nio.file.FileTreeViewトレイトを使用します。2つのメソッドを提供します。

  1. def list(glob: Glob): Seq[(Path, FileAttributes)]
  2. def list(globs: Seq[Glob]): Seq[(Path, FileAttributes)]

これらは、指定されたパターンに一致するすべてのパスを取得するために使用できます。

val scalaSources: Glob = ** / "*.scala"
val regularSources: Glob = "/foo/src/main/scala" / scalaSources
val scala212Sources: Glob = "/foo/src/main/scala-2.12"
val sources: Seq[Path] = FileTreeView.default.list(regularSources).map(_._1)
val allSources: Seq[Path] =
  FileTreeView.default.list(Seq(regularSources, scala212Sources)).map(_._1)

Seq[Glob]を入力として取るバリアントでは、sbtはすべてのGlobを集約して、ファイルシステム上のディレクトリを一度だけリストします。入力Seq[Glob]に含まれるいずれかGlobパターンにパス名が一致するすべてのファイルを返す必要があります。

ファイル属性 

FileTreeViewトレイトは型Tでパラメーター化されており、sbtでは常に(java.nio.file.Path, sbt.nio.file.FileAttributes)です。FileAttributesトレイトは、次のプロパティへのアクセスを提供します。

  1. isDirectoryPathがディレクトリを表す場合にtrueを返します。
  2. isRegularFilePathが通常のファイルを表す場合にtrueを返します。これは通常、isDirectoryの逆になります。
  3. isSymbolicLinkPathがシンボリックリンクの場合にtrueを返します。デフォルトのFileTreeView実装は常にシンボリックリンクに従います。シンボリックリンクが通常のファイルをターゲットとする場合、isSymbolicLinkisRegularFileの両方がtrueになります。同様に、リンクがディレクトリをターゲットとする場合、isSymbolicLinkisDirectoryの両方がtrueになります。リンクが壊れている場合、isSymbolicLinkはtrueになりますが、isDirectoryisRegularFileの両方がfalseになります。

FileTreeViewが常に属性を提供する理由は、ファイルの種類をチェックするにはシステムコールが必要で、時間がかかる可能性があるためです。主要なデスクトップオペレーティングシステムはすべて、ファイル名とファイルノードタイプの両方が返されるディレクトリのリスト作成のためのAPIを提供しています。これにより、sbtは余分なシステムコールを行うことなく、この情報を提供できます。これを効率的にパスをフィルタリングするために使用できます。

// No additional io is performed in the call to attributes.isRegularFile
val scalaSourcePaths =
  FileTreeView.default.list(Glob("/foo/src/main/scala/**/*.scala")).collect {
    case (path, attributes) if attributes.isRegularFile => path
  }

フィルタリング 

上記で説明したlistメソッドに加えて、sbt.nio.file.PathFilter引数を取る2つの追加のオーバーロードがあります。

  1. def list(glob: Glob, filter: PathFilter): Seq[(Path, FileAttributes)]
  2. def list(globs: Seq[Glob], filter: PathFilter): Seq[(Path, FileAttributes)]

PathFilterには、単一の抽象メソッドがあります。

def accept(path: Path, attributes: FileAttributes): Boolean

グロブパターンで指定されたクエリをさらにフィルタリングするために使用できます。

val regularFileFilter: PathFilter = (_, a) => a.isRegularFile
val scalaSourceFiles =
  FileTreeView.list(Glob("/foo/bar/src/main/scala/**/*.scala"), regularFileFilter)

GlobPathFilterとして使用できます。

val filter: PathFilter = ** / "*include*"
val scalaSourceFiles =
  FileTreeView.default.list(Glob("/foo/bar/src/main/scala/**/*.scala"), filter)

PathFilterのインスタンスは、!単項演算子で否定できます。

val hiddenFileFilter: PathFilter = (p, _) => Try(Files.isHidden(p)).getOrElse(false)
val notHiddenFileFilter: PathFilter = !hiddenFileFilter

これらは&&演算子で組み合わせることができます。

val regularFileFilter: PathFilter = (_, a) => a.isRegularFile
val notHiddenFileFilter: PathFilter = (p, _) => Try(Files.isHidden(p)).getOrElse(false)
val andFilter = regularFileFilter && notHiddenFileFilter
val scalaSources =
  FileTreeView.default.list(Glob("/foo/bar/src/main/scala/**/*.scala"), andFilter)

これらは||演算子で組み合わせることができます。

val scalaSources: PathFilter = ** / "*.scala"
val javaSources: PathFilter = ** / "*.java"
val jvmSourceFilter = scalaSources || javaSources
val jvmSourceFiles =
  FileTreeView.default.list(Glob("/foo/bar/src/**"), jvmSourceFilter)

StringからPathFilterへの暗黙の変換もあり、StringGlobに変換し、GlobPathFilterに変換します。

val regularFileFilter: PathFilter = (p, a) => a.isRegularFile
val regularScalaFiles: PathFilter = regularFileFilter && "**/*.scala"

アドホックフィルタに加えて、デフォルトのsbtスコープで使用できるいくつかの一般的なフィルタがあります。

  1. sbt.io.HiddenFileFilterFiles.isHiddenに従って非表示になっているファイルを受け入れます。POSIXシステムでは、名前が.で始まるかどうかをチェックするだけで済みますが、Windowsでは、dos:hidden属性を抽出するためにIOを実行する必要があります。
  2. sbt.io.RegularFileFilter(_, a: FileAttributes) => a.isRegularFileと同等です。
  3. sbt.io.DirectoryFilter(_, a: FileAttributes) => a.isDirectory と同等です。

sbt.io.FileFilter から sbt.nio.file.PathFilter へのコンバータもあり、sbt.io.FileFilter インスタンスの toNio メソッドを呼び出すことで使用できます。

val excludeFilter: sbt.io.FileFilter = HiddenFileFilter || DirectoryFilter
val excludePathFilter: sbt.nio.file.PathFilter = excludeFilter.toNio

HiddenFileFilterRegularFileFilterDirectoryFilter は、sbt.io.FileFiltersbt.nio.file.PathFilter の両方を継承しています。通常は PathFilter として扱うことができます。

val regularScalaFiles: PathFilter = RegularFileFilter && (** / "*.scala")

String から PathFinder への暗黙的な変換が必要な場合、これは機能しません。

 val regularScalaFiles = RegularFileFilter && "**/*.scala"
// won't compile because it gets interpreted as
// (RegularFileFilter: sbt.io.FileFilter).&&(("**/*.scala"): sbt.io.NameFilter)

このような状況では、toNio を使用してください。

 val regularScalaFiles = RegularFileFilter.toNio && "**/*.scala"

Glob のセマンティクスは NameFilter と異なることに注意することが重要です。sbt.io.FileFilter を使用して、.scala 拡張子で終わるファイルをフィルタリングするには、次のように記述します。

val scalaFilter: NameFilter = "*.scala"

同等の PathFilter は次のように記述されます。

val scalaFilter: PathFilter = "**/*.scala"

"*.scala" で表される glob は、scala で終わる単一コンポーネントを持つパスに一致します。一般的に、sbt.io.NameFiltersbt.nio.file.PathFilter に変換する際には、"**/" プレフィックスを追加する必要があります。

ストリーミング 

FileTreeView.listに加えて、FileTreeView.iteratorもあります。後者はメモリ圧力を軽減するために使用できます。

// Prints all of the files on the root file system
FileTreeView.iterator(Glob("/**")).foreach { case (p, _) => println(p) }

sbt のコンテキストでは、型パラメータ T は常に (java.nio.file.Path, sbt.nio.file.FileAttributes) です。FileTreeView の実装は、sbt で fileTreeView キーを使用して提供されます。

fileTreeView.value.list(baseDirectory.value / ** / "*.txt")

実装 

FileTreeView[+T] トレイトには、1 つの抽象メソッドがあります。

def list(path: Path): Seq[T]

sbt は FileTreeView[(Path, FileAttributes)] の実装のみを提供します。このコンテキストでは、list メソッドは、入力 path のすべての直接の子に関する (Path, FileAttributes) のペアを返す必要があります。

sbt によって提供される FileTreeView[(Path, FileAttribute)] の実装は 2 つあります。1. FileTreeView.native — これはネイティブ jni ライブラリを使用して、追加の IO を実行せずにファイルシステムからファイル名と属性を効率的に抽出します。ネイティブ実装は、64 ビット FreeBSD、Linux、Mac OS、Windows で利用できます。ネイティブ実装が利用できない場合は、java.nio.file ベースの実装にフォールバックします。2. FileTreeView.niojava.nio.file の API を使用して FileTreeView を実装します。

FileTreeView.default メソッドは FileTreeView.native を返します。

Glob または Seq[Glob] を引数として取る list メソッドと iterator メソッドは、FileTreeView[(Path, FileAttributes)] への拡張メソッドとして提供されています。FileTreeView[(Path, FileAttributes)] の実装は自動的にこれらの拡張機能を受け取るため、GlobSeq[Glob] と引き続き正しく動作する代替実装を簡単に記述できます。

val listedDirectories = mutable.Set.empty[Path]
val trackingView: FileTreeView[(Path, FileAttributes)] = path => {
  val results = FileTreeView.default.list(path)
  listedDirectories += path
  results
}
val scalaSources =
  trackingView.list(Glob("/foo/bar/src/main/scala/**/*.scala")).map(_._1)
println(listedDirectories) // prints all of the directories traversed by list

Glob 対 PathFinder 

sbt は長年にわたり、ファイルを収集するための DSL を提供する PathFinder API を持っています。重複部分はありますが、Glob は PathFinder よりも強力な抽象化ではありません。そのため、最適化に適しています。Glob はクエリの内容を記述しますが、方法は記述しません。PathFinder は内容と方法の両方を組み合わせるため、最適化が困難です。たとえば、次の sbt スニペットは

val paths = fileTreeView.value.list(
    baseDirectory.value / ** / "*.scala",
    baseDirectory.value / ** / "*.java").map(_._1)

プロジェクト内のすべての scala と java のソースを収集するために、ファイルシステムを一度だけトラバースします。これとは対照的に、

val paths =
    (baseDirectory.value ** "*.scala" +++
     baseDirectory.value ** "*.java").allPaths

2 回パスを行い、Glob バージョンと比較して約 2 倍の時間がかかります。