Glob pattern matcher (#122)

This commit is contained in:
清风之恋
2025-05-19 14:53:41 +08:00
committed by GitHub
parent 9c18b7abd9
commit 16603ee34a
4 changed files with 1165 additions and 1 deletions

View File

@@ -0,0 +1,119 @@
#pragma once
#include <expected>
#include <string>
#include <bitset>
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
using GlobCharSet = std::bitset<256>;
/// This class implements a glob pattern matcher to parse patterns to
/// watch relative to the base path.
///
/// Glob patterns can have the following syntax:
/// - `*` to match one or more characters in a path segment
/// - `?` to match on one character in a path segment
/// - `**` to match any number of path segments, including none
/// - `{}` to group conditions (e.g. `***.{ts,js}` matches all TypeScript
/// and JavaScript files)
/// - `[]` to declare a range of characters to match in a path segment
/// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
/// - `[!...]` to negate a range of characters to match in a path segment
/// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`,
/// but not `example.0`)
///
/// Note: Use only `/` for path segment seperator
class GlobPattern {
public:
/// \param Pat the pattern to match against
/// \param MaxSubPatterns if provided limit the number of allowed subpatterns
/// created from expanding braces otherwise disable
/// brace expansion
static std::expected<GlobPattern, std::string>
create(llvm::StringRef s, std::optional<size_t> max_subpattern_num = {});
// Returns true for glob pattern "*" or "**". Can be used to avoid expensive
// preparation/acquisition of the input for match().
bool isTrivialMatchAll() const {
if(!prefix.empty()) {
// have no prefix
return false;
}
if((sub_globs.size() == 1 && sub_globs[0].getPat() == "*") ||
(sub_globs.size() == 2 && sub_globs[0].getPat() == "**")) {
// not "*" or "**"
return true;
}
// default return false
return false;
}
/// \returns \p true if \p str matches this glob pattern
bool match(llvm::StringRef s);
private:
/// GlobPattern is seperated into `Prefix + SubGlobPattern`
std::string prefix;
/// if prefix contains full path:
///
/// xxx/yyy/*.c
/// ~~~~~~^
///
/// prefix contains incomplete path:
/// xxx/yyy*.c
/// ~~~~~~^
///
bool prefix_at_seg_end = false;
/// SubGlobPattern:
/// Pattern `foo.{c,cpp,cppm}`
/// -> extend to 3 SubGlobPatterns: `foo.c`, `foo.cpp`, `foo.cppm`
struct SubGlobPattern {
/// \param Pat the pattern to match against
static std::expected<SubGlobPattern, std::string> create(llvm::StringRef s);
/// \returns \p true if \p S matches this glob pattern
bool match(llvm::StringRef str) const;
llvm::StringRef getPat() const {
return llvm::StringRef{pat.data(), pat.size()};
}
struct Bracket {
size_t next_offset;
GlobCharSet bytes;
};
llvm::SmallVector<Bracket, 0> brackets;
// GlobSegment devide patterm into segments by '/'
// SubPattern:
// **include/aaa/bbb/test[0-9].cc
// ^~~~~~~~~^~~~^~~~^~~~~~~~~~~~~^
// 1 2 3 4
// Devided into 4 segments
//
// SubPattern:
// **/include/aaa/bbb/test[0-9].h
// ^~^~~~~~~~^~~~^~~~^~~~~~~~~~~~^
// 1 2 3 4 5
// Devided into 5 segments
struct GlobSegment {
size_t start;
size_t end;
};
llvm::SmallVector<GlobSegment, 6> glob_segments;
llvm::SmallVector<char, 0> pat;
};
llvm::SmallVector<SubGlobPattern, 1> sub_globs;
};
} // namespace clice

562
src/Support/GlobPattern.cpp Normal file
View File

@@ -0,0 +1,562 @@
#include <format>
#include "Support/GlobPattern.h"
namespace clice {
// Expands character ranges and returns a bitmap.
// For example, "a-cf-hz" is expanded to "abcfghz".
static std::expected<GlobCharSet, std::string> expand(llvm::StringRef s, llvm::StringRef original) {
GlobCharSet bv{false};
for(size_t i = 0, e = s.size(); i < e; ++i) {
switch(s[i]) {
case '\\': {
++i;
if(i == e) {
return std::unexpected{"Invalid expansions: stary `\\`"};
}
if(s[i] != '/') {
bv.set(s[i], true);
}
break;
}
case '-': {
if(i == 0 || i + 1 == e) {
bv.set('-', true);
break;
}
char c_begin = s[i - 1];
char c_end = s[i + 1];
++i;
if(c_end == '\\') {
++i;
if(s[i] == e) {
return std::unexpected{"Invalid expansions: stary `\\`"};
}
c_end = s[i];
}
if(c_begin > c_end) {
return std::unexpected{
std::format("Invalid expansion: `{}` is larger than `{}`", c_begin, c_end)};
}
for(char c = c_begin; c <= c_end; ++c) {
if(c != '/') {
bv.set(c, true);
}
}
break;
}
default: {
if(s[i] != '/') {
bv.set(s[i], true);
}
}
}
}
return bv;
}
// Identify brace expansions in S and return the list of patterns they expand
// into.
static std::expected<llvm::SmallVector<std::string, 1>, std::string>
parseBraceExpansions(llvm::StringRef s, std::optional<size_t> max_subpattern_num) {
llvm::SmallVector<std::string> subpatterns = {s.str()};
if(!max_subpattern_num || !s.contains('{')) {
return subpatterns;
}
struct BraceExpansion {
size_t start;
size_t length;
llvm::SmallVector<llvm::StringRef, 2> terms;
};
llvm::SmallVector<BraceExpansion, 0> brace_expansions;
BraceExpansion* current_be = nullptr;
size_t term_begin = 0;
for(size_t i = 0, e = s.size(); i != e; ++i) {
if(s[i] == '[') {
++i;
if(s[i] == ']') {
++i;
}
while(i != e && s[i] != ']') {
if(s[i] == '\\') {
if(i == e) {
return std::unexpected{
"Invalid glob pattern, unmatched '[', with stray " "'\\' inside"};
}
++i;
}
++i;
}
if(i == e) {
return std::unexpected{"Invalid glob pattern, unmatched '['"};
}
} else if(s[i] == '{') {
if(current_be) {
return std::unexpected{"Nested brace expansions are not supported"};
}
current_be = &brace_expansions.emplace_back();
current_be->start = i;
term_begin = i + 1;
} else if(s[i] == ',') {
if(!current_be) {
continue;
}
current_be->terms.push_back(s.substr(term_begin, i - term_begin));
term_begin = i + 1;
} else if(s[i] == '}') {
if(!current_be) {
continue;
}
if(current_be->terms.empty() && i - term_begin == 0) {
return std::unexpected{"Empty brace expression is not supported"};
}
current_be->terms.push_back(s.substr(term_begin, i - term_begin));
current_be->length = i - current_be->start + 1;
current_be = nullptr;
} else if(s[i] == '\\') {
++i;
if(i == e) {
return std::unexpected{"Invalid glob pattern, stray '\\'"};
}
}
}
if(current_be) {
return std::unexpected{"Incomplete brace expansion"};
}
size_t subpattern_num = 1;
for(auto& be: brace_expansions) {
if(subpattern_num > std::numeric_limits<size_t>::max() / be.terms.size()) {
subpattern_num = std::numeric_limits<size_t>::max();
break;
}
subpattern_num *= be.terms.size();
}
if(subpattern_num > *max_subpattern_num) {
return std::unexpected{"Too many brace expansions"};
}
for(auto& be: reverse(brace_expansions)) {
llvm::SmallVector<std::string> orig_sub_patterns;
std::swap(subpatterns, orig_sub_patterns);
for(llvm::StringRef term: be.terms) {
for(llvm::StringRef orig: orig_sub_patterns) {
subpatterns.emplace_back(orig).replace(be.start, be.length, term);
}
}
}
return subpatterns;
}
std::expected<GlobPattern, std::string>
GlobPattern::create(llvm::StringRef s, std::optional<size_t> max_subpattern_num) {
// Store the prefix that does not contain any metacharacter.
GlobPattern pat;
size_t prefix_size = s.find_first_of("?*[{\\");
if(prefix_size == std::string::npos) {
pat.prefix = s.substr(0, prefix_size).str();
// check if there is multiple `/` in prefix
size_t last_slash = 0;
size_t size = pat.prefix.size();
for(size_t i = 0; i < size; ++i) {
if(s[i] == '/' && i - last_slash == 1) {
return std::unexpected{"Multiple `/` is not allowed"};
}
last_slash = i;
}
return pat;
}
if(prefix_size != 0 && s[prefix_size - 1] == '/') {
pat.prefix_at_seg_end = true;
--prefix_size;
}
pat.prefix = s.substr(0, prefix_size).str();
// check if there is multiple `/` in prefix
size_t last_slash = 0;
size_t size = pat.prefix.size();
for(size_t i = 0; i < size; ++i) {
if(s[i] == '/' && i - last_slash == 1) {
return std::unexpected{"Multiple `/` is not allowed"};
}
last_slash = i;
}
s = s.substr(pat.prefix_at_seg_end ? prefix_size + 1 : prefix_size);
llvm::SmallVector<std::string, 1> sub_pats;
if(auto res = parseBraceExpansions(s, max_subpattern_num); res.has_value()) {
sub_pats = res.value();
} else {
return std::unexpected{res.error()};
}
for(auto sub_pat: sub_pats) {
if(auto res = SubGlobPattern::create(sub_pat); res.has_value()) {
pat.sub_globs.push_back(res.value());
} else {
return std::unexpected{res.error()};
}
}
return pat;
}
std::expected<GlobPattern::SubGlobPattern, std::string>
GlobPattern::SubGlobPattern::create(llvm::StringRef s) {
SubGlobPattern pat;
llvm::SmallVector<GlobSegment, 0> glob_segments;
GlobSegment* current_gs = &glob_segments.emplace_back();
current_gs->start = 0;
pat.pat.assign(s.begin(), s.end());
// Parse brackets.
size_t e = s.size();
for(size_t i = 0; i < e; ++i) {
if(!current_gs) {
current_gs = &glob_segments.emplace_back();
current_gs->start = i;
}
if(s[i] == '[') {
++i;
size_t j = i;
if(s[j] == ']') {
// ']' is allowed as the first character of a character class. '[]'
// is invalid. So, just skip the first character.
++j;
}
while(j != e && s[j] != ']') {
// Check if there is invalid escape char
++j;
if(s[j - 1] == '\\') {
if(j == e) {
// Reach here in such case:
// [a-zA-Z\]
// ~~~~~~~~^
return std::unexpected{
"Invalid glob pattern, unmatched '[' with stray '\\' inside"};
}
++j;
}
}
if(j == e) {
return std::unexpected{"Invalid glob pattern, unmatched '['"};
}
llvm::StringRef chars = s.substr(i, j - i);
bool invert = s[i] == '^' || s[i] == '!';
auto bv = invert ? expand(chars.substr(1), s) : expand(chars, s);
if(!bv.has_value()) {
return std::unexpected{bv.error()};
}
if(invert) {
bv->flip();
}
pat.brackets.push_back(Bracket{j + 1, bv.value()});
i = j;
} else if(s[i] == '\\') {
if(++i == e) {
return std::unexpected{"Invalid glob pattern, stray '\\'"};
}
} else if(s[i] == '/') {
if(i - current_gs->start == 1) {
return std::unexpected{"Multiple `/` is not allowed"};
}
current_gs->end = i;
current_gs = nullptr;
} else if(s[i] == '*') {
// check if there is multiple `*`
if(i + 1 < e - 1 && s[i + 1] == '*' && s[i + 2] == '*') {
return std::unexpected{"Multiple `*` is not allowed"};
}
}
}
if(current_gs) {
current_gs->end = e;
current_gs = nullptr;
}
std::swap(pat.glob_segments, glob_segments);
return pat;
}
bool GlobPattern::match(llvm::StringRef str) {
if(!str.consume_front(prefix)) {
return false;
}
if(str.empty() && sub_globs.empty()) {
return true;
}
if(!str.empty() && prefix_at_seg_end) {
if(str[0] != '/') {
return false;
}
str = str.substr(1);
}
for(auto& Glob: sub_globs) {
if(Glob.match(str)) {
return true;
}
}
return false;
}
bool GlobPattern::SubGlobPattern::match(llvm::StringRef str) const {
// p: Current position in the pattern
// s: Current position in the String
// b: Current processed bracket num
// s_start: String start
// s_end: String end
// p_start: Pattern start
// p_end: Pattern end
// seg_start: Position of the start of current segment
// s_end: Position of the end of current segment
// b: Current matched bracket
// current_glob_seg: Index of glob patterns
// wild_mode: Whether wildcards are allowed to cross segments
const char* s = str.data();
const char* const s_start = s;
const char* const s_end = s + str.size();
const char* p = pat.data();
const char* seg_start = p;
const char* const p_start = p;
const char* const p_end = p + pat.size();
const char* seg_end = p + glob_segments[0].end;
size_t b = 0;
size_t current_glob_seg = 0;
bool wild_mode = false;
// Status for backtracing
struct BacktraceStat {
size_t b;
size_t glob_seg;
bool wild_mode;
const char* p;
const char* s;
const char* seg_end;
const char* seg_start;
};
llvm::SmallVector<BacktraceStat, 6> backtrace_stack;
const size_t seg_num = glob_segments.size();
auto save_stat =
[&backtrace_stack, &b, &current_glob_seg, &wild_mode, &p, &s, &seg_end, &seg_start]() {
backtrace_stack.push_back({.b = b,
.glob_seg = current_glob_seg,
.wild_mode = wild_mode,
.p = p,
.s = s,
.seg_end = seg_end,
.seg_start = seg_start});
};
while(current_glob_seg < seg_num) {
if(s == s_end) {
// Return true if all pattern characters are processed or only
// '*' or '/' characters remain
return getPat().find_first_not_of("*/", p - pat.data()) == std::string::npos;
}
if(p != seg_end) {
switch(*p) {
case '*': {
if(p + 1 != p_end && *(p + 1) == '*') {
// Met '**'
p += 2;
wild_mode = true;
// Consume extra '*'
while(p != p_end && (*p == '*' || *p == '/')) {
if(*p == '/') {
if(current_glob_seg + 1 == seg_num) {
return true;
}
++current_glob_seg;
seg_start = p_start + glob_segments[current_glob_seg].start;
seg_end = p_start + glob_segments[current_glob_seg].end;
}
++p;
}
if(p == seg_end) {
// '**' at segment end
if(current_glob_seg + 1 == seg_num) {
// '**' at pattern end
return true;
}
// Goto next segment
++current_glob_seg;
while(s != s_end && *s == '/') {
++s;
}
p = p_start + glob_segments[current_glob_seg].start;
seg_start = p;
seg_end = p_start + glob_segments[current_glob_seg].end;
}
save_stat();
} else {
// Met '*'
++p;
wild_mode = false;
if(p == seg_end) {
while(s != s_end && *s != '/') {
++s;
}
if(s == s_end) {
continue;
}
if(s + 1 != s_end) {
++s;
}
if(current_glob_seg + 1 == seg_num) {
return true;
}
++current_glob_seg;
p = p_start + glob_segments[current_glob_seg].start;
seg_start = p;
seg_end = p_start + glob_segments[current_glob_seg].end;
}
save_stat();
}
continue;
}
case '?': {
if(p + 1 != seg_end && *(p + 1) == '*') {
// Handle '?*'
unsigned offset = *s == '\\' ? 2 : 1;
s += offset;
save_stat();
p += 2;
continue;
}
if(s != s_end && *s != '/') {
++p;
s = *s == '\\' ? s + 2 : s + 1;
continue;
}
break;
}
case '[': {
if(b < brackets.size() && brackets[b].bytes[uint8_t(*s)]) {
if(p == seg_start && !(s == s_start || *(s - 1) == '/')) {
// Segment start dismatch
break;
}
p = pat.data() + brackets[b].next_offset;
++b;
++s;
continue;
}
break;
}
case '\\': {
if(p + 1 != seg_end && *(p + 1) == *s) {
if(p == seg_start && !(s == s_start || *(s - 1) == '/')) {
// Segment start dismatch
break;
}
p += 2;
++s;
continue;
}
break;
}
default: {
if(*p == *s) {
if(p == seg_start && !(s == s_start || *(s - 1) == '/')) {
// Segment start dismatch
break;
}
++p;
++s;
continue;
}
break;
}
}
} else {
// P comes to a segment end
if(seg_end != p_end) {
if(wild_mode) {
// Step to next segment
++current_glob_seg;
while(s != s_end && *s != '/') {
++s;
}
if(s != s_end && *s == '/') {
++s;
}
if(current_glob_seg >= seg_num) {
return s == s_end;
}
p = p_start + glob_segments[current_glob_seg].start;
seg_start = p;
seg_end = p_start + glob_segments[current_glob_seg].end;
continue;
} else {
if(*seg_end != *s) {
return false;
}
// *S and *SegEnd should be '/'
// Escape repeat '/'
while(s != s_end && *s == '/') {
++s;
}
// Step to next segment
++current_glob_seg;
p = p_start + glob_segments[current_glob_seg].start;
seg_start = p;
seg_end = p_start + glob_segments[current_glob_seg].end;
continue;
}
}
}
if(backtrace_stack.empty()) {
return false;
}
// Backstrace
auto& state = backtrace_stack.back();
p = state.p;
s = ++state.s;
b = state.b;
current_glob_seg = state.glob_seg;
wild_mode = state.wild_mode;
seg_start = state.seg_start;
seg_end = state.seg_end;
// In non-WildMode, pop back invalid stat in the stack
if(!wild_mode && (s == s_end || *s == '/')) {
backtrace_stack.pop_back();
continue;
}
// In non-WildMode, '/' is a barrier
if(!wild_mode && *s == '/') {
return false;
}
}
return s == s_end;
}
} // namespace clice

View File

@@ -148,7 +148,6 @@ int main() {
params.command = "clang++ -std=c++20 main.cpp";
params.content = content;
params.outPath = path::join(".", "header2.pch");
params.bound.reset();
{
auto AST = compile(params);

View File

@@ -0,0 +1,484 @@
#include <iostream>
#include "Test/Test.h"
#include "Support/GlobPattern.h"
namespace clice::testing {
#define PATDEF(NAME, PAT) \
const char* PatString_##NAME = PAT; \
auto Res##NAME = clice::GlobPattern::create(PatString_##NAME, 100); \
if(!Res##NAME.has_value()) { \
std::cout << Res##NAME.error() << '\n'; \
} \
assert(Res##NAME.has_value()); \
auto NAME = Res##NAME.value();
TEST(GlobPattern, PattermSema) {
auto Pat1 = clice::GlobPattern::create("**/****.{c,cc}", 100);
EXPECT_EQ(Pat1.has_value(), false);
auto Pat2 = clice::GlobPattern::create("/foo/bar/baz////aaa.{c,cc}", 100);
EXPECT_EQ(Pat2.has_value(), false);
auto Pat3 = clice::GlobPattern::create("/foo/bar/baz/**////*.{c,cc}", 100);
EXPECT_EQ(Pat3.has_value(), false);
}
TEST(GlobPattern, Simple) {
PATDEF(Pat1, "node_modules")
EXPECT_EQ(Pat1.match("node_modules"), true);
EXPECT_EQ(Pat1.match("node_module"), false);
EXPECT_EQ(Pat1.match("/node_modules"), false);
EXPECT_EQ(Pat1.match("test/node_modules"), false);
PATDEF(Pat2, "test.txt")
EXPECT_EQ(Pat2.match("test.txt"), true);
EXPECT_EQ(Pat2.match("test?txt"), false);
EXPECT_EQ(Pat2.match("/text.txt"), false);
EXPECT_EQ(Pat2.match("test/test.txt"), false);
PATDEF(Pat3, "test(.txt")
EXPECT_EQ(Pat3.match("test(.txt"), true);
EXPECT_EQ(Pat3.match("test?txt"), false);
PATDEF(Pat4, "qunit")
EXPECT_EQ(Pat4.match("qunit"), true);
EXPECT_EQ(Pat4.match("qunit.css"), false);
EXPECT_EQ(Pat4.match("test/qunit"), false);
PATDEF(Pat5, "/DNXConsoleApp/**/*.cs")
EXPECT_EQ(Pat5.match("/DNXConsoleApp/Program.cs"), true);
EXPECT_EQ(Pat5.match("/DNXConsoleApp/foo/Program.cs"), true);
}
TEST(GlobPattern, DotHidden) {
PATDEF(Pat1, ".*");
EXPECT_EQ(Pat1.match(".git"), true);
EXPECT_EQ(Pat1.match(".hidden.txt"), true);
EXPECT_EQ(Pat1.match("git"), false);
EXPECT_EQ(Pat1.match("hidden.txt"), false);
EXPECT_EQ(Pat1.match("path/.git"), false);
EXPECT_EQ(Pat1.match("path/.hidden.txt"), false);
PATDEF(Pat2, "**/.*");
EXPECT_EQ(Pat2.match(".git"), true);
EXPECT_EQ(Pat2.match("/.git"), true);
EXPECT_EQ(Pat2.match(".hidden.txt"), true);
EXPECT_EQ(Pat2.match("git"), false);
EXPECT_EQ(Pat2.match("hidden.txt"), false);
EXPECT_EQ(Pat2.match("path/.git"), true);
EXPECT_EQ(Pat2.match("path/.hidden.txt"), true);
EXPECT_EQ(Pat2.match("/path/.git"), true);
EXPECT_EQ(Pat2.match("/path/.hidden.txt"), true);
EXPECT_EQ(Pat2.match("path/git"), false);
EXPECT_EQ(Pat2.match("pat.h/hidden.txt"), false);
PATDEF(Pat3, "._*");
EXPECT_EQ(Pat3.match("._git"), true);
EXPECT_EQ(Pat3.match("._hidden.txt"), true);
EXPECT_EQ(Pat3.match("git"), false);
EXPECT_EQ(Pat3.match("hidden.txt"), false);
EXPECT_EQ(Pat3.match("path/._git"), false);
EXPECT_EQ(Pat3.match("path/._hidden.txt"), false);
PATDEF(Pat4, "**/._*");
EXPECT_EQ(Pat4.match("._git"), true);
EXPECT_EQ(Pat4.match("._hidden.txt"), true);
EXPECT_EQ(Pat4.match("git"), false);
EXPECT_EQ(Pat4.match("hidden._txt"), false);
EXPECT_EQ(Pat4.match("path/._git"), true);
EXPECT_EQ(Pat4.match("path/._hidden.txt"), true);
EXPECT_EQ(Pat4.match("/path/._git"), true);
EXPECT_EQ(Pat4.match("/path/._hidden.txt"), true);
EXPECT_EQ(Pat4.match("path/git"), false);
EXPECT_EQ(Pat4.match("pat.h/hidden._txt"), false);
}
TEST(GlobPattern, EscapeCharacter) {
PATDEF(Pat1, R"(\*star)")
EXPECT_EQ(Pat1.match("*star"), true);
PATDEF(Pat2, R"(\{\*\})")
EXPECT_EQ(Pat2.match("{*}"), true);
}
TEST(GlobPattern, BracketExpr) {
PATDEF(Pat1, R"([a-zA-Z\]])")
EXPECT_EQ(Pat1.match(R"(])"), true);
EXPECT_EQ(Pat1.match(R"([)"), false);
EXPECT_EQ(Pat1.match(R"(s)"), true);
EXPECT_EQ(Pat1.match(R"(S)"), true);
EXPECT_EQ(Pat1.match(R"(0)"), false);
PATDEF(Pat2, R"([\^a-zA-Z""\\])")
EXPECT_EQ(Pat2.match(R"(")"), true);
EXPECT_EQ(Pat2.match(R"(^)"), true);
EXPECT_EQ(Pat2.match(R"(\)"), true);
EXPECT_EQ(Pat2.match(R"(")"), true);
EXPECT_EQ(Pat2.match(R"(x)"), true);
EXPECT_EQ(Pat2.match(R"(X)"), true);
EXPECT_EQ(Pat2.match(R"(0)"), false);
PATDEF(Pat3, R"([!0-9a-fA-F\-+\*])")
EXPECT_EQ(Pat3.match("1"), false);
EXPECT_EQ(Pat3.match("*"), false);
EXPECT_EQ(Pat3.match("s"), true);
EXPECT_EQ(Pat3.match("S"), true);
EXPECT_EQ(Pat3.match("H"), true);
EXPECT_EQ(Pat3.match("]"), true);
PATDEF(Pat4, R"([^\^0-9a-fA-F\-+\*])")
EXPECT_EQ(Pat4.match("1"), false);
EXPECT_EQ(Pat4.match("*"), false);
EXPECT_EQ(Pat4.match("^"), false);
EXPECT_EQ(Pat4.match("s"), true);
EXPECT_EQ(Pat4.match("S"), true);
EXPECT_EQ(Pat4.match("H"), true);
EXPECT_EQ(Pat4.match("]"), true);
PATDEF(Pat5, R"([\*-\^])")
EXPECT_EQ(Pat5.match("*"), true);
EXPECT_EQ(Pat5.match("a"), false);
EXPECT_EQ(Pat5.match("z"), false);
EXPECT_EQ(Pat5.match("A"), true);
EXPECT_EQ(Pat5.match("Z"), true);
EXPECT_EQ(Pat5.match("\\"), true);
EXPECT_EQ(Pat5.match("^"), true);
EXPECT_EQ(Pat5.match("-"), true);
PATDEF(Pat6, "foo.[^0-9]")
EXPECT_EQ(Pat6.match("foo.5"), false);
EXPECT_EQ(Pat6.match("foo.8"), false);
EXPECT_EQ(Pat6.match("bar.5"), false);
EXPECT_EQ(Pat6.match("foo.f"), true);
PATDEF(Pat7, "foo.[!0-9]")
EXPECT_EQ(Pat7.match("foo.5"), false);
EXPECT_EQ(Pat7.match("foo.8"), false);
EXPECT_EQ(Pat7.match("bar.5"), false);
EXPECT_EQ(Pat7.match("foo.f"), true);
PATDEF(Pat8, "foo.[0!^*?]")
EXPECT_EQ(Pat8.match("foo.5"), false);
EXPECT_EQ(Pat8.match("foo.8"), false);
EXPECT_EQ(Pat8.match("foo.0"), true);
EXPECT_EQ(Pat8.match("foo.!"), true);
EXPECT_EQ(Pat8.match("foo.^"), true);
EXPECT_EQ(Pat8.match("foo.*"), true);
EXPECT_EQ(Pat8.match("foo.?"), true);
PATDEF(Pat9, "foo[/]bar")
EXPECT_EQ(Pat9.match("foo/bar"), false);
PATDEF(Pat10, "foo.[[]")
EXPECT_EQ(Pat10.match("foo.["), true);
PATDEF(Pat11, "foo.[]]")
EXPECT_EQ(Pat11.match("foo.]"), true);
PATDEF(Pat12, "foo.[][!]")
EXPECT_EQ(Pat12.match("foo.]"), true);
EXPECT_EQ(Pat12.match("foo.["), true);
EXPECT_EQ(Pat12.match("foo.!"), true);
PATDEF(Pat13, "foo.[]-]")
EXPECT_EQ(Pat13.match("foo.]"), true);
EXPECT_EQ(Pat13.match("foo.-"), true);
PATDEF(Pat14, "foo.[0-9]")
EXPECT_EQ(Pat14.match("foo.5"), true);
EXPECT_EQ(Pat14.match("foo.8"), true);
EXPECT_EQ(Pat14.match("bar.5"), false);
EXPECT_EQ(Pat14.match("foo.f"), false);
}
TEST(GlobPattern, BraceExpr) {
PATDEF(Pat1, "*foo[0-9a-z].{c,cpp,cppm,?pp}")
EXPECT_EQ(Pat1.match("foo1.cc"), false);
EXPECT_EQ(Pat1.match("foo2.cpp"), true);
EXPECT_EQ(Pat1.match("foo3.cppm"), true);
EXPECT_EQ(Pat1.match("foot.cppm"), true);
EXPECT_EQ(Pat1.match("foot.hpp"), true);
EXPECT_EQ(Pat1.match("foot.app"), true);
EXPECT_EQ(Pat1.match("fooD.cppm"), false);
EXPECT_EQ(Pat1.match("BarfooD.cppm"), false);
EXPECT_EQ(Pat1.match("foofooD.cppm"), false);
PATDEF(Pat2, "proj/{build*,include,src}/*.{cc,cpp,h,hpp}")
EXPECT_EQ(Pat2.match("proj/include/foo.cc"), true);
EXPECT_EQ(Pat2.match("proj/include/bar.cpp"), true);
EXPECT_EQ(Pat2.match("proj/include/xxx/yyy/zzz/foo.cc"), false);
EXPECT_EQ(Pat2.match("proj/build-yyy/foo.h"), true);
EXPECT_EQ(Pat2.match("proj/build-xxx/foo.cpp"), true);
EXPECT_EQ(Pat2.match("proj/build/foo.cpp"), true);
EXPECT_EQ(Pat2.match("proj/build-xxx/xxx/yyy/zzz/foo.cpp"), false);
PATDEF(Pat3, "*.{html,js}")
EXPECT_EQ(Pat3.match("foo.js"), true);
EXPECT_EQ(Pat3.match("foo.html"), true);
EXPECT_EQ(Pat3.match("folder/foo.js"), false);
EXPECT_EQ(Pat3.match("/node_modules/foo.js"), false);
EXPECT_EQ(Pat3.match("foo.jss"), false);
EXPECT_EQ(Pat3.match("some.js/test"), false);
PATDEF(Pat4, "*.{html}")
EXPECT_EQ(Pat4.match("foo.html"), true);
EXPECT_EQ(Pat4.match("foo.js"), false);
EXPECT_EQ(Pat4.match("folder/foo.js"), false);
EXPECT_EQ(Pat4.match("/node_modules/foo.js"), false);
EXPECT_EQ(Pat4.match("foo.jss"), false);
EXPECT_EQ(Pat4.match("some.js/test"), false);
PATDEF(Pat5, "{node_modules,testing}")
EXPECT_EQ(Pat5.match("node_modules"), true);
EXPECT_EQ(Pat5.match("testing"), true);
EXPECT_EQ(Pat5.match("node_module"), false);
EXPECT_EQ(Pat5.match("dtesting"), false);
PATDEF(Pat6, "**/{foo,bar}")
EXPECT_EQ(Pat6.match("foo"), true);
EXPECT_EQ(Pat6.match("bar"), true);
EXPECT_EQ(Pat6.match("test/foo"), true);
EXPECT_EQ(Pat6.match("test/bar"), true);
EXPECT_EQ(Pat6.match("other/more/foo"), true);
EXPECT_EQ(Pat6.match("other/more/bar"), true);
EXPECT_EQ(Pat6.match("/foo"), true);
EXPECT_EQ(Pat6.match("/bar"), true);
EXPECT_EQ(Pat6.match("/test/foo"), true);
EXPECT_EQ(Pat6.match("/test/bar"), true);
EXPECT_EQ(Pat6.match("/other/more/foo"), true);
EXPECT_EQ(Pat6.match("/other/more/bar"), true);
PATDEF(Pat7, "{foo,bar}/**")
EXPECT_EQ(Pat7.match("foo"), true);
EXPECT_EQ(Pat7.match("bar"), true);
EXPECT_EQ(Pat7.match("bar/"), true);
EXPECT_EQ(Pat7.match("foo/test"), true);
EXPECT_EQ(Pat7.match("bar/test"), true);
EXPECT_EQ(Pat7.match("bar/test/"), true);
EXPECT_EQ(Pat7.match("foo/other/more"), true);
EXPECT_EQ(Pat7.match("bar/other/more"), true);
EXPECT_EQ(Pat7.match("bar/other/more/"), true);
PATDEF(Pat8, "{**/*.d.ts,**/*.js}")
EXPECT_EQ(Pat8.match("foo.js"), true);
EXPECT_EQ(Pat8.match("testing/foo.js"), true);
EXPECT_EQ(Pat8.match("/testing/foo.js"), true);
EXPECT_EQ(Pat8.match("foo.d.ts"), true);
EXPECT_EQ(Pat8.match("testing/foo.d.ts"), true);
EXPECT_EQ(Pat8.match("/testing/foo.d.ts"), true);
EXPECT_EQ(Pat8.match("foo.d"), false);
EXPECT_EQ(Pat8.match("testing/foo.d"), false);
EXPECT_EQ(Pat8.match("/testing/foo.d"), false);
PATDEF(Pat9, "{**/*.d.ts,**/*.js,path/simple.jgs}")
EXPECT_EQ(Pat9.match("foo.js"), true);
EXPECT_EQ(Pat9.match("testing/foo.js"), true);
EXPECT_EQ(Pat9.match("/testing/foo.js"), true);
EXPECT_EQ(Pat9.match("path/simple.jgs"), true);
EXPECT_EQ(Pat9.match("/path/simple.jgs"), false);
PATDEF(Pat10, "{**/*.d.ts,**/*.js,foo.[0-9]}")
EXPECT_EQ(Pat10.match("foo.5"), true);
EXPECT_EQ(Pat10.match("foo.8"), true);
EXPECT_EQ(Pat10.match("bar.5"), false);
EXPECT_EQ(Pat10.match("foo.f"), false);
EXPECT_EQ(Pat10.match("foo.js"), true);
PATDEF(Pat11, "prefix/{**/*.d.ts,**/*.js,foo.[0-9]}")
EXPECT_EQ(Pat11.match("prefix/foo.5"), true);
EXPECT_EQ(Pat11.match("prefix/foo.8"), true);
EXPECT_EQ(Pat11.match("prefix/bar.5"), false);
EXPECT_EQ(Pat11.match("prefix/foo.f"), false);
EXPECT_EQ(Pat11.match("prefix/foo.js"), true);
}
TEST(GlobPattern, WildGlob) {
PATDEF(Pat1, "**/*")
EXPECT_EQ(Pat1.match("foo"), true);
EXPECT_EQ(Pat1.match("foo/bar/baz"), true);
PATDEF(Pat2, "**/[0-9]*")
EXPECT_EQ(Pat2.match("114514foo"), true);
EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/zzz"), false);
EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/zzz114514"), false);
EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/114514"), true);
EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/114514zzz"), true);
PATDEF(Pat3, "**/*[0-9]")
EXPECT_EQ(Pat3.match("foo5"), true);
EXPECT_EQ(Pat3.match("foo/bar/baz/xxx/yyy/zzz"), false);
EXPECT_EQ(Pat3.match("foo/bar/baz/xxx/yyy/zzz114514"), true);
PATDEF(Pat4, "**/include/test/*.{cc,hh,c,h,cpp,hpp}")
EXPECT_EQ(Pat4.match("include/test/aaa.cc"), true);
EXPECT_EQ(Pat4.match("/include/test/aaa.cc"), true);
EXPECT_EQ(Pat4.match("xxx/yyy/include/test/aaa.cc"), true);
EXPECT_EQ(Pat4.match("include/foo/bar/baz/include/test/bbb.hh"), true);
EXPECT_EQ(Pat4.match("include/include/include/include/include/test/bbb.hpp"), true);
PATDEF(Pat5, "**include/test/*.{cc,hh,c,h,cpp,hpp}")
EXPECT_EQ(Pat5.match("include/test/fff.hpp"), true);
EXPECT_EQ(Pat5.match("xxx-yyy-include/test/fff.hpp"), true);
EXPECT_EQ(Pat5.match("xxx-yyy-include/test/.hpp"), true);
EXPECT_EQ(Pat5.match("/include/test/aaa.cc"), true);
EXPECT_EQ(Pat5.match("include/foo/bar/baz/include/test/bbb.hh"), true);
PATDEF(Pat6, "**/*foo.{c,cpp}")
EXPECT_EQ(Pat6.match("bar/foo.cpp"), true);
EXPECT_EQ(Pat6.match("bar/barfoo.cpp"), true);
EXPECT_EQ(Pat6.match("/foofoo.cpp"), true);
EXPECT_EQ(Pat6.match("foo/foo/foo/foo/foofoo.cpp"), true);
EXPECT_EQ(Pat6.match("foofoo.cpp"), true);
EXPECT_EQ(Pat6.match("barfoo.cpp"), true);
EXPECT_EQ(Pat6.match("foo.cpp"), true);
// Boundary test of `**`
PATDEF(Pat7, "**")
EXPECT_EQ(Pat7.match("foo"), true);
EXPECT_EQ(Pat7.match("foo/bar/baz"), true);
PATDEF(Pat8, "x/**")
EXPECT_EQ(Pat8.match("x/"), true);
EXPECT_EQ(Pat8.match("x/foo/bar/baz"), true);
EXPECT_EQ(Pat8.match("x"), true);
PATDEF(Pat9, "**/x")
EXPECT_EQ(Pat9.match("x"), true);
EXPECT_EQ(Pat9.match("/x"), true);
EXPECT_EQ(Pat9.match("/x/x/x/x/x"), true);
PATDEF(Pat10, "**/*")
EXPECT_EQ(Pat10.match("foo"), true);
EXPECT_EQ(Pat10.match("foo/bar"), true);
EXPECT_EQ(Pat10.match("foo/bar/baz"), true);
PATDEF(Pat11, "**/*.{cc,cpp}")
EXPECT_EQ(Pat11.match("foo/bar/baz.cc"), true);
EXPECT_EQ(Pat11.match("foo/foo/foo.cpp"), true);
EXPECT_EQ(Pat11.match("foo/bar/.cc"), true);
PATDEF(Pat12, "**/*?.{cc,cpp}")
EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/aaa.cc"), true);
EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/a.cc"), true);
EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/.cc"), false);
PATDEF(Pat13, "**/?*.{cc,cpp}")
EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/aaa.cc"), true);
EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/a.cc"), true);
EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/.cc"), false);
PATDEF(Pat14, "**/*[0-9]")
EXPECT_EQ(Pat14.match("foo5"), true);
EXPECT_EQ(Pat14.match("foo/bar/baz/xxx/yyy/zzz"), false);
EXPECT_EQ(Pat14.match("foo/bar/baz/xxx/yyy/zzz114514"), true);
PATDEF(Pat15, "**/*")
EXPECT_EQ(Pat15.match("foo"), true);
EXPECT_EQ(Pat15.match("foo/bar/baz"), true);
PATDEF(Pat16, "**/*.js")
EXPECT_EQ(Pat16.match("foo.js"), true);
EXPECT_EQ(Pat16.match("/foo.js"), true);
EXPECT_EQ(Pat16.match("folder/foo.js"), true);
EXPECT_EQ(Pat16.match("/node_modules/foo.js"), true);
EXPECT_EQ(Pat16.match("foo.jss"), false);
EXPECT_EQ(Pat16.match("some.js/test"), false);
EXPECT_EQ(Pat16.match("/some.js/test"), false);
PATDEF(Pat17, "**/project.json")
EXPECT_EQ(Pat17.match("project.json"), true);
EXPECT_EQ(Pat17.match("/project.json"), true);
EXPECT_EQ(Pat17.match("some/folder/project.json"), true);
EXPECT_EQ(Pat17.match("/some/folder/project.json"), true);
EXPECT_EQ(Pat17.match("some/folder/file_project.json"), false);
EXPECT_EQ(Pat17.match("some/folder/fileproject.json"), false);
EXPECT_EQ(Pat17.match("some/rrproject.json"), false);
PATDEF(Pat18, "test/**")
EXPECT_EQ(Pat18.match("test"), true);
EXPECT_EQ(Pat18.match("test/foo"), true);
EXPECT_EQ(Pat18.match("test/foo/"), true);
EXPECT_EQ(Pat18.match("test/foo.js"), true);
EXPECT_EQ(Pat18.match("test/other/foo.js"), true);
EXPECT_EQ(Pat18.match("est/other/foo.js"), false);
PATDEF(Pat19, "**")
EXPECT_EQ(Pat19.match("/"), true);
EXPECT_EQ(Pat19.match("foo.js"), true);
EXPECT_EQ(Pat19.match("folder/foo.js"), true);
EXPECT_EQ(Pat19.match("folder/foo/"), true);
EXPECT_EQ(Pat19.match("/node_modules/foo.js"), true);
EXPECT_EQ(Pat19.match("foo.jss"), true);
EXPECT_EQ(Pat19.match("some.js/test"), true);
PATDEF(Pat20, "test/**/*.js")
EXPECT_EQ(Pat20.match("test/foo.js"), true);
EXPECT_EQ(Pat20.match("test/other/foo.js"), true);
EXPECT_EQ(Pat20.match("test/other/more/foo.js"), true);
EXPECT_EQ(Pat20.match("test/foo.ts"), false);
EXPECT_EQ(Pat20.match("test/other/foo.ts"), false);
EXPECT_EQ(Pat20.match("test/other/more/foo.ts"), false);
PATDEF(Pat21, "**/**/*.js")
EXPECT_EQ(Pat21.match("foo.js"), true);
EXPECT_EQ(Pat21.match("/foo.js"), true);
EXPECT_EQ(Pat21.match("folder/foo.js"), true);
EXPECT_EQ(Pat21.match("/node_modules/foo.js"), true);
EXPECT_EQ(Pat21.match("foo.jss"), false);
EXPECT_EQ(Pat21.match("some.js/test"), false);
PATDEF(Pat22, "**/node_modules/**/*.js")
EXPECT_EQ(Pat22.match("foo.js"), false);
EXPECT_EQ(Pat22.match("folder/foo.js"), false);
EXPECT_EQ(Pat22.match("node_modules/foo.js"), true);
EXPECT_EQ(Pat22.match("/node_modules/foo.js"), true);
EXPECT_EQ(Pat22.match("node_modules/some/folder/foo.js"), true);
EXPECT_EQ(Pat22.match("/node_modules/some/folder/foo.js"), true);
EXPECT_EQ(Pat22.match("node_modules/some/folder/foo.ts"), false);
EXPECT_EQ(Pat22.match("foo.jss"), false);
EXPECT_EQ(Pat22.match("some.js/test"), false);
PATDEF(Pat23, "{**/node_modules/**,**/.git/**,**/bower_components/**}")
EXPECT_EQ(Pat23.match("node_modules"), true);
EXPECT_EQ(Pat23.match("/node_modules"), true);
EXPECT_EQ(Pat23.match("/node_modules/more"), true);
EXPECT_EQ(Pat23.match("some/test/node_modules"), true);
EXPECT_EQ(Pat23.match("/some/test/node_modules"), true);
EXPECT_EQ(Pat23.match("bower_components"), true);
EXPECT_EQ(Pat23.match("bower_components/more"), true);
EXPECT_EQ(Pat23.match("/bower_components"), true);
EXPECT_EQ(Pat23.match("some/test/bower_components"), true);
EXPECT_EQ(Pat23.match("/some/test/bower_components"), true);
EXPECT_EQ(Pat23.match(".git"), true);
EXPECT_EQ(Pat23.match("/.git"), true);
EXPECT_EQ(Pat23.match("some/test/.git"), true);
EXPECT_EQ(Pat23.match("/some/test/.git"), true);
EXPECT_EQ(Pat23.match("tempting"), false);
EXPECT_EQ(Pat23.match("/tempting"), false);
EXPECT_EQ(Pat23.match("some/test/tempting"), false);
EXPECT_EQ(Pat23.match("/some/test/tempting"), false);
PATDEF(Pat24, "{**/package.json,**/project.json}")
EXPECT_EQ(Pat24.match("package.json"), true);
EXPECT_EQ(Pat24.match("/package.json"), true);
EXPECT_EQ(Pat24.match("xpackage.json"), false);
EXPECT_EQ(Pat24.match("/xpackage.json"), false);
PATDEF(Pat25, "some/**/*.js")
EXPECT_EQ(Pat25.match("some/foo.js"), true);
EXPECT_EQ(Pat25.match("some/folder/foo.js"), true);
EXPECT_EQ(Pat25.match("something/foo.js"), false);
EXPECT_EQ(Pat25.match("something/folder/foo.js"), false);
PATDEF(Pat26, "some/**/*")
EXPECT_EQ(Pat26.match("some/foo.js"), true);
EXPECT_EQ(Pat26.match("some/folder/foo.js"), true);
EXPECT_EQ(Pat26.match("something/foo.js"), false);
EXPECT_EQ(Pat26.match("something/folder/foo.js"), false);
}
} // namespace clice::testing