sbt 1.3.0 引入了 Glob
类型,可用于指定文件系统查询。设计灵感来自 shell 通配符。Glob
只有一个公共方法 matches(java.nio.file.Path)
,可用于检查路径是否匹配通配符模式。
通配符可以显式构造,也可以使用使用 /
运算符扩展查询的 DSL。在所有提供的示例中,我们使用 java.nio.file.Path
,但也可以使用 java.io.File
。
最简单的通配符表示单个路径。使用以下方法显式创建单个路径通配符:
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
有两个特殊的通配符对象:1) AnyPath
(由 *
缩写)匹配只有一个名称组件的任何路径 2) RecursiveGlob
(由 **
缩写)匹配所有路径
使用 AnyPath
,我们可以显式构造一个匹配目录中所有子项的通配符
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
递归通配符类似
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 / **
通配符也可以使用路径名称构造。以下三个通配符等效
val pathGlob = Paths.get("foo").resolve("bar")
val glob = Glob("foo/bar")
val altGlob = Glob("foo") / "bar"
解析通配符路径时,任何 /
字符在 Windows 上会自动转换为 \
。
通配符可以在每个路径级别应用名称过滤器。例如,
val scalaSources = Paths.get("/foo/bar").toGlob / ** / "src" / "*.scala"
指定 /foo/bar
的所有后代,这些后代具有 scala
文件扩展名,其父目录名为 src
。
更高级的查询也是可能的
val scalaAndJavaSources =
Paths.get("/foo/bar").toGlob / ** / "src" / "*.{scala,java}"
AnyPath
特殊通配符可用于控制查询的深度。例如,通配符
val twoDeep = Glob("/foo/bar") / * / * / *
匹配 /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
递归通配符无法使用正则表达式语法表示,因为 **
在正则表达式中无效,并且路径是按组件匹配的(因此 "foo/.*/foo.txt"
实际上被拆分为三个正则表达式 {"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
查询文件系统以查找与一个或多个 Glob
模式匹配的文件是通过 sbt.nio.file.FileTreeView
特性完成的。它提供两个方法
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 将以一种方式聚合所有通配符,这样它将始终仅在文件系统上列出任何目录一次。它应该返回所有路径名称与输入 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
参数
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 上,它需要执行 io 以提取 dos:hidden
属性。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"
的通配符匹配以 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)
。sbt 中使用 fileTreeView
键提供了一个 FileTreeView
的实现。
fileTreeView.value.list(baseDirectory.value / ** / "*.txt")
FileTreeView[+T]
特性只有一个抽象方法。
def list(path: Path): Seq[T]
sbt 只提供了 FileTreeView[(Path, FileAttributes)]
的实现。在这种情况下,list
方法应该返回输入 path
的所有直接子项的 (Path, FileAttributes)
对。
sbt 提供了两种 FileTreeView[(Path, FileAttribute)]
的实现: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
。
list
和 iterator
方法以 Glob
或 Seq[Glob]
作为参数,作为 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 长期以来一直拥有 PathFinder api,它提供了一个用于收集文件的 dsl。虽然存在重叠,但 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
将进行两次遍历,因此与 Glob 版本相比,运行时间将大约长一倍。