@@ -0,0 +1,189 @@ | |||
#include "./fnmatch.hpp" | |||
#include <cassert> | |||
#include <stdexcept> | |||
#include <string_view> | |||
#include <vector> | |||
using charptr = const char*; | |||
namespace dds::detail::fnmatch { | |||
namespace { | |||
class base_pattern_elem { | |||
public: | |||
virtual bool match(charptr first, charptr last) const noexcept = 0; | |||
virtual ~base_pattern_elem() = default; | |||
std::unique_ptr<base_pattern_elem> next; | |||
}; | |||
class rt_star : public base_pattern_elem { | |||
bool match(charptr first, charptr last) const noexcept { | |||
while (first != last) { | |||
auto did_match = next->match(first, last); | |||
if (did_match) { | |||
return true; | |||
} | |||
++first; | |||
} | |||
// We're at the end. Try once more | |||
return next->match(first, last); | |||
} | |||
}; | |||
class rt_any_char : public base_pattern_elem { | |||
bool match(charptr first, charptr last) const noexcept { | |||
if (first == last) { | |||
return false; | |||
} | |||
return next->match(first + 1, last); | |||
} | |||
}; | |||
class rt_oneof : public base_pattern_elem { | |||
std::string _chars; | |||
bool match(charptr first, charptr last) const noexcept { | |||
if (first == last) { | |||
return false; | |||
} | |||
auto idx = _chars.find(*first); | |||
if (idx == _chars.npos) { | |||
return false; | |||
} | |||
return next->match(first + 1, last); | |||
} | |||
public: | |||
explicit rt_oneof(std::string chars) | |||
: _chars(chars) {} | |||
}; | |||
class rt_lit : public base_pattern_elem { | |||
std::string _lit; | |||
bool match(charptr first, charptr last) const noexcept { | |||
auto remaining = static_cast<std::size_t>(std::distance(first, last)); | |||
if (remaining < _lit.size()) { | |||
return false; | |||
} | |||
auto eq = std::equal(first, first + _lit.size(), _lit.begin()); | |||
if (!eq) { | |||
return false; | |||
} | |||
return next->match(first + _lit.size(), last); | |||
} | |||
public: | |||
explicit rt_lit(std::string lit) | |||
: _lit(lit) {} | |||
}; | |||
class rt_end : public base_pattern_elem { | |||
bool match(charptr first, charptr last) const noexcept { return first == last; } | |||
}; | |||
} // namespace | |||
class pattern_impl { | |||
std::unique_ptr<base_pattern_elem> _head; | |||
std::unique_ptr<base_pattern_elem>* _next_to_compile = &_head; | |||
template <typename T, typename... Args> | |||
void _add_elem(Args&&... args) { | |||
*_next_to_compile = std::make_unique<T>(std::forward<Args>(args)...); | |||
_next_to_compile = &(*_next_to_compile)->next; | |||
} | |||
charptr _compile_oneof(charptr cur, charptr last) { | |||
std::string chars; | |||
while (cur != last) { | |||
auto c = *cur; | |||
if (c == ']') { | |||
// We've reached the end of the group | |||
_add_elem<rt_oneof>(chars); | |||
return cur + 1; | |||
} | |||
if (c == '\\') { | |||
++cur; | |||
if (cur == last) { | |||
throw std::runtime_error("Untermated [group] in pattern"); | |||
} | |||
chars.push_back(*cur); | |||
} else { | |||
chars.push_back(c); | |||
} | |||
++cur; | |||
} | |||
throw std::runtime_error("Unterminated [group] in pattern"); | |||
} | |||
charptr _compile_lit(charptr cur, charptr last) { | |||
std::string lit; | |||
while (cur != last) { | |||
auto c = *cur; | |||
if (c == '*' || c == '[' || c == '?') { | |||
break; | |||
} | |||
if (c == '\\') { | |||
++cur; | |||
if (cur == last) { | |||
throw std::runtime_error("Invalid \\ at end of pattern"); | |||
} | |||
// Push back whatever character follows | |||
lit.push_back(*cur); | |||
++cur; | |||
continue; | |||
} else { | |||
lit.push_back(c); | |||
} | |||
++cur; | |||
} | |||
_add_elem<rt_lit>(lit); | |||
return cur; | |||
} | |||
void _compile_next(charptr first, charptr last) { | |||
if (first == last) { | |||
return; | |||
} | |||
auto c = *first; | |||
if (c == '*') { | |||
_add_elem<rt_star>(); | |||
_compile_next(first + 1, last); | |||
} else if (c == '[') { | |||
first = _compile_oneof(first + 1, last); | |||
_compile_next(first, last); | |||
} else if (c == '?') { | |||
_add_elem<rt_any_char>(); | |||
_compile_next(first + 1, last); | |||
} else { | |||
// Literal string | |||
first = _compile_lit(first, last); | |||
_compile_next(first, last); | |||
} | |||
} | |||
public: | |||
pattern_impl(std::string_view str) { | |||
_compile_next(str.data(), str.data() + str.size()); | |||
// Set the tail of the list to be an rt_end to detect end-of-string | |||
_add_elem<rt_end>(); | |||
} | |||
bool match(charptr first, charptr last) const noexcept { | |||
assert(_head); | |||
return _head->match(first, last); | |||
} | |||
}; | |||
} // namespace dds::detail::fnmatch | |||
dds::fnmatch::pattern dds::fnmatch::compile(std::string_view str) { | |||
return pattern{std::make_shared<detail::fnmatch::pattern_impl>(str)}; | |||
} | |||
bool dds::fnmatch::pattern::_match(charptr first, charptr last) const noexcept { | |||
assert(_impl); | |||
return _impl->match(first, last); | |||
} |
@@ -0,0 +1,321 @@ | |||
#pragma once | |||
#include <cstdlib> | |||
#include <iterator> | |||
#include <memory> | |||
#include <optional> | |||
#include <string> | |||
#include <type_traits> | |||
namespace dds { | |||
namespace fnmatch { | |||
template <typename... Elems> | |||
struct ct_pattern; | |||
class pattern; | |||
} // namespace fnmatch | |||
namespace detail::fnmatch { | |||
template <typename... Sub> | |||
struct seq {}; | |||
struct star {}; | |||
struct any_one {}; | |||
template <typename Char> | |||
constexpr std::size_t length(const Char* str) { | |||
std::size_t ret = 0; | |||
while (*str != Char(0)) { | |||
++str; | |||
++ret; | |||
} | |||
return ret; | |||
} | |||
template <auto... Chars> | |||
struct oneof {}; | |||
template <auto... Chars> | |||
struct not_oneof {}; | |||
template <auto Char> | |||
struct just {}; | |||
template <typename> | |||
struct is_just : std::false_type {}; | |||
template <auto C> | |||
struct is_just<just<C>> : std::true_type {}; | |||
template <typename Matcher, auto NewCur> | |||
struct oneof_ret { | |||
using type = Matcher; | |||
constexpr static auto end_offset = NewCur; | |||
}; | |||
template <auto... Chars, auto End> | |||
constexpr auto negate(oneof_ret<oneof<Chars...>, End>) { | |||
return oneof_ret<not_oneof<Chars...>, End>(); | |||
} | |||
template <auto Cur, auto Len, auto... Chars, typename String> | |||
constexpr auto compile_oneof_chars(String s) { | |||
constexpr auto str = s(); | |||
constexpr auto cur_char = str[Cur]; | |||
static_assert(Cur != Len, "Unterminated '[' group in pattern"); | |||
static_assert(Cur + 1 != Len || cur_char != '\\', "Escape \\ at end of pattern"); | |||
if constexpr (cur_char == ']') { | |||
return oneof_ret<oneof<Chars...>, Cur + 1>(); | |||
} else if constexpr (cur_char == '\\') { | |||
constexpr auto next_char = str[Cur + 1]; | |||
return compile_oneof_chars<Cur + 2, Len, Chars..., next_char>(s); | |||
} else { | |||
return compile_oneof_chars<Cur + 1, Len, Chars..., cur_char>(s); | |||
} | |||
} | |||
template <auto Cur, auto Len, typename String> | |||
constexpr auto compile_oneof(String s) { | |||
constexpr auto str = s(); | |||
constexpr bool negated = str[Cur] == '!'; | |||
constexpr auto oneof_start = Cur + (negated ? 1 : 0); | |||
auto oneof = compile_oneof_chars<oneof_start, Len>(s); | |||
if constexpr (negated) { | |||
return negate(oneof); | |||
} else { | |||
return oneof; | |||
} | |||
} | |||
template <auto Cur, auto Len, typename... Matchers, typename String> | |||
constexpr auto compile_next(String s) { | |||
constexpr auto str = s(); | |||
constexpr auto cur_char = str[Cur]; | |||
if constexpr (Cur == Len) { | |||
return dds::fnmatch::ct_pattern<Matchers...>(); | |||
} else if constexpr (cur_char == '*') { | |||
return compile_next<Cur + 1, Len, Matchers..., star>(s); | |||
} else if constexpr (cur_char == '?') { | |||
return compile_next<Cur + 1, Len, Matchers..., any_one>(s); | |||
} else if constexpr (cur_char == '[') { | |||
constexpr auto oneof_ret = compile_oneof<Cur + 1, Len>(s); | |||
return compile_next<oneof_ret.end_offset, | |||
Len, | |||
Matchers..., | |||
typename decltype(oneof_ret)::type>(s); | |||
} else if constexpr (cur_char == '\\') { | |||
// Escape sequence | |||
static_assert(Cur + 1 != Len, "Escape \\ at end of pattern."); | |||
constexpr auto next_char = str[Cur + 1]; | |||
return compile_next<Cur + 2, Len, Matchers..., just<next_char>>(s); | |||
} else { | |||
return compile_next<Cur + 1, Len, Matchers..., just<cur_char>>(s); | |||
} | |||
} | |||
template <typename Iter1, typename Iter2> | |||
constexpr bool equal(Iter1 a_first, Iter1 a_last, Iter2 b_first) { | |||
while (a_first != a_last) { | |||
if (*a_first != *b_first) { | |||
return false; | |||
} | |||
++a_first; | |||
++b_first; | |||
} | |||
return true; | |||
} | |||
} // namespace detail::fnmatch | |||
namespace fnmatch { | |||
template <typename... Elems> | |||
struct ct_pattern { | |||
private: | |||
/// VVVVVVVVVVVVVVVVVVV Optimized Cases VVVVVVVVVVVVVVVVVVVVVVV | |||
/** | |||
* Common case of a star '*' followed by literals to the end of the pattern | |||
*/ | |||
template <typename Iter, auto C, auto... Chars> | |||
static constexpr bool match_1(Iter cur, | |||
const Iter last, | |||
detail::fnmatch::star, | |||
detail::fnmatch::just<C> c1, | |||
detail::fnmatch::just<Chars>... t) { | |||
// We know the length of tail required, so we can just skip ahead without | |||
// a loop | |||
auto cur_len = std::distance(cur, last); | |||
if (cur_len < sizeof...(Chars) + 1) { | |||
// Not enough remaining to match | |||
return false; | |||
} | |||
// Skip ahead and match the rest | |||
auto to_skip = cur_len - (sizeof...(Chars) + 1); | |||
return match_1(std::next(cur, to_skip), last, c1, t...); | |||
} | |||
/** | |||
* Common case of a sequence of literals at the tail. | |||
*/ | |||
template <typename Iter, auto... Chars> | |||
static constexpr bool match_1(Iter cur, const Iter last, detail::fnmatch::just<Chars>...) { | |||
constexpr auto LitLength = sizeof...(Chars); | |||
auto remaining = std::distance(cur, last); | |||
if (remaining != LitLength) { | |||
return false; | |||
} | |||
// Put our characters into an array for a quick comparison | |||
std::decay_t<decltype(*cur)> chars[LitLength] = {Chars...}; | |||
return detail::fnmatch::equal(chars, chars + LitLength, cur); | |||
} | |||
/// VVVVVVVVVVVVVVVVVVVV General cases VVVVVVVVVVVVVVVVVVVVVVVV | |||
template <typename Iter, typename... Tail> | |||
static constexpr bool match_1(Iter cur, const Iter last, detail::fnmatch::star, Tail... t) { | |||
while (cur != last) { | |||
auto did_match = match_1(cur, last, t...); | |||
if (did_match) { | |||
return true; | |||
} | |||
++cur; | |||
} | |||
// We've advanced to the end of the string, but we might still have a match... | |||
return match_1(cur, last, t...); | |||
} | |||
template <typename Iter, auto... Chars, typename... Tail> | |||
static constexpr bool | |||
match_1(Iter cur, const Iter last, detail::fnmatch::not_oneof<Chars...>, Tail... t) { | |||
if (cur == last) { | |||
return false; | |||
} | |||
if (((*cur == Chars) || ...)) { | |||
return false; | |||
} | |||
return match_1(std::next(cur), last, t...); | |||
} | |||
template <typename Iter, auto... Chars, typename... Tail> | |||
static constexpr bool | |||
match_1(Iter cur, const Iter last, detail::fnmatch::oneof<Chars...>, Tail... t) { | |||
if (cur == last) { | |||
return false; | |||
} | |||
if (((*cur == Chars) || ...)) { | |||
return match_1(std::next(cur), last, t...); | |||
} else { | |||
// current char is not in pattern | |||
return false; | |||
} | |||
} | |||
template <typename Iter, | |||
auto C, | |||
typename... Tail, | |||
// Only enable this overload if the tail is not entirely just<> items | |||
// (we have an optimization for that case) | |||
typename = std::enable_if_t<!(detail::fnmatch::is_just<Tail>() && ...)>> | |||
static constexpr bool match_1(Iter cur, const Iter last, detail::fnmatch::just<C>, Tail... t) { | |||
if (cur == last) { | |||
// We've reached the end, but we have more things to match | |||
return false; | |||
} | |||
if (*cur != C) { | |||
// Wrong char | |||
return false; | |||
} else { | |||
// Good char, keep going | |||
return match_1(std::next(cur), last, t...); | |||
} | |||
} | |||
template <typename Iter, typename... Tail> | |||
static constexpr bool match_1(Iter cur, const Iter last, detail::fnmatch::any_one, Tail... t) { | |||
if (cur == last) { | |||
return false; | |||
} | |||
return match_1(std::next(cur), last, t...); | |||
} | |||
template <typename Iter> | |||
static constexpr bool match_1(Iter cur, Iter last) { | |||
return cur == last; | |||
} | |||
public: | |||
static constexpr bool match(const char* fname) { | |||
return match_1(fname, fname + detail::fnmatch::length(fname), Elems()...); | |||
} | |||
}; | |||
template <typename StringGenerator, typename = decltype(std::declval<StringGenerator&>()())> | |||
constexpr auto compile(StringGenerator&& s) { | |||
constexpr auto pattern = s(); | |||
constexpr auto len = detail::fnmatch::length(pattern); | |||
return decltype(detail::fnmatch::compile_next<0, len>(s))(); | |||
} | |||
pattern compile(std::string_view str); | |||
} // namespace fnmatch | |||
namespace detail::fnmatch { | |||
class pattern_impl; | |||
} // namespace detail::fnmatch | |||
namespace fnmatch { | |||
class pattern { | |||
std::shared_ptr<const detail::fnmatch::pattern_impl> _impl; | |||
bool _match(const char* begin, const char* end) const noexcept; | |||
public: | |||
constexpr static std::size_t noalloc_size = 256; | |||
pattern(std::shared_ptr<const detail::fnmatch::pattern_impl> ptr) | |||
: _impl(ptr) {} | |||
~pattern() = default; | |||
pattern(const pattern&) = default; | |||
pattern(pattern&&) = default; | |||
pattern& operator=(const pattern&) = default; | |||
pattern& operator=(pattern&&) = default; | |||
template <typename Iter> | |||
bool match(Iter first, Iter last) const { | |||
auto dist = static_cast<std::size_t>(std::distance(first, last)); | |||
if (dist < noalloc_size) { | |||
char buffer[noalloc_size]; | |||
auto buf_end = std::copy(first, last, buffer); | |||
return _match(buffer, buf_end); | |||
} else { | |||
// Allocates | |||
std::string str(first, last); | |||
return _match(str.data(), str.data() + str.size()); | |||
} | |||
} | |||
bool match(const char* str) const { | |||
return match(str, str + dds::detail::fnmatch::length(str)); | |||
} | |||
template <typename Seq> | |||
bool match(const Seq& seq) const { | |||
using std::begin; | |||
using std::end; | |||
return match(begin(seq), end(seq)); | |||
} | |||
std::optional<std::string> literal_spelling() const noexcept; | |||
}; | |||
} // namespace fnmatch | |||
} // namespace dds |
@@ -0,0 +1,32 @@ | |||
#include <dds/util/fnmatch.hpp> | |||
#include <catch2/catch.hpp> | |||
TEST_CASE("Basic fnmatch matching") { | |||
auto pat = dds::fnmatch::compile("foo.bar"); | |||
CHECK_FALSE(pat.match("foo.baz")); | |||
CHECK_FALSE(pat.match("foo.")); | |||
CHECK_FALSE(pat.match("foo.barz")); | |||
CHECK_FALSE(pat.match("foo.bar ")); | |||
CHECK_FALSE(pat.match(" foo.bar")); | |||
CHECK(pat.match("foo.bar")); | |||
pat = dds::fnmatch::compile("foo.*"); | |||
CHECK(pat.match("foo.")); | |||
auto m = pat.match("foo.b"); | |||
CHECK(m); | |||
CHECK(pat.match("foo. ")); | |||
CHECK_FALSE(pat.match("foo")); | |||
CHECK_FALSE(pat.match(" foo.bar")); | |||
pat = dds::fnmatch::compile("foo.*.cpp"); | |||
for (auto fname : {"foo.bar.cpp", "foo..cpp", "foo.cat.cpp"}) { | |||
auto m = pat.match(fname); | |||
CHECK(m); | |||
} | |||
for (auto fname : {"foo.cpp", "foo.cpp"}) { | |||
auto m = pat.match(fname); | |||
CHECK_FALSE(m); | |||
} | |||
} |
@@ -0,0 +1,238 @@ | |||
#include "./glob.hpp" | |||
#include "./fnmatch.hpp" | |||
#include <neo/assert.hpp> | |||
#include <optional> | |||
namespace { | |||
enum glob_coro_ret { | |||
reenter_again, | |||
yield_value, | |||
done, | |||
}; | |||
} // namespace | |||
namespace dds::detail { | |||
struct rglob_item { | |||
std::optional<dds::fnmatch::pattern> pattern; | |||
}; | |||
struct glob_impl { | |||
std::vector<rglob_item> items; | |||
}; | |||
struct glob_iter_state { | |||
fs::path root; | |||
const glob_impl& impl; | |||
std::vector<rglob_item>::const_iterator pat_iter = impl.items.begin(); | |||
const bool is_leaf_pattern = std::next(pat_iter) == impl.items.end(); | |||
fs::directory_entry entry; | |||
fs::directory_iterator dir_iter{root}; | |||
const bool is_rglob = !pat_iter->pattern.has_value(); | |||
std::unique_ptr<glob_iter_state> _next_state; | |||
int _state_label = 0; | |||
fs::directory_entry get_entry() const noexcept { | |||
if (_next_state) { | |||
return _next_state->get_entry(); | |||
} | |||
return entry; | |||
} | |||
#define CORO_REENTER_POINT \ | |||
case __LINE__: \ | |||
static_assert(true) | |||
#define CORO_SAVE_POINT _state_label = __LINE__ | |||
#define YIELD(E) \ | |||
do { \ | |||
CORO_SAVE_POINT; \ | |||
entry = E; \ | |||
return yield_value; \ | |||
} while (0); \ | |||
CORO_REENTER_POINT | |||
#define EXIT_DIRECTORY() \ | |||
do { \ | |||
return done; \ | |||
} while (0); \ | |||
CORO_REENTER_POINT | |||
#define ENTER_DIRECTORY(D, Pat) \ | |||
do { \ | |||
_next_state.reset(new glob_iter_state{fs::path(D), impl, Pat}); \ | |||
CORO_SAVE_POINT; \ | |||
return reenter_again; \ | |||
} while (0); \ | |||
CORO_REENTER_POINT | |||
#define CONTINUE() \ | |||
do { \ | |||
_state_label = 0; \ | |||
return reenter_again; \ | |||
} while (0) | |||
glob_coro_ret reenter() { | |||
if (_next_state) { | |||
auto st = _next_state->reenter(); | |||
if (st == done) { | |||
_next_state.reset(); | |||
return reenter_again; | |||
} | |||
return st; | |||
} | |||
const bool dir_done = dir_iter == fs::directory_iterator(); | |||
const auto cur_pattern = pat_iter->pattern; | |||
const bool cur_is_rglob = !cur_pattern.has_value(); | |||
switch (_state_label) { | |||
case 0: | |||
// | |||
if (dir_done) { | |||
EXIT_DIRECTORY(); | |||
} | |||
entry = *dir_iter++; | |||
if (cur_is_rglob) { | |||
if (is_leaf_pattern) { | |||
YIELD(entry); | |||
} else if (std::next(pat_iter)->pattern.value().match( | |||
fs::path(entry).filename().string())) { | |||
// The next pattern in the glob will match this file directly. | |||
if (entry.is_directory()) { | |||
ENTER_DIRECTORY(entry, std::next(pat_iter)); | |||
} else { | |||
YIELD(entry); | |||
} | |||
} | |||
if (entry.is_directory()) { | |||
ENTER_DIRECTORY(entry, pat_iter); | |||
} else { | |||
// A non-directory file matches an `**` pattern? Ignore it. | |||
} | |||
} else { | |||
if (cur_pattern->match(fs::path(entry).filename().string())) { | |||
// We match this entry | |||
if (is_leaf_pattern) { | |||
YIELD(entry); | |||
} else if (entry.is_directory()) { | |||
ENTER_DIRECTORY(entry, std::next(pat_iter)); | |||
} | |||
} | |||
} | |||
} | |||
CONTINUE(); | |||
} | |||
}; // namespace dds::detail | |||
} // namespace dds::detail | |||
namespace { | |||
dds::detail::glob_impl compile_glob_expr(std::string_view pattern) { | |||
using namespace dds::detail; | |||
glob_impl acc{}; | |||
while (!pattern.empty()) { | |||
const auto next_slash = pattern.find('/'); | |||
const auto next_part = pattern.substr(0, next_slash); | |||
if (next_slash != pattern.npos) { | |||
pattern.remove_prefix(next_slash + 1); | |||
} else { | |||
pattern = ""; | |||
} | |||
if (next_part == "**") { | |||
acc.items.emplace_back(); | |||
} else { | |||
acc.items.push_back({dds::fnmatch::compile(next_part)}); | |||
} | |||
} | |||
if (acc.items.empty()) { | |||
throw std::runtime_error("Invalid path glob expression (Must not be empty!)"); | |||
} | |||
return acc; | |||
} | |||
} // namespace | |||
dds::glob_iterator::glob_iterator(dds::glob gl, dds::path_ref root) | |||
: _impl(gl._impl) | |||
, _done(false) { | |||
_state = std::make_shared<detail::glob_iter_state>(detail::glob_iter_state{root, *_impl}); | |||
increment(); | |||
} | |||
void dds::glob_iterator::increment() { | |||
auto st = reenter_again; | |||
while (st == reenter_again) { | |||
st = _state->reenter(); | |||
} | |||
_done = st == done; | |||
} | |||
dds::fs::directory_entry dds::glob_iterator::dereference() const noexcept { | |||
return _state->get_entry(); | |||
} | |||
dds::glob dds::glob::compile(std::string_view pattern) { | |||
glob ret; | |||
ret._impl = std::make_shared<dds::detail::glob_impl>(compile_glob_expr(pattern)); | |||
return ret; | |||
} | |||
namespace { | |||
using path_iter = dds::fs::path::const_iterator; | |||
using pat_iter = std::vector<dds::detail::rglob_item>::const_iterator; | |||
bool check_matches(path_iter elem_it, | |||
const path_iter elem_stop, | |||
pat_iter pat_it, | |||
const pat_iter pat_stop) noexcept { | |||
if (elem_it == elem_stop && pat_it == pat_stop) { | |||
return true; | |||
} | |||
if (elem_it == elem_stop || pat_it == pat_stop) { | |||
return false; | |||
} | |||
if (pat_it->pattern.has_value()) { | |||
// A regular pattern | |||
if (!pat_it->pattern->match(elem_it->string())) { | |||
return false; | |||
} | |||
return check_matches(++elem_it, elem_stop, ++pat_it, pat_stop); | |||
} else { | |||
// An rglob pattern "**". Check by peeling of individual path elements | |||
const auto next_pat = std::next(pat_it); | |||
for (; elem_it != elem_stop; ++elem_it) { | |||
if (check_matches(elem_it, elem_stop, next_pat, pat_stop)) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
} | |||
} // namespace | |||
bool dds::glob::match(dds::path_ref filepath) const noexcept { | |||
return check_matches(filepath.begin(), | |||
filepath.end(), | |||
_impl->items.cbegin(), | |||
_impl->items.cend()); | |||
} |
@@ -0,0 +1,61 @@ | |||
#pragma once | |||
#include <dds/util/fs.hpp> | |||
#include <neo/iterator_facade.hpp> | |||
#include <string_view> | |||
#include <vector> | |||
namespace dds { | |||
namespace detail { | |||
struct glob_impl; | |||
struct glob_iter_state; | |||
} // namespace detail | |||
class glob; | |||
class glob_iterator : public neo::iterator_facade<glob_iterator> { | |||
std::shared_ptr<const detail::glob_impl> _impl; | |||
std::shared_ptr<detail::glob_iter_state> _state; | |||
bool _done = true; | |||
public: | |||
glob_iterator() = default; | |||
glob_iterator(glob impl, path_ref root); | |||
fs::directory_entry dereference() const noexcept; | |||
void increment(); | |||
struct sentinel_type {}; | |||
bool at_end() const noexcept { return _done; } | |||
glob_iterator begin() const noexcept { return *this; } | |||
auto end() const noexcept { return sentinel_type{}; } | |||
}; | |||
class glob { | |||
friend class glob_iterator; | |||
std::shared_ptr<const detail::glob_impl> _impl; | |||
glob() = default; | |||
public: | |||
static glob compile(std::string_view str); | |||
auto scan_from(path_ref root) const noexcept { return glob_iterator(*this, root); } | |||
auto begin() const noexcept { return scan_from(fs::current_path()); } | |||
auto end() const noexcept { return glob_iterator::sentinel_type{}; } | |||
bool match(path_ref) const noexcept; | |||
}; | |||
} // namespace dds |
@@ -0,0 +1,69 @@ | |||
#include <dds/util/glob.hpp> | |||
#include <catch2/catch.hpp> | |||
TEST_CASE("Simple glob") { | |||
auto this_dir = dds::fs::path(__FILE__).parent_path(); | |||
auto glob = dds::glob::compile("*.test.cpp"); | |||
auto it = glob.scan_from(this_dir); | |||
for (; it != glob.end(); ++it) { | |||
auto&& el = *it; | |||
} | |||
int n_found = 0; | |||
for (auto found : glob.scan_from(this_dir)) { | |||
++n_found; | |||
} | |||
CHECK(n_found > 0); | |||
n_found = 0; | |||
for (auto found : dds::glob::compile("glob.test.cpp").scan_from(this_dir)) { | |||
n_found++; | |||
} | |||
CHECK(n_found == 1); | |||
auto me_it = dds::glob::compile("src/**/glob.test.cpp").begin(); | |||
REQUIRE(!me_it.at_end()); | |||
++me_it; | |||
CHECK(me_it.at_end()); | |||
auto all_tests = dds::glob::compile("src/**/*.test.cpp"); | |||
n_found = 0; | |||
for (auto f : all_tests) { | |||
n_found += 1; | |||
} | |||
CHECK(n_found > 10); | |||
CHECK(n_found < 1000); // If we have more than 1000 .test files, that's crazy | |||
} | |||
TEST_CASE("Check globs") { | |||
auto glob = dds::glob::compile("foo/bar*/baz"); | |||
CHECK(glob.match("foo/bar/baz")); | |||
CHECK(glob.match("foo/barffff/baz")); | |||
CHECK_FALSE(glob.match("foo/bar")); | |||
CHECK_FALSE(glob.match("foo/ffbar/baz")); | |||
CHECK_FALSE(glob.match("foo/bar/bazf")); | |||
CHECK_FALSE(glob.match("foo/bar/")); | |||
glob = dds::glob::compile("foo/**/bar.txt"); | |||
CHECK(glob.match("foo/bar.txt")); | |||
CHECK(glob.match("foo/thing/bar.txt")); | |||
CHECK(glob.match("foo/thing/another/bar.txt")); | |||
CHECK_FALSE(glob.match("foo/fail")); | |||
CHECK_FALSE(glob.match("foo/bar.txtf")); | |||
CHECK_FALSE(glob.match("foo/bar.txt/f")); | |||
CHECK_FALSE(glob.match("foo/fbar.txt")); | |||
CHECK_FALSE(glob.match("foo/thing/fail")); | |||
CHECK_FALSE(glob.match("foo/thing/another/fail")); | |||
CHECK_FALSE(glob.match("foo/thing/bar.txt/fail")); | |||
CHECK_FALSE(glob.match("foo/bar.txt/fail")); | |||
glob = dds::glob::compile("foo/**/bar/**/baz.txt"); | |||
CHECK(glob.match("foo/bar/baz.txt")); | |||
CHECK(glob.match("foo/thing/bar/baz.txt")); | |||
CHECK(glob.match("foo/thing/bar/baz.txt")); | |||
CHECK(glob.match("foo/thing/bar/thing/baz.txt")); | |||
CHECK(glob.match("foo/bar/thing/baz.txt")); | |||
CHECK(glob.match("foo/bar/baz/baz.txt")); | |||
} |