sbt 1.3.0では、ファイルシステムクエリを指定するために使用できるGlob
型が導入されました。この設計は、シェルのグロブにヒントを得ています。Glob
には、パスがグロブパターンに一致するかどうかを確認するために使用できる、公開メソッドmatches(java.nio.file.Path)
が1つだけあります。
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
は受け入れられません。
Glob
APIはグロブ構文を使用します(詳細は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
1つ以上のGlob
パターンに一致するファイルをファイルシステムからクエリするには、sbt.nio.file.FileTreeView
トレイトを使用します。2つのメソッドを提供します。
def list(glob: Glob): Seq[(Path, FileAttributes)]
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
トレイトは、次のプロパティへのアクセスを提供します。
isDirectory
— Path
がディレクトリを表す場合にtrueを返します。isRegularFile
— Path
が通常のファイルを表す場合にtrueを返します。これは通常、isDirectory
の逆になります。isSymbolicLink
— Path
がシンボリックリンクの場合にtrueを返します。デフォルトのFileTreeView
実装は常にシンボリックリンクに従います。シンボリックリンクが通常のファイルをターゲットとする場合、isSymbolicLink
とisRegularFile
の両方がtrueになります。同様に、リンクがディレクトリをターゲットとする場合、isSymbolicLink
とisDirectory
の両方がtrueになります。リンクが壊れている場合、isSymbolicLink
はtrueになりますが、isDirectory
とisRegularFile
の両方が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つの追加のオーバーロードがあります。
def list(glob: Glob, filter: PathFilter): Seq[(Path, FileAttributes)]
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)
Glob
はPathFilter
として使用できます。
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
への暗黙の変換もあり、String
をGlob
に変換し、Glob
をPathFilter
に変換します。
val regularFileFilter: PathFilter = (p, a) => a.isRegularFile
val regularScalaFiles: PathFilter = regularFileFilter && "**/*.scala"
アドホックフィルタに加えて、デフォルトのsbtスコープで使用できるいくつかの一般的なフィルタがあります。
sbt.io.HiddenFileFilter
— Files.isHidden
に従って非表示になっているファイルを受け入れます。POSIXシステムでは、名前が.
で始まるかどうかをチェックするだけで済みますが、Windowsでは、dos:hidden
属性を抽出するためにIOを実行する必要があります。sbt.io.RegularFileFilter
— (_, a: FileAttributes) => a.isRegularFile
と同等です。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
HiddenFileFilter
、RegularFileFilter
、DirectoryFilter
は、sbt.io.FileFilter
と sbt.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.NameFilter
を sbt.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.nio
— java.nio.file
の API を使用して FileTreeView
を実装します。
FileTreeView.default
メソッドは FileTreeView.native
を返します。
Glob
または Seq[Glob]
を引数として取る list
メソッドと iterator
メソッドは、FileTreeView[(Path, FileAttributes)]
への拡張メソッドとして提供されています。FileTreeView[(Path, FileAttributes)]
の実装は自動的にこれらの拡張機能を受け取るため、Glob
と Seq[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
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 倍の時間がかかります。