1. 通配符

通配符 

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

使用 FileTreeView 查询文件系统 

查询文件系统以查找与一个或多个 Glob 模式匹配的文件是通过 sbt.nio.file.FileTreeView 特性完成的。它提供两个方法

  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 将以一种方式聚合所有通配符,这样它将始终仅在文件系统上列出任何目录一次。它应该返回所有路径名称与输入 Seq[Glob] 中提供的任何Glob 模式匹配的文件。

文件属性 

FileTreeView 特性由一个类型 T 参数化,该类型在 sbt 中始终是 (java.nio.file.Path, sbt.nio.file.FileAttributes)FileAttributes 特性提供对以下属性的访问

  1. isDirectory - 如果 Path 表示目录,则返回 true。
  2. isRegularFile - 如果 Path 表示常规文件,则返回 true。这通常应该与 isDirectory 相反。
  3. isSymbolicLink - 如果 Path 是符号链接,则返回 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 参数

  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)

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)

还存在从 StringPathFilter 的隐式转换,它将 String 转换为 Glob 并将 Glob 转换为 PathFilter

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

除了特设过滤器外,在默认 sbt 范围内还提供了一些常用过滤器

  1. sbt.io.HiddenFileFilter - 接受根据 Files.isHidden 隐藏的任何文件。在 posix 系统上,这只会检查名称是否以 . 开头,而在 Windows 上,它需要执行 io 以提取 dos:hidden 属性。
  2. sbt.io.RegularFileFilter - 等效于 (_, a: FileAttributes) => a.isRegularFile
  3. sbt.io.DirectoryFilter - 等效于 (_, a: FileAttributes) => a.isDirectory

还存在从 sbt.io.FileFiltersbt.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")

当需要从 StringPathFinder 的隐式转换时,这将不起作用。

 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

listiterator 方法以 GlobSeq[Glob] 作为参数,作为 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 长期以来一直拥有 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 版本相比,运行时间将大约长一倍。