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