/** * PANDA 3D SOFTWARE * Copyright (c) Carnegie Mellon University. All rights reserved. * * All use of this software is subject to the terms of the revised BSD * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * * @file globPattern.cxx * @author drose * @date 2000-05-30 */ #include "globPattern.h" #include "string_utils.h" #include using std::string; /** * Returns true if the pattern includes any special globbing characters, or * false if it is just a literal string. */ bool GlobPattern:: has_glob_characters() const { string::const_iterator pi; pi = _pattern.begin(); while (pi != _pattern.end()) { switch (*pi) { case '*': case '?': case '[': return true; case '\\': ++pi; if (pi == _pattern.end()) { return false; } } ++pi; } return false; } /** * Returns the initial part of the pattern before the first glob character. * Since many glob patterns begin with a sequence of static characters and end * with one or more glob characters, this can be used to optimized searches * through sorted indices. */ string GlobPattern:: get_const_prefix() const { string prefix; size_t p = 0; // current point size_t q = 0; // starting point while (p < _pattern.size()) { switch (_pattern[p]) { case '*': case '?': case '[': return prefix + _pattern.substr(q, p - q); case '\\': // Skip over the backslash. prefix += _pattern.substr(q, p - q); ++p; q = p; } ++p; } return prefix += _pattern.substr(q, p - q); } /** * Treats the GlobPattern as a filename pattern, and returns a list of any * actual files that match the pattern. This is the behavior of the standard * Posix glob() function. Any part of the filename may contain glob * characters, including intermediate directory names. * * If cwd is specified, it is the directory that relative filenames are taken * to be relative to; otherwise, the actual current working directory is * assumed. * * The return value is the number of files matched, which are added to the * results vector. */ int GlobPattern:: match_files(vector_string &results, const Filename &cwd) const { string prefix, pattern, suffix; string source = _pattern; if (!source.empty() && source[0] == '/') { // If the first character is a slash, that becomes the prefix. prefix = "/"; source = source.substr(1); } size_t slash = source.find('/'); if (slash == string::npos) { pattern = source; } else { pattern = source.substr(0, slash); suffix = source.substr(slash); } GlobPattern glob(pattern); glob.set_case_sensitive(_case_sensitive); return glob.r_match_files(prefix, suffix, results, cwd); } /** * The recursive implementation of match_files(). */ int GlobPattern:: r_match_files(const Filename &prefix, const string &suffix, vector_string &results, const Filename &cwd) { string next_pattern, next_suffix; size_t slash = suffix.find('/'); if (slash == string::npos) { next_pattern = suffix; } else if (slash + 1 == suffix.size()) { // If the slash is at the end, we need to keep it, since it indicates that // we only want to match directories. next_pattern = suffix.substr(0, slash); next_suffix = "/"; } else { next_pattern = suffix.substr(0, slash); next_suffix = suffix.substr(slash + 1); } if (_pattern == "**" && next_pattern == "**") { // Collapse consecutive globstar patterns. return r_match_files(prefix, next_suffix, results, cwd); } Filename parent_dir; if (prefix.is_local() && !cwd.empty()) { parent_dir = Filename(cwd, prefix); } else { parent_dir = prefix; } GlobPattern next_glob(next_pattern); next_glob.set_case_sensitive(_case_sensitive); if (!has_glob_characters()) { // If there are no special characters in the pattern, it's a literal // match. Filename fn(parent_dir, _pattern); if (suffix.empty()) { // Time to stop. if (fn.exists()) { results.push_back(Filename(prefix, _pattern)); return 1; } return 0; } else if (fn.is_directory()) { // If the pattern ends with a slash, match a directory only. if (suffix == "/") { results.push_back(Filename(prefix, _pattern + "/")); return 1; } else { return next_glob.r_match_files(Filename(prefix, _pattern), next_suffix, results, cwd); } } } // If there *are* special glob characters, we must attempt to match the // pattern against the files in this directory. vector_string dir_files; if (!parent_dir.scan_directory(dir_files)) { // Not a directory, or unable to read directory; stop here. return 0; } // Now go through each file in the directory looking for one that matches // the pattern. int num_matched = 0; // A globstar pattern matches zero or more directories. if (_pattern == "**") { // Try to match this directory (as if the globstar wasn't there) if (suffix.empty()) { // This is a directory. Add it. results.push_back(Filename(prefix)); num_matched++; } else if (suffix == "/") { // Keep the trailing slash, but be sure not to duplicate it. results.push_back(Filename(prefix, "")); num_matched++; } else { num_matched += next_glob.r_match_files(prefix, next_suffix, results, cwd); } next_suffix = suffix; next_glob = *this; } for (const string &local_file : dir_files) { if (_pattern[0] == '.' || (local_file.empty() || local_file[0] != '.')) { if (matches(local_file)) { // We have a match; continue. if (Filename(parent_dir, local_file).is_directory()) { if (suffix.empty() && _pattern != "**") { results.push_back(Filename(prefix, local_file)); num_matched++; } else if (suffix == "/" && _pattern != "**") { results.push_back(Filename(prefix, local_file + "/")); num_matched++; } else { num_matched += next_glob.r_match_files(Filename(prefix, local_file), next_suffix, results, cwd); } } else if (suffix.empty()) { results.push_back(Filename(prefix, local_file)); num_matched++; } } } } return num_matched; } /** * Treats the GlobPattern as a filename pattern, and returns true if the given * filename matches the pattern. Unlike matches(), this will not match slash * characters for single asterisk characters, and it will ignore path * components that only contain a dot. */ bool GlobPattern:: matches_file(Filename candidate) const { if (_pattern.empty()) { // Special case. return candidate.empty(); } // Either both must be absolute, or both must be relative. if ((_pattern[0] != '/') != candidate.is_local()) { return false; } return r_matches_file(_pattern, candidate); } /** * The recursive implementation of matches_file(). */ bool GlobPattern:: r_matches_file(const string &pattern, const Filename &candidate) const { // Split off the next bit of pattern. std::string next_pattern; GlobPattern glob; glob.set_case_sensitive(get_case_sensitive()); glob.set_nomatch_chars(get_nomatch_chars()); bool pattern_end; size_t slash = pattern.find('/'); if (slash == string::npos) { glob.set_pattern(pattern); pattern_end = true; } else { glob.set_pattern(pattern.substr(0, slash)); next_pattern = pattern.substr(slash + 1); pattern_end = false; if (slash == 0 || (slash == 1 && pattern[0] == '.')) { // Ignore // and /./ in patterns return r_matches_file(next_pattern, candidate); } } // Also split off the next component in the candidate filename. std::string part; Filename next_candidate; bool candidate_end; size_t fn_slash = ((const std::string &)candidate).find('/'); if (fn_slash == string::npos) { part = candidate; candidate_end = true; } else { part = candidate.substr(0, fn_slash); next_candidate = candidate.substr(fn_slash + 1); candidate_end = false; // Ignore // and /./ in filenames. if (fn_slash == 0 || part == ".") { return r_matches_file(pattern, next_candidate); } } // Now check if the current part matches the current pattern. bool part_matches; if (glob.get_pattern() == "**") { // This matches any number of parts. if (pattern_end) { // We might as well stop checking here; it matches whatever might come. return true; } // We branch out to three options: either we match nothing, we match this // part only, or we match this part and maybe more. return r_matches_file(next_pattern, candidate) || (!candidate_end && r_matches_file(next_pattern, next_candidate)) || (!candidate_end && r_matches_file(pattern, next_candidate)); } else if (glob.get_pattern() == "*" && _nomatch_chars.empty()) { // Matches any part (faster version of below) part_matches = true; } else if ((glob.get_pattern() == "." && part.empty()) || (glob.get_pattern().empty() && part == ".")) { // So that /path/. matches /path/, and vice versa. part_matches = true; } else if (glob.has_glob_characters()) { part_matches = glob.matches(part); } else if (get_case_sensitive()) { part_matches = (part == glob.get_pattern()); } else { part_matches = (cmp_nocase(part, glob.get_pattern()) == 0); } if (!part_matches) { // It doesn't match, so we end our search here. return false; } if (candidate_end && pattern_end) { // We've reached the end of both candidate and pattern, so it matches. return true; } if (pattern_end != candidate_end) { // One of them has ended, but the other hasn't, so it's not a match. return false; } // It matches; move on to the next part. return r_matches_file(next_pattern, next_candidate); } /** * The recursive implementation of matches(). This returns true if the * pattern substring [pi, pend) matches the candidate substring [ci, cend), * false otherwise. */ bool GlobPattern:: matches_substr(string::const_iterator pi, string::const_iterator pend, string::const_iterator ci, string::const_iterator cend) const { // If we run out of pattern or candidate string, it's a match only if they // both ran out at the same time. if (pi == pend || ci == cend) { // A special exception: we allow ci to reach the end before pi, only if pi // is one character before the end and that last character is '*'. if ((ci == cend) && (std::distance(pi, pend) == 1) && (*pi) == '*') { return true; } return (pi == pend && ci == cend); } switch (*pi) { case '*': // A '*' in the pattern string means to match any sequence of zero or more // characters in the candidate string. This means we have to recurse // twice: either consume one character of the candidate string and // continue to try matching the *, or stop trying to match the * here. if (_nomatch_chars.find(*ci) == string::npos) { return matches_substr(pi, pend, ci + 1, cend) || matches_substr(pi + 1, pend, ci, cend); } else { // On the other hand, if this is one of the nomatch chars, we can only // stop here. return matches_substr(pi + 1, pend, ci, cend); } case '?': // A '?' in the pattern string means to match exactly one character in the // candidate string. That's easy. return matches_substr(pi + 1, pend, ci + 1, cend); case '[': // An open square bracket begins a set. ++pi; if ((*pi) == '!') { ++pi; if (matches_set(pi, pend, *ci)) { return false; } } else { if (!matches_set(pi, pend, *ci)) { return false; } } if (pi == pend) { // Oops, there wasn't a closing square bracket. return false; } return matches_substr(pi + 1, pend, ci + 1, cend); case '\\': // A backslash escapes the next special character. ++pi; if (pi == pend) { return false; } // fall through. default: // Anything else means to match exactly that. if (_case_sensitive) { if ((*pi) != (*ci)) { return false; } } else { if (tolower(*pi) != tolower(*ci)) { return false; } } return matches_substr(pi + 1, pend, ci + 1, cend); } } /** * Called when an unescaped open square bracked is scanned, this is called * with pi positioned after the opening square bracket, scans the set * sequence, leaving pi positioned on the closing square bracket, and returns * true if the indicated character matches the set of characters indicated, * false otherwise. */ bool GlobPattern:: matches_set(string::const_iterator &pi, string::const_iterator pend, char ch) const { bool matched = false; while (pi != pend && (*pi) != ']') { if ((*pi) == '\\') { // Backslash escapes the next character. ++pi; if (pi == pend) { return false; } } if (ch == (*pi)) { matched = true; } // Maybe it's an a-z style range? char start = (*pi); ++pi; if (pi != pend && (*pi) == '-') { ++pi; if (pi != pend && (*pi) != ']') { // Yes, we have a range: start-end. if ((*pi) == '\\') { // Backslash escapes. ++pi; if (pi == pend) { return false; } } char end = (*pi); ++pi; if ((ch >= start && ch <= end) || (!_case_sensitive && ((tolower(ch) >= start && tolower(ch) <= end) || (toupper(ch) >= start && toupper(ch) <= end)))) { matched = true; } } else { // This was a - at the end of the string. if (ch == '-') { matched = true; } } } } return matched; }