///|
pub(all) enum GlobToken {
Literal(UInt16)
Separator
AnyCharacter
AnySegment
AnyPath
CharacterClass(
chars~ : Array[UInt16],
ranges~ : Array[(UInt16, UInt16)],
negated~ : Bool
)
} derive(Debug, Eq)
///|
pub(all) struct GlobProgram {
source : String
alternatives : Array[Array[GlobToken]]
diagnostics : Array[String]
} derive(Debug, Eq)
///|
fn find_closing_brace(pattern : String, start : Int) -> Int? {
let mut escaped = false
for index in start.. Array[String] {
let items : Array[String] = []
let mut start = 0
let mut escaped = false
for index in 0.. Int? {
if value == "" {
return None
}
let mut sign = 1
let mut index = 0
if value[0] == '-' {
sign = -1
index = 1
}
if index == value.length() {
return None
}
let mut result = 0
while index < value.length() {
let code = value[index]
if code < '0' || code > '9' {
return None
}
result = result * 10 + code.to_int() - ('0' : UInt16).to_int()
index += 1
}
Some(result * sign)
}
///|
fn expand_numeric_range(content : String) -> Array[String]? {
match content.find("..") {
None => None
Some(separator) => {
guard parse_decimal(content[:separator].to_owned()) is Some(start) else {
return None
}
guard parse_decimal(content[separator + 2:].to_owned()) is Some(finish) else {
return None
}
if (start - finish).abs() > 1000 {
return None
}
let values : Array[String] = []
if start <= finish {
for value in start..<=finish {
values.push(value.to_string())
}
} else {
for value in start>=..finish {
values.push(value.to_string())
}
}
Some(values)
}
}
}
///|
fn expand_braces_once(pattern : String) -> Array[String]? {
let mut escaped = false
for index in 0.. values
None => split_brace_items(content)
}
if items.length() <= 1 {
return None
}
let expanded : Array[String] = []
for item in items {
expanded.push(
pattern[:index].to_owned() + item + pattern[end + 1:].to_owned(),
)
}
return Some(expanded)
}
}
None
}
///|
fn expand_braces(pattern : String) -> Array[String] {
let pending : Array[String] = [pattern]
let result : Array[String] = []
let mut expansions = 0
while pending.length() > 0 {
let current = pending.pop().unwrap()
match expand_braces_once(current) {
Some(items) if expansions < 256 => {
expansions += items.length()
for item in items {
pending.push(item)
}
}
_ => result.push(current)
}
}
result
}
///|
fn parse_character_class(
pattern : String,
start : Int,
) -> (GlobToken, Int, String?) {
let mut index = start + 1
let mut negated = false
if index < pattern.length() &&
(pattern[index] == '!' || pattern[index] == '^') {
negated = true
index += 1
}
let chars : Array[UInt16] = []
let ranges : Array[(UInt16, UInt16)] = []
let mut found_end = false
while index < pattern.length() {
if pattern[index] == ']' && (chars.length() > 0 || ranges.length() > 0) {
found_end = true
index += 1
break
}
let first = pattern[index]
if index + 2 < pattern.length() &&
pattern[index + 1] == '-' &&
pattern[index + 2] != ']' {
ranges.push((first, pattern[index + 2]))
index += 3
} else {
chars.push(first)
index += 1
}
}
if found_end {
(CharacterClass(chars~, ranges~, negated~), index, None)
} else {
(Literal('['), start + 1, Some("unclosed character class"))
}
}
///|
fn compile_alternative(pattern : String) -> (Array[GlobToken], Array[String]) {
let tokens : Array[GlobToken] = []
let diagnostics : Array[String] = []
let mut index = 0
while index < pattern.length() {
match pattern[index] {
'/' => {
tokens.push(Separator)
index += 1
}
'*' =>
if index + 1 < pattern.length() && pattern[index + 1] == '*' {
tokens.push(AnyPath)
index += 2
while index < pattern.length() && pattern[index] == '*' {
index += 1
}
} else {
tokens.push(AnySegment)
index += 1
}
'?' => {
tokens.push(AnyCharacter)
index += 1
}
'[' => {
let (token, next, issue) = parse_character_class(pattern, index)
tokens.push(token)
if issue is Some(message) {
diagnostics.push(message)
}
index = next
}
'\\' =>
if index + 1 < pattern.length() {
tokens.push(Literal(pattern[index + 1]))
index += 2
} else {
tokens.push(Literal('\\'))
index += 1
}
code => {
tokens.push(Literal(code))
index += 1
}
}
}
(tokens, diagnostics)
}
///|
pub fn compile_glob(pattern : String) -> GlobProgram {
let alternatives : Array[Array[GlobToken]] = []
let diagnostics : Array[String] = []
for expanded in expand_braces(pattern) {
let (tokens, issues) = compile_alternative(expanded)
alternatives.push(tokens)
for issue in issues {
diagnostics.push(issue)
}
}
{ source: pattern, alternatives, diagnostics }
}
///|
fn class_contains(
code : UInt16,
chars : Array[UInt16],
ranges : Array[(UInt16, UInt16)],
) -> Bool {
if chars.any(item => item == code) {
return true
}
ranges.any(range => {
let (start, finish) = range
if start <= finish {
code >= start && code <= finish
} else {
code >= finish && code <= start
}
})
}
///|
fn match_tokens(
tokens : Array[GlobToken],
path : String,
token_index : Int,
path_index : Int,
failed : Map[String, Bool],
) -> Bool {
let key = token_index.to_string() + ":" + path_index.to_string()
if failed.contains(key) {
return false
}
if token_index == tokens.length() {
return path_index == path.length()
}
let matched = match tokens[token_index] {
Literal(expected) =>
path_index < path.length() &&
path[path_index] == expected &&
match_tokens(tokens, path, token_index + 1, path_index + 1, failed)
Separator =>
path_index < path.length() &&
path[path_index] == '/' &&
match_tokens(tokens, path, token_index + 1, path_index + 1, failed)
AnyCharacter =>
path_index < path.length() &&
path[path_index] != '/' &&
match_tokens(tokens, path, token_index + 1, path_index + 1, failed)
AnySegment => {
let mut cursor = path_index
let mut success = match_tokens(
tokens,
path,
token_index + 1,
cursor,
failed,
)
while !success && cursor < path.length() && path[cursor] != '/' {
cursor += 1
success = match_tokens(tokens, path, token_index + 1, cursor, failed)
}
success
}
AnyPath => {
let mut cursor = path_index
let mut success = match_tokens(
tokens,
path,
token_index + 1,
cursor,
failed,
)
while !success && cursor < path.length() {
cursor += 1
success = match_tokens(tokens, path, token_index + 1, cursor, failed)
}
success
}
CharacterClass(chars~, ranges~, negated~) =>
if path_index >= path.length() || path[path_index] == '/' {
false
} else {
let contains = class_contains(path[path_index], chars, ranges)
(if negated { !contains } else { contains }) &&
match_tokens(tokens, path, token_index + 1, path_index + 1, failed)
}
}
if !matched {
failed[key] = true
}
matched
}
///|
pub fn GlobProgram::matches(self : GlobProgram, path : String) -> Bool {
let normalized = normalize_path(path)
for alternative in self.alternatives {
if match_tokens(alternative, normalized, 0, 0, {}) {
return true
}
}
false
}
///|
pub fn glob_matches(pattern : String, path : String) -> Bool {
let normalized = normalize_path(path)
let subject = if pattern.contains("/") || pattern.contains("\\") {
normalized
} else {
basename(normalized)
}
compile_glob(pattern).matches(subject)
}
///|
pub fn explain_glob(pattern : String, path : String) -> String {
let program = compile_glob(pattern)
if program.diagnostics.length() > 0 {
"模式无效:" + program.diagnostics.join(";")
} else if glob_matches(pattern, path) {
"路径与模式匹配"
} else if !pattern.contains("/") {
"模式只与文件名 `" + basename(path) + "` 比较,结果不匹配"
} else {
"相对路径 `" + normalize_path(path) + "` 与模式不匹配"
}
}