diff --git a/src/dotfile.cpp b/src/dotfile.cpp index 0d69881..c7ffa8b 100644 --- a/src/dotfile.cpp +++ b/src/dotfile.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: MIT */ +#include // assert #include // tolower #include // size_t #include // fprintf, printf, stderr @@ -362,30 +363,133 @@ void Dotfile::forEachDotfile(const std::vector& targets, const std: if (path.is_directory() || filter(path)) { continue; } - if (!targets.empty() && !include(path.path().string(), targets)) { + if (!targets.empty() && !include(path.path(), targets)) { continue; } callback(path, index++); } } -bool Dotfile::filter(const std::filesystem::path& path) +bool Dotfile::filter(const std::filesystem::directory_entry& path) { - for (auto& excludePath : Config::the().excludePaths()) { - if (excludePath.second == "file") { - if (path.string() == Config::the().workingDirectory() / excludePath.first) { - return true; - } + std::string pathString = path.path().string(); + assert(pathString.front() == '/'); + + // Cut off working directory + size_t cutFrom = pathString.find(Config::the().workingDirectory()) == 0 ? Config::the().workingDirectorySize() : 0; + pathString = pathString.substr(cutFrom); + + for (const auto& excludePathMapEntry : Config::the().excludePaths()) { + const auto& excludePath = excludePathMapEntry.first; + + if (pathString == excludePath) { + return true; } - else if (excludePath.second == "directory") { - if (path.string().find(Config::the().workingDirectory() / excludePath.first) == 0) { - return true; - } + + // If starts with '/', only match in the working directory root + bool onlyMatchInRoot = false; + if (excludePath.front() == '/') { + onlyMatchInRoot = true; + } + + // If ends with '/', only match directories + bool onlyMatchDirectories = false; + if (excludePath.back() == '/') { + onlyMatchDirectories = true; } - else if (excludePath.second == "endsWith") { - if (path.string().find(excludePath.first) == path.string().size() - excludePath.first.size()) { - return true; + + // Parsing + + bool tryPatternState = true; + + size_t pathIterator = 0; + size_t excludeIterator = 0; + + if (!onlyMatchInRoot) { + pathIterator++; + } + + // Current path charter 'x' == next ignore pattern characters '*x' + // Example, iterator at []: [.]log/output.txt + // [*].log + if (pathIterator < pathString.length() + && excludeIterator < excludePath.length() - 1 + && excludePath.at(excludeIterator) == '*' + && pathString.at(pathIterator) == excludePath.at(excludeIterator + 1)) { + excludeIterator++; + } + + for (; pathIterator < pathString.length() && excludeIterator < excludePath.length();) { + char character = pathString.at(pathIterator); + pathIterator++; + + if (!tryPatternState && character == '/') { + tryPatternState = true; + continue; + } + + if (!tryPatternState) { + continue; + } + + if (character == excludePath.at(excludeIterator)) { + // Fail if the final match hasn't reached the end of the ignore pattern + // Example, iterator at []: doc/buil[d] + // buil[d]/ + if (pathIterator == pathString.length() && excludeIterator < excludePath.length() - 1) { + break; + } + + // Next path character 'x' == next ignore pattern characters '*x', skip the '*' + // Example, iterator at []: /includ[e]/header.h + // /includ[e]*/ + if (pathIterator < pathString.length() + && excludeIterator < excludePath.length() - 2 + && excludePath.at(excludeIterator + 1) == '*' + && pathString.at(pathIterator) == excludePath.at(excludeIterator + 2)) { + excludeIterator++; + } + + excludeIterator++; + continue; + } + + if (excludePath.at(excludeIterator) == '*') { + // Fail if we're entering a subdirectory and we should only match in the root + // Example, iterator at []: /src[/]include/header.h + // /[*]include/ + if (onlyMatchInRoot && character == '/') { + break; + } + + // Next path character == next ignore pattern character + if (pathIterator < pathString.length() + && excludeIterator + 1 < excludePath.length() + && pathString.at(pathIterator) == excludePath.at(excludeIterator + 1)) { + excludeIterator++; + } + + continue; } + + // Reset filter pattern if it hasnt been completed at this point + // Example, iterator at []: /[s]rc/include/header.h + // /[i]nclude*/ + if (excludeIterator < excludePath.length() - 1) { + excludeIterator = 0; + } + + tryPatternState = false; + } + + if (excludeIterator == excludePath.length()) { + return true; + } + if (excludePath.back() == '*' && excludeIterator == excludePath.length() - 1) { + return true; + } + if (onlyMatchDirectories && excludeIterator == excludePath.length() - 1) { + return true; } } diff --git a/src/dotfile.h b/src/dotfile.h index 25f2cdc..ac16348 100644 --- a/src/dotfile.h +++ b/src/dotfile.h @@ -39,7 +39,7 @@ private: void selectivelyCommentOrUncomment(const std::string& path); void forEachDotfile(const std::vector& targets, const std::function& callback); - bool filter(const std::filesystem::path& path); + bool filter(const std::filesystem::directory_entry& path); bool include(const std::filesystem::path& path, const std::vector& targets); bool isSystemTarget(const std::string& target); }; diff --git a/test/unit/testdotfile.cpp b/test/unit/testdotfile.cpp index f8db6f3..b569df1 100644 --- a/test/unit/testdotfile.cpp +++ b/test/unit/testdotfile.cpp @@ -213,8 +213,8 @@ TEST_CASE(PushDotfilesWithExcludePath) Config::the().setExcludePaths({ { "__test-file-1", "file" }, - { "__subdir", "directory" }, - { ".test", "endsWith" }, + { "__subdir/", "directory" }, + { "*.test", "endsWith" }, }); Dotfile::the().push(fileNames); Config::the().setExcludePaths({});