#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); | |||||
} |
#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 |
#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); | |||||
} | |||||
} |
#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()); | |||||
} |
#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 |
#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")); | |||||
} |