Move glob() and walk() generator versions to standalone functions. (#173)

This commit is contained in:
Mikaël Capelle
2025-07-08 19:32:59 +02:00
committed by GitHub
parent 867061d2f8
commit 01c5b6eb35
5 changed files with 298 additions and 253 deletions
+9 -43
View File
@@ -29,6 +29,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <mutex>
#include <stdexcept>
#include <vector>
#include <version>
#include <QDateTime>
#include <QFlags>
@@ -112,14 +113,6 @@ struct QDLLEXPORT UnsupportedOperationException : public Exception
using Exception::Exception;
};
/**
* @brief Exception thrown when an invalid glob pattern is specified.
*/
struct QDLLEXPORT InvalidGlobPatternException : public Exception
{
using Exception::Exception;
};
/**
* @brief Represent an entry in a file tree, either a file or a directory. This class
* inherited by IFileTree so that operations on entry are the same for a file or
@@ -631,20 +624,6 @@ public: // Walk & Glob operations
};
enum class GlobPatternType
{
/**
* @brief Glob mode, similar to python pathlib.Path.glob function
*/
GLOB,
/**
* @brief Regex mode, each part of the pattern (between / or \) is considered a
* regex, except for ** which is still considered as glob.
*/
REGEX
};
/**
* @brief Walk this tree, calling the given function for each entry in it.
*
@@ -662,27 +641,6 @@ public: // Walk & Glob operations
callback,
QString sep = "\\") const;
/**
* @brief Walk this tree, returning entries.
*
* During the walk, parent tree are guaranteed to be visited before their childrens.
* The current tree is not included in the return generator.
*
* @return a generator over the entries.
*/
std::generator<std::shared_ptr<const FileTreeEntry>> walk() const;
/**
* @brief Glob entries matching the given pattern in this tree.
*
* @param pattern Glob pattern to match, using the same syntax as QRegularExpression.
* @param patternType Type of the pattern.
*
* @return a generator over the entries matching the given pattern.
*/
std::generator<std::shared_ptr<const FileTreeEntry>>
glob(QString pattern, GlobPatternType patternType = GlobPatternType::GLOB) const;
public: // Utility functions:
/**
* @brief Create a new orphan empty tree.
@@ -1161,4 +1119,12 @@ private: // Private stuff, look away!
} // namespace MOBase
// __has_cpp_attribute(__cpp_lib_generator) does not seem to work, maybe some conflict
// with Qt?
#ifdef __cpp_lib_generator
#include "ifiletree_utils.h"
#endif
#endif
+82
View File
@@ -0,0 +1,82 @@
/*
Mod Organizer shared UI functionality
Copyright (C) 2020 MO2 Team. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef UIBASE_IFILETREE_UTILS_H
#define UIBASE_IFILETREE_UTILS_H
#include <generator>
#include <QString>
#include "dllimport.h"
#include "exceptions.h"
#include "ifiletree.h"
namespace MOBase
{
/**
* @brief Exception thrown when an invalid glob pattern is specified.
*/
struct QDLLEXPORT InvalidGlobPatternException : public Exception
{
using Exception::Exception;
};
enum class GlobPatternType
{
/**
* @brief Glob mode, similar to python pathlib.Path.glob function
*/
GLOB,
/**
* @brief Regex mode, each part of the pattern (between / or \) is considered a
* regex, except for ** which is still considered as glob.
*/
REGEX
};
/**
* @brief Walk this tree, returning entries.
*
* During the walk, parent tree are guaranteed to be visited before their childrens.
* The current tree is not included in the return generator.
*
* @return a generator over the entries.
*/
QDLLEXPORT std::generator<std::shared_ptr<const FileTreeEntry>>
walk(std::shared_ptr<const IFileTree> fileTree);
/**
* @brief Glob entries matching the given pattern in this tree.
*
* @param pattern Glob pattern to match, using the same syntax as QRegularExpression.
* @param patternType Type of the pattern.
*
* @return a generator over the entries matching the given pattern.
*/
QDLLEXPORT std::generator<std::shared_ptr<const FileTreeEntry>>
glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
GlobPatternType patternType = GlobPatternType::GLOB);
} // namespace MOBase
#endif
+1
View File
@@ -35,6 +35,7 @@ set(root_headers
)
set(interface_headers
../include/uibase/ifiletree.h
../include/uibase/ifiletree_utils.h
../include/uibase/iinstallationmanager.h
../include/uibase/imodinterface.h
../include/uibase/imodlist.h
+174 -178
View File
@@ -180,184 +180,6 @@ void IFileTree::walk(
}
}
std::generator<std::shared_ptr<const FileTreeEntry>> IFileTree::walk() const
{
std::stack<std::shared_ptr<const FileTreeEntry>> stack;
// we start by pushing all the entries in this tree, this avoid having to do extra
// check later to avoid leading separator
for (const auto& entry : entries() | std::views::reverse) {
stack.push(entry);
}
while (!stack.empty()) {
auto entry = stack.top();
stack.pop();
co_yield entry;
if (entry->isDir()) {
auto tree = entry->astree();
for (const auto& child : tree->entries() | std::views::reverse) {
stack.push(child);
}
}
}
}
namespace
{
using glob_stack_t = std::stack<
std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>;
// check the given entry against the list of patterns and add new (entry, patterns) to
// the given stack
//
std::generator<std::shared_ptr<const FileTreeEntry>>
ifiletree_glob_impl_step(glob_stack_t& glob_stack,
std::shared_ptr<const FileTreeEntry> entry,
std::span<QRegularExpression> const& patterns)
{
// no more patterns, nothing to do
if (patterns.size() == 0) {
co_return;
}
// special handling if the pattern is empty (correspond to a '**' glob)
if (patterns[0].pattern() == "") {
// if there are more patterns after '**', we need to check the entry again, e.g.,
// if the entry name is 'x' and the pattern is '**/x' to match it
if (patterns.size() != 1) {
glob_stack.emplace(entry, patterns.subspan(1));
}
// if the entry is a file, there is nothing to do with '**'
if (entry->isFile()) {
co_return;
}
// if this is the end of the patterns list, we need to yield the current entry
// since it is a directory
if (patterns.size() == 1) {
co_yield entry;
}
// recurse over childs, but for directories, we need to keep the leading '**' in
// the list of patterns since '**' can match multiple level of directories
auto tree = entry->astree();
for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1));
}
}
// otherwise (if the first patterns is not '**'), we simply check if we have a match
else if (patterns[0].match(entry->name()).hasMatch()) {
// this was the last pattern and we have a match, so we yield the current entry,
// not that this will yield intermediate matching directory, but this is expected
if (patterns.size() == 1) {
co_yield entry;
}
// if the entry is not a directory, we have nothing more to do
if (entry->isFile()) {
co_return;
}
// if all that remain after this pattern is a '**', we need to yield the current
// entry since '**' can also match an empty succession of directories, e.g. 'a/b'
// is matched by 'a/b/**'
if (patterns.size() == 2 && patterns[1].pattern() == "") {
co_yield entry;
}
// we then need to recurse over
auto tree = entry->astree();
for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
glob_stack.emplace(*rit, patterns.subspan(1));
}
}
}
// main function for IFileTree::glob - this function simply manages the stack of of
// entry / patterns to handle, and then falls back to ifiletree_glob_impl_step
//
std::generator<std::shared_ptr<const FileTreeEntry>>
ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry,
std::span<QRegularExpression> const& patterns)
{
glob_stack_t stack;
stack.emplace(entry, patterns);
while (!stack.empty()) {
const auto [entry, patterns] = stack.top();
stack.pop();
co_yield std::ranges::elements_of(
ifiletree_glob_impl_step(stack, entry, patterns));
}
}
// actual implementation for IFileTree::glob
//
std::generator<std::shared_ptr<const FileTreeEntry>>
ifiletree_glob_impl(std::shared_ptr<const IFileTree> tree, QString pattern,
IFileTree::GlobPatternType patternType)
{
// replace \\ by / to simply handling
pattern = pattern.trimmed().replace("\\", "/");
// reduce successions of **/** to **, this makes it easier to handle it in the
// actual implementation
pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**");
// we are going to match directly starting from the child of the tree, so we need
// to handle the root here
//
// note: this is the only pattern that can match the tree itself
if (pattern == "**") {
co_yield tree;
}
// split pattern into blocks, we keep
const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive
? QRegularExpression::CaseInsensitiveOption
: QRegularExpression::NoPatternOption;
std::vector<QRegularExpression> patterns;
for (const auto& part : pattern.split("/")) {
if (part == "**") {
// for '**' pattern, push an empty regex
patterns.emplace_back();
} else {
const auto pattern =
patternType == IFileTree::GlobPatternType::GLOB
? QRegularExpression::wildcardToRegularExpression(
part, QRegularExpression::NonPathWildcardConversion)
: part;
const QRegularExpression regex(pattern, regexOptions);
if (!regex.isValid()) {
throw InvalidGlobPatternException(regex.errorString());
}
patterns.push_back(regex);
}
}
// fall back to the main glob function for the child of this tree
for (const auto& child : *tree) {
co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns));
}
}
} // namespace
/**
*
*/
std::generator<std::shared_ptr<const FileTreeEntry>>
IFileTree::glob(QString pattern, GlobPatternType patternType) const
{
return ifiletree_glob_impl(astree(), pattern, patternType);
}
/**
*
*/
@@ -1030,4 +852,178 @@ void IFileTree::populate() const
}
}
// walk and glob with generator
std::generator<std::shared_ptr<const FileTreeEntry>>
walk(std::shared_ptr<const IFileTree> fileTree)
{
std::stack<std::shared_ptr<const FileTreeEntry>> stack;
// we start by pushing all the entries in this tree, this avoid having to do extra
// check later to avoid leading separator
for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) {
stack.push(*rit);
}
while (!stack.empty()) {
auto entry = stack.top();
stack.pop();
co_yield entry;
if (entry->isDir()) {
auto tree = entry->astree();
for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
stack.push(*rit);
}
}
}
}
namespace
{
using glob_stack_t = std::stack<
std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>;
// check the given entry against the list of patterns and add new (entry, patterns) to
// the given stack
//
std::generator<std::shared_ptr<const FileTreeEntry>>
ifiletree_glob_impl_step(glob_stack_t& glob_stack,
std::shared_ptr<const FileTreeEntry> entry,
std::span<QRegularExpression> const& patterns)
{
// no more patterns, nothing to do
if (patterns.size() == 0) {
co_return;
}
// special handling if the pattern is empty (correspond to a '**' glob)
if (patterns[0].pattern() == "") {
// if there are more patterns after '**', we need to check the entry again, e.g.,
// if the entry name is 'x' and the pattern is '**/x' to match it
if (patterns.size() != 1) {
glob_stack.emplace(entry, patterns.subspan(1));
}
// if the entry is a file, there is nothing to do with '**'
if (entry->isFile()) {
co_return;
}
// if this is the end of the patterns list, we need to yield the current entry
// since it is a directory
if (patterns.size() == 1) {
co_yield entry;
}
// recurse over childs, but for directories, we need to keep the leading '**' in
// the list of patterns since '**' can match multiple level of directories
auto tree = entry->astree();
for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1));
}
}
// otherwise (if the first patterns is not '**'), we simply check if we have a match
else if (patterns[0].match(entry->name()).hasMatch()) {
// this was the last pattern and we have a match, so we yield the current entry,
// not that this will yield intermediate matching directory, but this is expected
if (patterns.size() == 1) {
co_yield entry;
}
// if the entry is not a directory, we have nothing more to do
if (entry->isFile()) {
co_return;
}
// if all that remain after this pattern is a '**', we need to yield the current
// entry since '**' can also match an empty succession of directories, e.g. 'a/b'
// is matched by 'a/b/**'
if (patterns.size() == 2 && patterns[1].pattern() == "") {
co_yield entry;
}
// we then need to recurse over
auto tree = entry->astree();
for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
glob_stack.emplace(*rit, patterns.subspan(1));
}
}
}
// main function for IFileTree::glob - this function simply manages the stack of of
// entry / patterns to handle, and then falls back to ifiletree_glob_impl_step
//
std::generator<std::shared_ptr<const FileTreeEntry>>
ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry,
std::span<QRegularExpression> const& patterns)
{
glob_stack_t stack;
stack.emplace(entry, patterns);
while (!stack.empty()) {
const auto [childEntry, childPatterns] = stack.top();
stack.pop();
co_yield std::ranges::elements_of(
ifiletree_glob_impl_step(stack, childEntry, childPatterns));
}
}
} // namespace
/**
*
*/
std::generator<std::shared_ptr<const FileTreeEntry>>
glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
GlobPatternType patternType)
{
// replace \\ by / to simply handling
pattern = pattern.trimmed().replace("\\", "/");
// reduce successions of **/** to **, this makes it easier to handle it in the
// actual implementation
pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**");
// we are going to match directly starting from the child of the tree, so we need
// to handle the root here
//
// note: this is the only pattern that can match the tree itself
if (pattern == "**") {
co_yield fileTree;
}
// split pattern into blocks, we keep
const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive
? QRegularExpression::CaseInsensitiveOption
: QRegularExpression::NoPatternOption;
std::vector<QRegularExpression> patterns;
for (const auto& part : pattern.split("/")) {
if (part == "**") {
// for '**' pattern, push an empty regex
patterns.emplace_back();
} else {
const auto regexpPattern =
patternType == GlobPatternType::GLOB
? QRegularExpression::wildcardToRegularExpression(
part, QRegularExpression::NonPathWildcardConversion)
: part;
const QRegularExpression regex(regexpPattern, regexOptions);
if (!regex.isValid()) {
throw InvalidGlobPatternException(regex.errorString());
}
patterns.push_back(regex);
}
}
// fall back to the main glob function for the child of this tree
for (const auto& child : *fileTree) {
co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns));
}
}
} // namespace MOBase
+32 -32
View File
@@ -890,14 +890,14 @@ TEST(IFileTreeTest, TreeWalkOperations)
// same as above but with generator version
{
// Populate the vector:
auto entries = fileTree->walk() | std::ranges::to<std::vector>();
auto entries = walk(fileTree) | std::ranges::to<std::vector>();
decltype(entries) expected{map["a"], map["b"], map["b/u"], map["b/v"],
map["e"], map["e/q"], map["e/q/p"], map["e/q/c.t"],
map["c.x"], map["d.y"]};
EXPECT_EQ(entries, expected);
entries.clear();
for (const auto entry : fileTree->walk()) {
for (const auto entry : walk(fileTree)) {
if (entry->name() == "e") {
break; // Stop on e
}
@@ -921,7 +921,7 @@ TEST(IFileTreeTest, TreeGlobOperations)
{
using entrySet = std::unordered_set<std::shared_ptr<const FileTreeEntry>>;
const auto REGEX = IFileTree::GlobPatternType::REGEX;
const auto REGEX = GlobPatternType::REGEX;
{
auto fileTree = FileListTree::makeTree({{"a/", true},
@@ -939,61 +939,61 @@ TEST(IFileTreeTest, TreeGlobOperations)
entrySet entries, expected;
entries = fileTree->glob("*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>();
expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob(".*", REGEX) | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, ".*", REGEX) | std::ranges::to<std::unordered_set>();
expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>();
expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**", REGEX) | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**", REGEX) | std::ranges::to<std::unordered_set>();
expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("*.x") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*.x") | std::ranges::to<std::unordered_set>();
expected = {map["c.x"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob(".*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, ".*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
expected = {map["c.x"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/*.x") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/*.x") | std::ranges::to<std::unordered_set>();
expected = {map["c.x"], map["e/q/m.x"]};
EXPECT_EQ(entries, expected);
entries =
fileTree->glob("**/.*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
glob(fileTree, "**/.*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
expected = {map["c.x"], map["e/q/m.x"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("*.t") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*.t") | std::ranges::to<std::unordered_set>();
expected = {};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/*.t") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/*.t") | std::ranges::to<std::unordered_set>();
expected = {map["a/g.t"], map["e/q/c.t"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("a/*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "a/*") | std::ranges::to<std::unordered_set>();
expected = {map["a/g.t"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("a/.*", REGEX) | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "a/.*", REGEX) | std::ranges::to<std::unordered_set>();
expected = {map["a/g.t"]};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/*.[xt]") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/*.[xt]") | std::ranges::to<std::unordered_set>();
expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]};
EXPECT_EQ(entries, expected);
entries =
fileTree->glob("**/.*[.][xt]", REGEX) | std::ranges::to<std::unordered_set>();
glob(fileTree, "**/.*[.][xt]", REGEX) | std::ranges::to<std::unordered_set>();
expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]};
EXPECT_EQ(entries, expected);
}
@@ -1050,17 +1050,17 @@ TEST(IFileTreeTest, TreeGlobOperations)
entrySet entries, expected;
entries = fileTree->glob("*.h") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*.h") | std::ranges::to<std::unordered_set>();
expected = {};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>();
expected = {map.at("aq.js"), map.at("bb"), map.at("cm.tx"), map.at("dp.js"),
map.at("ev"), map.at("go.ya"), map.at("gw.md"), map.at("hh"),
map.at("hl"), map.at("in"), map.at("mz"), map.at("sc")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("*/*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*/*") | std::ranges::to<std::unordered_set>();
expected = {
map.at("bb/ce.cp"), map.at("bb/cm.tx"), map.at("bb/gw"), map.at("bb/iw.cp"),
map.at("bb/js"), map.at("bb/px.cp"), map.at("hl/ds.in"), map.at("in/nu"),
@@ -1069,20 +1069,20 @@ TEST(IFileTreeTest, TreeGlobOperations)
map.at("sc/nd.o"), map.at("sc/nv.o"), map.at("sc/rv.ui"), map.at("sc/tv.h")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("*/*/*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "*/*/*") | std::ranges::to<std::unordered_set>();
expected = {map.at("bb/gw/cp.qm"), map.at("bb/gw/hq.qm"), map.at("bb/gw/pu.ts"),
map.at("bb/gw/tu.ts"), map.at("bb/js/cm.tx"), map.at("bb/js/co.cp"),
map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"),
map.at("in/nu/xx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>();
expected = {fileTree, map.at("bb"), map.at("bb/gw"), map.at("bb/js"),
map.at("hl"), map.at("in"), map.at("in/nu"), map.at("in/nu/lw"),
map.at("in/nu/xx"), map.at("mz"), map.at("sc")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/*") | std::ranges::to<std::unordered_set>();
expected = {map.at("aq.js"),
map.at("bb"),
map.at("cm.tx"),
@@ -1131,41 +1131,41 @@ TEST(IFileTreeTest, TreeGlobOperations)
map.at("sc/tv.h")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/cm.tx") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/cm.tx") | std::ranges::to<std::unordered_set>();
expected = {map.at("cm.tx"), map.at("bb/cm.tx"), map.at("bb/js/cm.tx"),
map.at("sc/cm.tx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/sc/**/cm.tx") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/sc/**/cm.tx") | std::ranges::to<std::unordered_set>();
expected = {map.at("sc/cm.tx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("**/sc") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "**/sc") | std::ranges::to<std::unordered_set>();
expected = {map.at("sc")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("in/**") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "in/**") | std::ranges::to<std::unordered_set>();
expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("in/**/**") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "in/**/**") | std::ranges::to<std::unordered_set>();
expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("in/*/*") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "in/*/*") | std::ranges::to<std::unordered_set>();
expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"),
map.at("in/nu/xx")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("in/*/*.h") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "in/*/*.h") | std::ranges::to<std::unordered_set>();
expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("sc/**/*.cp") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "sc/**/*.cp") | std::ranges::to<std::unordered_set>();
expected = {map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/lr.cp")};
EXPECT_EQ(entries, expected);
entries = fileTree->glob("sc/**/n*.o") | std::ranges::to<std::unordered_set>();
entries = glob(fileTree, "sc/**/n*.o") | std::ranges::to<std::unordered_set>();
expected = {map.at("sc/nd.o"), map.at("sc/nv.o")};
EXPECT_EQ(entries, expected);
}