sbt 1.3.0 引入了 Glob
類型,可用於指定檔案系統查詢。此設計的靈感來自 Shell 的glob。Glob
只有一個公用方法 matches(java.nio.file.Path)
,可用於檢查路徑是否符合 glob 模式。
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
使用一個或多個 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 會以僅列出檔案系統上的任何目錄一次的方式來彙總所有 globs。它應該傳回其路徑名稱符合輸入 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
可用於進一步篩選 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)
還有從 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 上,則需要執行 I/O 操作來提取 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"
表示的 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
。
以 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 長期以來一直擁有 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 版本相比,執行時間大約會長一倍。