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