1. Globs

Globs 

sbt 1.3.0 引入了 Glob 類型,可用於指定檔案系統查詢。此設計的靈感來自 Shell 的globGlob 只有一個公用方法 matches(java.nio.file.Path),可用於檢查路徑是否符合 glob 模式。

建構 Globs 

Globs 可以明確建構,或使用 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

也可以使用 glob DSL 建立:

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

有兩個特殊的 glob 物件:1) AnyPath(別名為 *)符合僅具有一個名稱元件的任何路徑。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 / **

路徑名稱 

Globs 也可以使用路徑名稱建構。以下三個 globs 是等效的:

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

剖析 glob 路徑時,任何 / 字元都會在 Windows 上自動轉換為 \

篩選器 

Globs 可以在每個路徑層級套用名稱篩選器。例如:

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

指定 /foo/bar 的所有後代,這些後代具有 scala 副檔名且其父目錄名為 src

也可以使用更進階的查詢:

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

深度 

AnyPath 特殊 glob 可用於控制查詢的深度。例如,glob:

  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 使用 glob 語法(詳細資訊請參閱 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

遞迴 globs 無法使用規則運算式語法來表示,因為 ** 在規則運算式中無效,而且路徑是按元件比對(因此 "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 會以僅列出檔案系統上的任何目錄一次的方式來彙總所有 globs。它應該傳回其路徑名稱符合輸入 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

可用於進一步篩選 glob 模式所指定的查詢:

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 上,則需要執行 I/O 操作來提取 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" 表示的 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)。sbt 中提供了一個 FileTreeView 的實作,並使用 fileTreeView 鍵。

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

實作 

FileTreeView[+T] trait 具有單一抽象方法。

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

sbt 僅提供 FileTreeView[(Path, FileAttributes)] 的實作。在此上下文中,list 方法應返回輸入 path 所有直接子項的 (Path, FileAttributes) 配對。

sbt 提供了兩種 FileTreeView[(Path, FileAttribute)] 的實作:1. FileTreeView.native — 它使用原生 JNI 程式庫,從檔案系統中有效率地提取檔案名稱和屬性,而無需執行額外的 I/O 操作。原生實作適用於 64 位元的 FreeBSD、Linux、Mac OS 和 Windows。如果沒有可用的原生實作,它會回退到基於 java.nio.file 的實作。2. FileTreeView.nio — 使用 java.nio.file 中的 API 來實作 FileTreeView

FileTreeView.default 方法返回 FileTreeView.native

GlobSeq[Glob] 作為參數的 listiterator 方法作為 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

Globs vs. PathFinder 

sbt 長期以來一直擁有 PathFinder API,它提供了一種用於收集檔案的 DSL。雖然有重疊之處,但 Globs 是一個比 PathFinder 功能較弱的抽象概念。這使得它們更適合優化。Globs 描述查詢的內容,而不是查詢的方式。PathFinders 結合了查詢的內容和方式,這使得它們更難以優化。例如,以下 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 版本相比,執行時間大約會長一倍。