[binutils-gdb] gdb: introduce '**' for skip globs
Andrew Burgess via Gdb-cvs <[email protected]>
| Newsgroups | gmane.comp.gdb.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=5ed98c177f9a2381fe5c6ab873d05f1e5dcb8f7f commit 5ed98c177f9a2381fe5c6ab873d05f1e5dcb8f7f Author: Andrew Burgess <[email protected]> Date: Wed Feb 4 20:31:31 2026 +0000 gdb: introduce '**' for skip globs There are cases when a user might wish to skip every file of a particular type within a directory, and all its sub-directories. Currently, you'll need to account for each sub-directory in your skip patterns, so given: /include/a.h /include/xxx/b.h /include/yyy/zzz/c.h To skip all of these requires: skip -gfile /include/*.h skip -gfile /include/*/*.h skip -gfile /include/*/*/*.h Which isn't really that much, but if another layer of sub-directory is added then you'll need to add yet another skip pattern. This commit introduces '**' which is modelled after the bash globstar feature. The '**' matches 0 or more directories. You can now write: skip -gfile /include/**/*.h And this will skip all of the files listed above in a single command. There was an earlier attempt to solve this problem with commit: commit 02646a4c561ec88491114b87950cbb827c7d614c Date: Sun Dec 29 14:57:44 2024 -0800 skip -gfile: call fnmatch without FNM_FILE_NAME But I reverted this in commit: commit f08ffbbf2691bad2d5df660ee644647687775f0c Date: Mon Feb 9 16:31:23 2026 +0000 Revert "skip -gfile: call fnmatch without FNM_FILE_NAME" Due to bug PR gdb/33872. The initial commit also changed GDB in a non-backward compatible way, that is 'skip -gfile /include/*.h' would now match all .h files in every sub-directory of /include/, which might not be what the user wants. The approach taken in this commit avoids changing this behaviour and allows the user to better select what they want to match. My initial implementation can be found here: https://inbox.sourceware.org/gdb-patches/[email protected] This worked by splitting both the filename and the glob on every '/' and then using a recursive algorithm to match each part of the file name. This initial approach only split on '/' which means there would have been some regressions on DOS based file systems where '\' can be used as a directory separator, though this would probably have been easy enough to fix. However, it was pointed out that the splitting and matching was rather inefficient, and it might be better to convert the glob into a regexp and use that for matching. I figured; how hard can that be, which is how this version of the patch came to be. Turns out it's not that simple. I'm not going to go though all the details here, but the basic idea is that, when the user creates a glob skip GDB calls glob_to_regexp to convert the glob to a regexp, which is then compiled and stored in the skiplist_entry. The regexp can then be used as you'd expect to check for matches. The matching is now done in a global function do_skip_gfile_p, which makes it easier to write unit tests. The skiplist_entry::do_skip_gfile_p just calls the global function. I've retained the initial file basename check, which still uses fnmatch, as this is likely quicker than performing the regexp match, at least, I hope so, I've not tried to benchmark anything. Converting a glob to a regexp is mostly straight forward, except for bracket expressions, we dont want these to match against directory separators, so we need to filter the directory separators out, this retains compatibility with the fnmatch FNM_PATHNAME flag. It is this filtering that is the cause of most of the pain. The details for all of this can be found in glob_bracket_expr::parse. We also need to take care to handle case insensitive file systems. This is mostly just adding REG_ICASE to the regexp flags, however, there are some issues with, you guessed it, bracket expressions. If we have a character range that spans out of the upper case letter set, for example [W-_], then, at least for glibc, when REG_ICASE is set this ends up being treated like [w-_]. This is unfortunate because 'w' is after '_' so the range is now invalid. To resolve this we end up splitting the range into two giving: [W-Z[-_], now when glibc adjusts to lower case, we end up matching with [w-z[-_]. For DOS like file systems, the created regexp adds a pattern that matches both '/' and '\' whenever either of these is seen in the glob. But there's also some additional work needed for, you guessed it, bracket expressions; we need to ensure '\' is removed from any ranges. One final issue with, you guessed it, bracket expressions, is character classes, e.g. [:alpha:]. Many classes are fine, but some, like [:punct:] include '/' and '\'. For now I just raise an error if the user tries to use a class that includes a directory separator. This is a change in behaviour, but hopefully isn't going to impact too many people. The only fix I can currently see for this would be to expand the problematic character classes into their component characters, and then remove the directory separators. But I haven't tried to do that just yet. There are documentation updates and tests for the new feature. All existing behaviour should remain unchanged. On the testing side I've added both some full DejaGNU tests and some self tests. The self tests are focused on just the core glob matching function, but the full DejaGNU tests ensure that the whole mechanism, from skip creation to its final usage, is also tested. Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=33872 Reviewed-By: Eli Zaretskii <[email protected]> Reviewed-By: Keith Seitz <[email protected]> Diff: --- gdb/doc/gdb.texinfo | 38 ++ gdb/skip.c | 1103 +++++++++++++++++++++++++++++++++- gdb/symtab.c | 34 -- gdb/symtab.h | 3 - gdb/testsuite/gdb.base/skip-tree-1.c | 26 + gdb/testsuite/gdb.base/skip-tree-2.c | 26 + gdb/testsuite/gdb.base/skip-tree-3.c | 26 + gdb/testsuite/gdb.base/skip-tree-4.c | 26 + gdb/testsuite/gdb.base/skip-tree.c | 38 ++ gdb/testsuite/gdb.base/skip-tree.exp | 199 ++++++ gdb/testsuite/gdb.base/skip.exp | 8 + 11 files changed, 1465 insertions(+), 62 deletions(-) diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index ceb69669ea6..16cd01aad4d 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -6964,6 +6964,44 @@ over when stepping. (@value{GDBP}) skip -gfile utils/*.c @end smallexample +If @var{file-glob-pattern} is an absolute filename, for example, on +Unix systems, if @var{file-glob-pattern} starts with @file{/}, then +the glob will only match full filenames. + +@smallexample +(@value{GDBP}) skip -gfile /include/*.h +@end smallexample + +In this case @file{/include/foo.h} would match, but +@file{/usr/include/foo.h} would not. In contrast@: + +@smallexample +(@value{GDBP}) skip -gfile include/*.h +@end smallexample + +This would match both @file{/include/foo.h} and +@file{/usr/include/foo.h} as the glob pattern is not an absolute +filename. + +In addition to the standard @code{glob} special characters, +@value{GDBN} supports @samp{**}. The @samp{**} pattern, called +@dfn{globstar}, can be used to match against 0 or more filename +components. For example, given the files @file{/include/a.h}, +@file{/include/xxx/b.h}, @file{/include/yyy/zzz/c.h}, then all of +these could be skipped using@: + +@smallexample +(@value{GDBP}) skip -gfile /include/**/*.h +@end smallexample + +The @samp{**} pattern must appear on its own within the pattern, it +cannot be combined with other characters, so @samp{include/**/*.h} +will provide the globstar behaviour, but @samp{include/x**/*.h} will +not as @samp{x**} combines the @samp{**} with the @samp{x} character. +The @samp{x**} will be treated as @samp{x} followed by two @samp{*} +characters, matching zero or more characters within that one directory +component. + @item -function @var{linespec} Functions named by @var{linespec} or the function containing the line named by @var{linespec} will be skipped over when stepping. diff --git a/gdb/skip.c b/gdb/skip.c index 06bf7ce485e..eb47cee9e34 100644 --- a/gdb/skip.c +++ b/gdb/skip.c @@ -40,6 +40,11 @@ #include "gdbsupport/buildargv.h" #include "safe-ctype.h" #include "readline/tilde.h" +#include "gdbsupport/selftest.h" + +#include <initializer_list> +#include <unordered_set> +#include <vector> /* True if we want to print debug printouts related to file/function skipping. */ @@ -124,6 +129,10 @@ private: /* data */ /* If this is a function regexp, the compiled form. */ std::optional<compiled_regex> m_compiled_function_regexp; + /* If this is a file glob, then we convert the glob to a regexp, and + place the compiled form here. */ + std::optional<compiled_regex> m_compiled_file_regexp; + /* Enabled/disabled state. */ bool m_enabled = true; }; @@ -131,6 +140,558 @@ private: /* data */ static std::list<skiplist_entry> skiplist_entries; static int highest_skiplist_entry_num = 0; +/* Structure to hold the results of parsing a glob bracket expression. */ + +struct glob_bracket_expr +{ + /* Type used to express a character range, e.g 'a-z'. In this case the + first item in the pair would be 'a', and the second item 'z'. */ + using char_range = std::pair<unsigned char, unsigned char>; + + /* Parse a bracket expression and return an instance of this class. If + the bracket expression is invalid then return an empty optional. PTR + must point to the '[' character that starts the bracket expression. */ + static std::optional<glob_bracket_expr> parse (const char *ptr); + + /* Convert the parsed bracket expression into a regular expression, and + return the regular expression as a string. */ + std::string to_string () const; + + /* Return a pointer to the end of the bracket expression. This will point + to the final ']' that closes the expression. This will never be NULL. */ + const char *end () const; + +private: + /* When true the character bracket expression is negated, that is we want + to match everything not mentioned in the expression. When false we + only want to match things mentioned in the expression. */ + bool m_negated = false; + + /* Each string is something like '[:alpha:]' and represents a character + class. If validation is wanted then it should be done before items + are added to this list, the contents of this list are not validated as + they are used. */ + std::vector<std::string> m_char_classes; + + /* Character ranges stored as pairs, first entry is the range start, + second entry is the range end. Both are inclusive. */ + std::vector<char_range> m_char_ranges; + + /* Single characters within the bracket expression. */ + std::unordered_set<char> m_chars; + + /* Points at the closing ']' for the bracket expression. */ + const char *m_end = nullptr; +}; + +/* Return true if PTR is at the start of a character class descriptor, + e.g. "[:alpha:]". This doesn't validate the actual character class name, + just that PTR is at the start of something that looks like a character + class. */ + +static bool +at_character_class (const char *ptr) +{ + if (*ptr != '[') + return false; + ++ptr; + + if (*ptr != ':') + return false; + ++ptr; + + /* Require at least one character in the class name. */ + if (!c_isalpha (*ptr)) + return false; + + while (c_isalpha (*ptr)) + ++ptr; + + if (*ptr != ':') + return false; + ++ptr; + + return *ptr == ']'; +} + +/* Return true if PTR is pointing to something like 'A-B', a character + range as could be found within a glob's bracket expression. */ + +static bool +at_character_range (const char *ptr) +{ + gdb_assert (*ptr != '\0'); + + if (*(ptr + 1) != '-') + return false; + + if (*(ptr + 2) == '\0' || *(ptr + 2) == ']') + return false; + + return true; +} + +/* Helper for sanitize_range. Pass RANGE to SHOULD_SPLIT, if it returns + true then call DO_SPLIT to split RANGE, placing the resulting ranges in + RESULTS. If SHOULD_SPLIT returns false then just place RANGE in + RESULTS. */ + +template<typename Pred, typename Split> +static void +split_a_range (std::vector<glob_bracket_expr::char_range> &results, + const glob_bracket_expr::char_range &range, + Pred should_split, Split do_split) +{ + if (should_split (range)) + do_split (results, range); + else + results.emplace_back (range); +} + +/* Helper for sanitize_range. For each range in RESULTS, if SHOULD_SPLIT + returns true, replace it with the sub-ranges produced by DO_SPLIT. + Otherwise keep it unchanged. */ + +template<typename Pred, typename Split> +static void +resplit_all_ranges (std::vector<glob_bracket_expr::char_range> &results, + Pred should_split, Split do_split) +{ + std::vector<glob_bracket_expr::char_range> temp; + for (const glob_bracket_expr::char_range &p : results) + split_a_range (temp, p, should_split, do_split); + results = std::move (temp); +} + +/* The character range START-END is taken from a filename glob. When + converting to a regular expression we cannot allow a directory + separator to appear within the range. So, if a directory separator + does appear between START and END (inclusive), split the range into + multiple ranges, excluding the directory separator. + + For Unix systems the only directory separator we check for is '/', but + on DOS like file systems we also check for '\'. + + On case insensitive filesystems we also need to be careful about ranges + that span into, or out of, the upper case character range. For glibc + at least, the REG_ICASE matching which we use (for case insensitive + file systems) lowers any uppercase characters it finds. So a range + like [W-_] becomes [w-_], which is invalid as 'w' is after '_'. To + avoid this problem we split ranges at 'A' and 'Z', so the above range + becomes [W-Z[-_], then when glibc lowers the letters this becomes + [w-z[-_] which is still valid. + + Return a vector of all the ranges needed to cover START to END but + exclude directory separators. */ + +static std::vector<glob_bracket_expr::char_range> +sanitize_range (unsigned char start, unsigned char end) +{ + /* Ranges that start or end at a directory separator are invalid, and + should have been filtered out before now. */ + gdb_assert (!IS_DIR_SEPARATOR (start) && !IS_DIR_SEPARATOR (end)); + + /* Backward ranges are invalid, and should have been filtered out before + now. */ + gdb_assert (start <= end); + + std::vector<glob_bracket_expr::char_range> results; + + /* Always split the range on '/'. This creates at most two ranges. */ + split_a_range (results, { start, end }, + [] (const glob_bracket_expr::char_range &p) + { return p.first < '/' && p.second > '/'; }, + [] (std::vector<glob_bracket_expr::char_range> &out, + const glob_bracket_expr::char_range &p) + { + out.emplace_back (p.first, '/' - 1); + out.emplace_back ('/' + 1, p.second); + }); + +#ifdef HAVE_DOS_BASED_FILE_SYSTEM + /* Exclude '\\' from ranges. The character after '\\' is ']', which + needs to be kept as a single-character range since it has special + meaning within bracket expressions, by keeping the ']' as a single + character range the ']' can be moved within the bracket expression to + a valid location. */ + resplit_all_ranges (results, + [] (const glob_bracket_expr::char_range &p) + { return p.first < '\\' && p.second > '\\'; }, + [] (std::vector<glob_bracket_expr::char_range> &out, + const glob_bracket_expr::char_range &p) + { + out.emplace_back (p.first, '\\' - 1); + gdb_assert ('\\' + 1 == ']'); + out.emplace_back (']', ']'); + if (p.second > ']') + out.emplace_back (']' + 1, p.second); + }); +#endif /* HAVE_DOS_BASED_FILE_SYSTEM */ + +#ifdef HAVE_CASE_INSENSITIVE_FILE_SYSTEM + /* Split ranges at the 'A' and 'Z' boundaries to allow for case + insensitive matching. glibc's ICASE matching lowers upper case + letters within ranges, so the range [W-_] becomes, with ICASE + matching, [w-_]. Unfortunately, 'w' is after '_' so this range is + now invalid. By splitting the range into [W-Z[-_] glibc is now free + to lower this to [w-z[-_] which is still valid. */ + resplit_all_ranges (results, + [] (const glob_bracket_expr::char_range &p) + { return p.first < 'A' && p.second >= 'A'; }, + [] (std::vector<glob_bracket_expr::char_range> &out, + const glob_bracket_expr::char_range &p) + { + out.emplace_back (p.first, 'A' - 1); + out.emplace_back ('A', p.second); + }); + resplit_all_ranges (results, + [] (const glob_bracket_expr::char_range &p) + { return p.first <= 'Z' && p.second > 'Z'; }, + [] (std::vector<glob_bracket_expr::char_range> &out, + const glob_bracket_expr::char_range &p) + { + out.emplace_back (p.first, 'Z'); + out.emplace_back ('Z' + 1, p.second); + }); +#endif /* HAVE_CASE_INSENSITIVE_FILE_SYSTEM */ + + return results; +} + +/* CHAR_CLASS should be a character class name like "[:name:]". Return + true if CHAR_CLASS matches a directory separator, which is just '/' + unless we're on a DOS like filesystem, in which case we check for '/' + or '\'. */ + +static bool +character_class_matches_dir_separator (const std::string &char_class) +{ + std::string pattern = string_printf ("[%s]", char_class.c_str ()); + compiled_regex re (pattern.c_str (), REG_NOSUB | REG_EXTENDED, + _("character class check")); + +#ifdef HAVE_DOS_BASED_FILE_SYSTEM + static constexpr const char *dir_sep_str = "/\\"; +#else + static constexpr const char *dir_sep_str = "/"; +#endif /* not HAVE_DOS_BASED_FILE_SYSTEM */ + + return re.exec (dir_sep_str, 0, nullptr, 0) == 0; +} + +/* See class declaration above. */ + +std::optional<glob_bracket_expr> +glob_bracket_expr::parse (const char *ptr) +{ + gdb_assert (*ptr == '['); + ++ptr; + + glob_bracket_expr result; + + /* POSIX defines '!' as the negation character within a bracket + expression, and says that a '^' as the first character is undefined. + Both fnmatch and bash treat a leading '^' as negation, which matches + the regexp behaviour. We match this. */ + if (*ptr == '!' || *ptr == '^') + { + result.m_negated = true; + result.m_chars.insert ('/'); + ptr++; + } + + char end_bracket_char = '\0'; + + for (; *ptr != '\0' && *ptr != end_bracket_char; ++ptr) + { + end_bracket_char = ']'; + + /* Is this a character range? */ + if (at_character_range (ptr)) + { + char range_start = *ptr; + char range_end = *(ptr + 2); + + /* If the range starts with ']' then the opening ']' must be the + first character in the bracket expression (after any + negation). Insert the ']' as a single character, and + increment the range start. We know the range_end cannot also + be ']' as that would close the bracket expression, and not be + a valid range. Having the ']' as a lone character make it + easier to place this in the correct place within the bracket + expression when we generate the regexp. */ + if (range_start == ']') + { + gdb_assert (range_end != ']'); + result.m_chars.insert (range_start); + range_start++; + } + + if (IS_DIR_SEPARATOR (range_start) || IS_DIR_SEPARATOR (range_end)) + return {}; + + if (range_end < range_start) + { + /* Don't use RANGE_START here as it might have been modified + above. We could use RANGE_END but don't just to be + consistent. */ + error (_("skip glob regexp: invalid character range %c-%c"), + *ptr, *(ptr + 2)); + } + + std::vector<glob_bracket_expr::char_range> ranges + = sanitize_range (range_start, range_end); + for (glob_bracket_expr::char_range p : ranges) + { + if (p.first == p.second) + result.m_chars.insert (p.first); + else + result.m_char_ranges.push_back (std::move (p)); + } + ptr += 2; + } + else if (at_character_class (ptr)) + { + const char *end = strchr (ptr, ']'); + gdb_assert (end != nullptr); + std::string cc (ptr, end - ptr + 1); + if (character_class_matches_dir_separator (cc)) + { + /* We are converting a glob to a regexp and trying to + replicate the FNM_PATHNAME flag of fnmatch. If we allow a + character class that matches '/' then this might cause + problems. + + We could try to split the class into multiple ranges that + cover all the same characters, but not '/', but for now we + just give an error. */ + error (_("skip glob regexp: unsupported character class '%s'"), + cc.c_str ()); + } + result.m_char_classes.push_back (std::move (cc)); + ptr = end; + } + else + result.m_chars.insert (*ptr); + } + + if (*ptr != ']') + return {}; + + result.m_end = ptr; + return result; +} + +/* See class declaration above. */ + +std::string +glob_bracket_expr::to_string () const +{ + std::string result ("["); + + if (m_negated) + result += '^'; + + if (m_chars.find (']') != m_chars.end ()) + result += ']'; + + for (const glob_bracket_expr::char_range &p : m_char_ranges) + { + /* These should be converted to single characters by sanitize_range. */ + gdb_assert (p.first != ']' && p.second != ']'); + + result += p.first; + result += '-'; + result += p.second; + } + + for (const std::string &s : m_char_classes) + result += s; + + for (const char c : m_chars) + { + if (strchr ("-^]", c) == nullptr) + result += c; + } + + if (m_chars.find ('^') != m_chars.end ()) + result += '^'; + if (m_chars.find ('-') != m_chars.end ()) + result += '-'; + result += ']'; + return result; +} + +/* See class declaration above. */ + +const char * +glob_bracket_expr::end () const +{ + /* The parse member function always sets to non-NULL before releasing an + instance of this class into the world. */ + gdb_assert (m_end != nullptr); + return m_end; +} + +/* When we convert a filename glob into a regular expression, these are + the flags to use. If filenames are case insensitive then we add the + ICASE (ignore case) flag. */ + +static constexpr int file_glob_regexp_flags = (REG_NOSUB + | REG_EXTENDED +#ifdef HAVE_CASE_INSENSITIVE_FILE_SYSTEM + | REG_ICASE +#endif /* HAVE_CASE_INSENSITIVE_FILE_SYSTEM */ + ); + +/* GLOB is the user supplied glob pattern as supplied to the 'skip -gfile + GLOB' command. This function builds and returns a regular expression + that matches GLOB. + + On DOS based filesystems, where backslash can serve as a directory + separator, the returned regexp will accept either directory separator + character. + + On case insensitive filesystems it is expected that the regexp will be + compiled with the REG_ICASE flag. Some character ranges within bracket + expressions will be split in order to facilitate case insensitive + matching, see sanitize_range for more details. */ + +static std::string +glob_to_regexp (const std::string& glob) +{ + std::string result; + + /* If the GLOB is absolute then we add a beginning anchor, thus a glob + '/tmp/file.c' will not match '/blah/tmp/file.c'. If the glob is not + absolute then we add a preceding slash, this means a glob 'a/file.c' + will not match '/aa/file.c', but will match '/tmp/a/file.c'. */ + if (!IS_ABSOLUTE_PATH (glob.c_str ())) + result += "/"; + else + result += "^"; + + static const std::string dir_separator_regexp +#ifdef HAVE_DOS_BASED_FILE_SYSTEM + ("(\\\\|/)"); +#else + ("/"); +#endif /* not HAVE_DOS_BASED_FILE_SYSTEM */ + + /* Many patterns want to match any character. Any in this case doesn't + include slash, so lets define a regexp to match anything except a + slash. */ + static const std::string any_char +#ifdef HAVE_DOS_BASED_FILE_SYSTEM + ("[^\\/]"); +#else + ("[^/]"); +#endif /* not HAVE_DOS_BASED_FILE_SYSTEM */ + + /* True when we can accept '**', false otherwise. The '**' pattern is + modelled on the bash globstar feature, it matches zero or more + directories. */ + bool can_globstar = true; + + for (const char *ptr = glob.c_str (); + *ptr != '\0'; + ++ptr) + { + /* In a position where '**' could appear, but this is not the first + '*', so we can no longer accept '**'. This will reset after the + next slash. */ + if (can_globstar && *ptr != '*') + can_globstar = false; + + if (strchr (".+()|{}$^", *ptr) != nullptr) + { + /* This is a character that has no special meaning within a glob, + but does within a regexp, this needs to be escaped. */ + result = result + '\\' + *ptr; + } + else if (*ptr == '*') + { + /* Are we looking at '**' in a location where this is valid. After + the '**' must be either a slash, or the end of the string. */ + if (can_globstar && *(ptr + 1) == '*' + && (IS_DIR_SEPARATOR (*(ptr + 2)) || *(ptr + 2) == '\0')) + { + /* If the '**' is at the end of the string then we allow it + to match anything. This is inline with bash. */ + if (*(ptr + 2) == '\0') + { + result += ".*"; + ptr += 1; + } + /* The '**' is followed by a slash. We accept zero or more + directories, which are any character sequence followed by + a slash. */ + else + { + result += "(" + any_char + "*" + dir_separator_regexp + ")*"; + ptr += 2; + } + } + /* A single '*' or the start of '**' is a location where '**' is + not valid. Match any number (zero or more) non-slash + characters. */ + else + result += any_char + '*'; + } + else if (*ptr == '?') + { + /* Match a single non-slash character. */ + result += any_char; + } + else if (IS_DIR_SEPARATOR (*ptr)) + { + /* After a slash we can see '**' again. On builds where + backslash is also a possible directory separator, this + converts the backslash to a forward slash. */ + result += dir_separator_regexp; + can_globstar = true; + } + else if (*ptr == '\\') + { + /* The code that this regexp logic replaced used to call fnmatch + with FNM_NOESCAPE flag. This flag means that backslash has no + special meaning. We maintain that here by escaping any + backslashes. This will only trigger if the earlier + IS_DIR_SEPARATOR check doesn't match backslashes. */ + result += "\\\\"; + } + else if (*ptr == '[') + { + std::optional<glob_bracket_expr> g = glob_bracket_expr::parse (ptr); + + if (g.has_value ()) + { + /* Add a regexp version of this bracket expression. */ + result += g->to_string (); + + /* Skip over the bracket expression in the glob. */ + ptr = g->end (); + } + else + { + /* Not a bracket expression. Just treat this as a literal + character. */ + result += "\\["; + } + } + else + { + /* All other characters are passed through. */ + result += *ptr; + } + } + + /* Anchor the regexp to the end of the filename. */ + result += '$'; + + return result; +} + skiplist_entry::skiplist_entry (bool file_is_glob, std::string &&file, bool function_is_regexp, @@ -144,7 +705,12 @@ skiplist_entry::skiplist_entry (bool file_is_glob, gdb_assert (!m_file.empty () || !m_function.empty ()); if (m_file_is_glob) - gdb_assert (!m_file.empty ()); + { + gdb_assert (!m_file.empty ()); + m_compiled_file_regexp.emplace (glob_to_regexp (m_file).c_str (), + file_glob_regexp_flags, + _("skip glob regexp")); + } if (m_function_is_regexp) { @@ -626,36 +1192,72 @@ skiplist_entry::do_skip_file_p (const symtab_and_line &function_sal) const return result; } -bool -skiplist_entry::do_skip_gfile_p (const symtab_and_line &function_sal) const -{ - bool result; +/* The implementation of skiplist_entry::do_skip_gfile_p. This exists as a + separate function so that this function can be unit tested. - /* Check first sole SYMTAB->FILENAME. It may not be a substring of - symtab_to_fullname as it may contain "./" etc. */ - if (gdb_filename_fnmatch (m_file.c_str (), function_sal.symtab->filename (), - FNM_FILE_NAME | FNM_NOESCAPE) == 0) - result = true; + PATTERN is the user supplied pattern held within the skiplist_entry, and + RE is the compiled regexp version of PATTERN, created when the + skiplist_entry was created. - /* Before we invoke symtab_to_fullname, which is expensive, do a quick - comparison of the basenames. - Note that we assume that lbasename works with glob-style patterns. - If the basename of the glob pattern is something like "*.c" then this - isn't much of a win. Oh well. */ - else if (!basenames_may_differ - && gdb_filename_fnmatch (lbasename (m_file.c_str ()), - lbasename (function_sal.symtab->filename ()), + The two callbacks GET_FILENAME and GET_FULLNAME return the result of + symtab::filename and symtab_to_fullname respectively. These are + provided as callbacks though so that the self tests don't need to create + fake symtabs. + + Returns true if PATTERN matches the filename or fullname, and false + otherwise. + + This function tries to avoid calling GET_FULLNAME as this can be more + expensive. The PATTERN will first be matched against the result of + calling GET_FILENAME if possible. */ + +static bool +do_skip_gfile_p (const std::string &pattern, const compiled_regex &re, + gdb::function_view<const char * ()> get_filename, + gdb::function_view<const char * ()> get_fullname) +{ + /* If basenames don't match then the full pattern cannot match. The + gdb_filename_fnmatch already handles case insensitive filesystems, and + as we're only checking the basenames here, directory separators are + not a problem. */ + if (!basenames_may_differ + && gdb_filename_fnmatch (lbasename (pattern.c_str ()), + lbasename (get_filename ()), FNM_FILE_NAME | FNM_NOESCAPE) != 0) - result = false; - else - { - /* Note: symtab_to_fullname caches its result, thus we don't have to. */ - const char *fullname = symtab_to_fullname (function_sal.symtab); + return false; - result = compare_glob_filenames_for_search (fullname, m_file.c_str ()); + /* If the pattern is absolute, e.g. starts with '/', then we're going to + have to compare against the full filename, we can skip the check + against the symtab filename. */ + bool is_absolute_pattern = IS_ABSOLUTE_PATH (pattern.c_str ()); + + /* The symtab's filename might not be the full filename, this will depend + on how the symtab was compiled and/or how the DWARF was generated. + But with gcc at least, compiling a relative filename results in a + symtab with a relative filename. However, in many cases, the skip + pattern is also only a partial filename, and matches against the end + part of the symtab filename, so rather than the (relatively expensive) + fullname lookup, check first against the symtab filename. */ + if (!basenames_may_differ && !is_absolute_pattern) + { + if (re.exec (get_filename (), 0, nullptr, 0) == 0) + return true; } - return result; + return re.exec (get_fullname (), 0, nullptr, 0) == 0; +} + +bool +skiplist_entry::do_skip_gfile_p (const symtab_and_line &function_sal) const +{ + gdb_assert (m_compiled_file_regexp.has_value ()); + return ::do_skip_gfile_p (m_file, m_compiled_file_regexp.value (), + [&] () { + return function_sal.symtab->filename (); + }, + [&] () { + return symtab_to_fullname (function_sal.symtab); + }); } bool @@ -778,6 +1380,452 @@ save_skip_command (const char *filename, int from_tty) entry.print_recreate (&fp); } +#if GDB_SELF_TEST + +namespace selftests { + +/* Define a single test of the do_skip_gfile_p function. */ +struct skip_gfile_test +{ + /* The glob pattern to match against the filename. */ + const char *pattern; + + /* The filename is split into PREFIX and SUFFIX. This reflects how GDB + stores only part of the filename (as found in the DWARF) within the + symtab as the 'filename'. The PREFIX is the part GDB figures out from + the compilation directory. The full filename is created by + concatenating PREFIX to SUFFIX. */ + const char *prefix; + const char *suffix; + + /* True if we expect PATTERN to match against the filename created from + PREFIX and SUFFIX. */ + bool expect_match; +}; + +/* Some tests check that case sensitivity works, these tests expect a + glob to not match a particular filename. On case insensitive file + systems these globs will match. We define this constant to use for + those tests. */ + +#ifdef HAVE_CASE_INSENSITIVE_FILE_SYSTEM +static constexpr bool false_if_case_sensitive = true; +#else +static constexpr bool false_if_case_sensitive = false; +#endif /* ! HAVE_CASE_INSENSITIVE_FILE_SYSTEM */ + +/* List of all do_skip_gfile_p tests. */ +static constexpr std::initializer_list<skip_gfile_test> skip_gfile_tests = { + /* Basic glob feature testing. */ + { "*", "/tmp/", "foo/hello.c", true }, + { "xx", "/tmp/", "foo/hello.c", false }, + { "hello.?", "/tmp/", "foo/hello.c", true }, + { "hello.?", "/tmp/", "foo/hello.cc", false }, + { "aa/bb*/file*.*c*", "/tmp/", "aa/bb/file.c", true }, + { "*.c", "/tmp/", "aa/bb/file.c", true }, + { "bb/*.c", "/tmp/", "aa/bb/file.c", true }, + { "ee/*.c", "/tmp/", "ee/bb/file.c", false }, + { "b/*.c", "/tmp/", "aa/bb/file.c", false }, + + /* Testing the globstar '**' feature. */ + { "ee/**/*.c", "/tmp/", "ee/bb/file.c", true }, + { "dd/**/**/*.c", "/tmp/", "dd/file.c", true }, + { "dd/**/**/*.c", "/tmp/", "dd/aa/file.c", true }, + { "dd/**/**/*.c", "/tmp/", "dd/aa/bb/file.c", true }, + { "dd/**/**/*.c", "/tmp/", "dd/aa/bb/cc/file.c", true }, + { "dd/**/**/*.c", "/tmp/", "dd/aa/bb/cc/dd/file.c", true }, + { "**/**/**/*.c", "/tmp/", "file.c", true }, + { "**/**/**/*.c", "/tmp/aa/", "file.c", true }, + { "**/**/**/*.c", "/tmp/aa/bb/", "file.c", true }, + { "**/**/**/*.c", "/tmp/aa/bb/cc/", "file.c", true }, + { "**/**/**/*.c", "/tmp/aa/bb/cc/dd/", "file.c", true }, + { "/tmp/**", "/tmp/", "file.c", true }, + { "/tmp/**", "/tmp/aa/", "file.c", true }, + + /* When '**' appears in the middle of a path component (not after + '/' or at the start), it is not globstar but two regular '*' + wildcards. */ + { "a**b/*.c", "/tmp/", "ab/foo.c", true }, + { "a**b/*.c", "/tmp/", "axxb/foo.c", true }, + { "a**b/*.c", "/tmp/", "a/b/foo.c", false }, + + { "[a-c]*.h", "/tmp/", "axxx.h", true }, + { "*[[:digit:]]*.h", "/tmp/", "xx1xx.h", true }, + { "[!a-c]*.h", "/tmp/", "axxx.h", false }, + { "*[]].h", "/tmp/", "xx].h", true }, + { "*[!]].h", "/tmp/", "xx].h", false }, + + /* An absolute pattern must match the full filename. */ + { "/tmp/foo.cc", "/blah/tmp/", "foo.cc", false }, + { "/blah/tmp/foo.cc", "/blah/tmp/", "foo.cc", true }, + + /* Characters that are special in regular expressions but should be + treated as literal characters in glob patterns. */ + + /* The '+' character means 'one or more' in a regular expression. */ + { "file+.c", "/tmp/", "file+.c", true }, + { "dir+/*.c", "/tmp/", "dir+/foo.c", true }, + { "dir+/*.c", "/tmp/", "dirr/foo.c", false }, + { "dir+/*.c", "/tmp/", "dirrr/foo.c", false }, + + /* The '(' and ')' characters form capture groups in regexp. */ + { "(foo)/*.c", "/tmp/", "(foo)/bar.c", true }, + { "(foo)/*.c", "/tmp/", "foo/bar.c", false }, + + /* The '|' character means alternation in a regexp. */ + { "a|b/*.c", "/tmp/", "a|b/foo.c", true }, + { "a|b/*.c", "/tmp/", "b/foo.c", false }, + + /* The '{' and '}' characters form interval expressions in regexp. + (e.g., a{2} matches "aa"). */ + { "a{2}/*.c", "/tmp/", "a{2}/foo.c", true }, + { "a{2}/*.c", "/tmp/", "aa/foo.c", false }, + + /* The '$' and '^' characters are anchors in regexp. */ + { "file$.c", "/tmp/", "file$.c", true }, + { "^file.c", "/tmp/", "^file.c", true }, + + /* Bracket expressions that mention a '/' are not valid within a glob and + POSIX requires that they be treated as literal content. */ + { "aa[.-/]bb/*.c", "/tmp/", "aa.bb/foo.c", false }, + { "aa[.-/]bb/*.c", "/tmp/", "aa[.-/]bb/foo.c", true }, + { "aa[/-/]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[/-/]bb/*.c", "/tmp/", "aa[/-/]bb/foo.c", true }, + { "aa[/-1]bb/*.c", "/tmp/", "aa[/-1]bb/foo.c", true }, + { "aa[/-1]bb/*.c", "/tmp/", "aa0bb/foo.c", false }, + + /* Within bracket expressions, ranges that span '/' should not match the + '/' character. */ + { "aa[.-0]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[.-0]bb/*.c", "/tmp/", "aa.bb/foo.c", true }, + { "aa[.-0]bb/*.c", "/tmp/", "aa0bb/foo.c", true }, + { "aa[.-1]bb/*.c", "/tmp/", "aa.bb/foo.c", true }, + { "aa[.-1]bb/*.c", "/tmp/", "aa0bb/foo.c", true }, + { "aa[.-1]bb/*.c", "/tmp/", "aa1bb/foo.c", true }, + { "aa[.-1]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[.-1]bb/*.c", "/tmp/", "aa2bb/foo.c", false }, + { "aa[,-0]bb/*.c", "/tmp/", "aa,bb/foo.c", true }, + { "aa[,-0]bb/*.c", "/tmp/", "aa-bb/foo.c", true }, + { "aa[,-0]bb/*.c", "/tmp/", "aa.bb/foo.c", true }, + { "aa[,-0]bb/*.c", "/tmp/", "aa0bb/foo.c", true }, + { "aa[,-0]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[,-0]bb/*.c", "/tmp/", "aa1bb/foo.c", false }, + + /* Negated bracket expressions should also not match '/'. POSIX says + that, within a glob, a bracket expression starting with '^' is + undefined, but fnmatch (and bash) treat '^' the same as '!'. */ + { "a[!a]b/*.c", "/tmp/", "a/b/foo.c", false }, + { "a[!a]b/*.c", "/tmp/", "axb/foo.c", true }, + { "a[^a]b/*.c", "/tmp/", "a/b/foo.c", false }, + { "a[^a]b/*.c", "/tmp/", "axb/foo.c", true }, + + /* Historically GDB used fnmatch to perform glob matching, and passed + FNM_NOESCAPE as an option to fnmatch, which means that '\' is not an + escape character, but should be treated as a literal character. When + we switch to using regexp instead of fnmatch we preserved this + behaviour. + + Given the above '\*' in a glob doesn't escape the '*', the '\' is + literal and '*' is still a wildcard. */ + { "a\\b.c", "/tmp/", "a\\b.c", true }, + { "a\\*.c", "/tmp/", "a\\foo.c", true }, + { "a\\*.c", "/tmp/", "a*.c", false }, + + /* As with the previous test, historically FNM_PERIOD was not used, so + '*' and '?' should match a leading period in a filename. */ + { "*.c", "/tmp/", ".hidden.c", true }, + { "?idden.c", "/tmp/", ".idden.c", true }, + + /* An unmatched '[' should be treated as a literal character. */ + { "[.c", "/tmp/", "[.c", true }, + { "[.c", "/tmp/", "x.c", false }, + + /* A '-' at the start or end of a bracket expression is literal. */ + { "[-ab]*.c", "/tmp/", "-foo.c", true }, + { "[ab-]*.c", "/tmp/", "-foo.c", true }, + + /* A '-' immediately after a range is literal, not a second range + operator. So [a-c-f] is the range 'a'-'c', a literal '-', and + a literal 'f'. Characters between 'c' and 'f' (like 'e') that + are outside the range should not match. */ + { "[a-c-f]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[a-c-f]dir/*.c", "/tmp/", "-dir/foo.c", true }, + { "[a-c-f]dir/*.c", "/tmp/", "fdir/foo.c", true }, + { "[a-c-f]dir/*.c", "/tmp/", "edir/foo.c", false }, + + /* In a POSIX glob bracket expression, a leading '^' is undefined. + However fnmatch (and bash) treat this the same as '!', that is, as + match negation. GDB copies this behaviour. */ + { "[^abc]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[^abc]dir/*.c", "/tmp/", "^dir/foo.c", true }, + { "[^abc]dir/*.c", "/tmp/", "xdir/foo.c", true }, + + /* Due to the above '^^' within a bracket means match everything except + '^' (and '/' of course). */ + { "[^^]dir/*.c", "/tmp/", "^dir/foo.c", false }, + { "[^^]dir/*.c", "/tmp/", "xdir/foo.c", true }, + { "aa[^^]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[^^]bb/*.c", "/tmp/", "aa^bb/foo.c", false }, + { "aa[^^]bb/*.c", "/tmp/", "aa-bb/foo.c", true }, + + /* The glob '[!^]' is the same as the previous, but uses the official + POSIX glob '!' character for negation. */ + { "[!^]dir/*.c", "/tmp/", "^dir/foo.c", false }, + { "[!^]dir/*.c", "/tmp/", "adir/foo.c", true }, + + /* The globs '[]' and '[!]' are invalid as the ']' is considered a + character within the bracket expression, this means that the bracket + expression is never terminated. This is handled by treating the + characters as literals. */ + { "[!]dir/*.c", "/tmp/", "[!]dir/foo.c", true }, + { "[!]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[^]dir/*.c", "/tmp/", "[^]dir/foo.c", true }, + { "[^]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "aa[]bb/*.c", "/tmp/", "aabb/foo.c", false }, + { "aa[]bb/*.c", "/tmp/", "aa[]bb/foo.c", true }, + + /* When '^' is NOT the first character in a bracket expression, it is + just a character to match or not match. */ + { "[abc^]dir/*.c", "/tmp/", "^dir/foo.c", true }, + { "[abc^]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[abc^]dir/*.c", "/tmp/", "xdir/foo.c", false }, + { "[!abc^]dir/*.c", "/tmp/", "^dir/foo.c", false }, + { "[!abc^]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[!abc^]dir/*.c", "/tmp/", "xdir/foo.c", true }, + + /* Negated bracket expression with ']' as the first element. Test with + both '!' and '^' for the reasons discussed above. */ + { "[!]a]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[!]a]dir/*.c", "/tmp/", "]dir/foo.c", false }, + { "[!]a]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[^]a]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[^]a]dir/*.c", "/tmp/", "]dir/foo.c", false }, + { "[^]a]dir/*.c", "/tmp/", "adir/foo.c", false }, + + /* Within a bracket expression, a range that starts with the negation + character is not treated like a range, so [!-a] means match everything + except '-' and 'a'. Test with both '!' and '^' for the reasons + discussed above. */ + { "[!-a]dir/*.c", "/tmp/", "_dir/foo.c", true }, + { "[!-a]dir/*.c", "/tmp/", "^dir/foo.c", true }, + { "[!-a]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[!-a]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[^-a]dir/*.c", "/tmp/", "_dir/foo.c", true }, + { "[^-a]dir/*.c", "/tmp/", "^dir/foo.c", true }, + { "[^-a]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[^-a]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[!-]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[!-]dir/*.c", "/tmp/", "-dir/foo.c", false }, + { "[^-]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[^-]dir/*.c", "/tmp/", "-dir/foo.c", false }, + + /* Within a bracket expression, test ranges starting and ending with + '-'. */ + { "aa[--0]bb/*.c", "/tmp/", "aa-bb/foo.c", true }, + { "aa[--0]bb/*.c", "/tmp/", "aa.bb/foo.c", true }, + { "aa[--0]bb/*.c", "/tmp/", "aa0bb/foo.c", true }, + { "aa[--0]bb/*.c", "/tmp/", "aa/bb/foo.c", false }, + { "aa[+--]bb/*.c", "/tmp/", "aa-bb/foo.c", true }, + { "aa[+--]bb/*.c", "/tmp/", "aa+bb/foo.c", true }, + { "aa[+--]bb/*.c", "/tmp/", "aa,bb/foo.c", true }, + { "aa[+--]bb/*.c", "/tmp/", "aa.bb/foo.c", false }, + + /* Basic character class matching. [[:digit:]] matches any decimal + digit character. */ + { "[[:digit:]]dir/*.c", "/tmp/", "1dir/foo.c", true }, + { "[[:digit:]]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "adir/[[:digit:]]*.c", "/tmp/", "adir/1foo.c", true }, + { "[[:digit:]]*.c", "/tmp/", "adir/1foo.c", true }, + + /* [[:alpha:]] matches any alphabetic character. */ + { "[[:alpha:]]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[[:alpha:]]dir/*.c", "/tmp/", "1dir/foo.c", false }, + + /* [[:upper:]] and [[:lower:]] match upper- and lowercase letters + respectively. */ + { "[[:upper:]]dir/*.c", "/tmp/", "Adir/foo.c", true }, + { "[[:upper:]]dir/*.c", "/tmp/", "adir/foo.c", false_if_case_sensitive }, + { "[[:lower:]]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[[:lower:]]dir/*.c", "/tmp/", "Adir/foo.c", false_if_case_sensitive }, + + /* Negated character class: [![:digit:]] matches non-digit characters, + but should not match '/'. */ + { "a[![:digit:]]b/*.c", "/tmp/", "axb/foo.c", true }, + { "a[![:digit:]]b/*.c", "/tmp/", "a1b/foo.c", false }, + { "a[![:digit:]]b/*.c", "/tmp/", "a/b/foo.c", false }, + + /* Character class combined with literal characters. [[:digit:]ab] + should match any digit, or 'a', or 'b'. */ + { "[[:digit:]ab]dir/*.c", "/tmp/", "1dir/foo.c", true }, + { "[[:digit:]ab]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[[:digit:]ab]dir/*.c", "/tmp/", "bdir/foo.c", true }, + { "[[:digit:]ab]dir/*.c", "/tmp/", "xdir/foo.c", false }, + + /* Character class combined with a character range. [[:digit:]a-f] + should match any digit or any letter from 'a' to 'f'. */ + { "[[:digit:]a-f]dir/*.c", "/tmp/", "1dir/foo.c", true }, + { "[[:digit:]a-f]dir/*.c", "/tmp/", "cdir/foo.c", true }, + { "[[:digit:]a-f]dir/*.c", "/tmp/", "gdir/foo.c", false }, + + /* Multiple character classes in one bracket expression. + [[:digit:][:alpha:]] should match digits and letters. */ + { "[[:digit:][:alpha:]]dir/*.c", "/tmp/", "1dir/foo.c", true }, + { "[[:digit:][:alpha:]]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[[:digit:][:alpha:]]dir/*.c", "/tmp/", "-dir/foo.c", false }, + + /* Character class appearing in the basename portion of the + pattern. */ + { "dir/file[[:digit:]].c", "/tmp/", "dir/file1.c", true }, + { "dir/file[[:digit:]].c", "/tmp/", "dir/filea.c", false }, + + { "[W-_]dir/*.c", "/tmp/", "_dir/foo.c", true }, + { "[A-C]dir/*.c", "/tmp/", "Adir/foo.c", true }, + { "[]-_]dir/*.c", "/tmp/", "^dir/foo.c", true }, + +#ifdef HAVE_CASE_INSENSITIVE_FILE_SYSTEM + /* Basic case insensitive matching. */ + { "Dir/*.c", "/tmp/", "dir/foo.c", true }, + { "DIR/*.c", "/tmp/", "dir/foo.c", true }, + { "Dir/*.c", "/tmp/", "other/foo.c", false }, + { "dir/*.c", "/tmp/", "dir/FOO.c", true }, + { "dir/*.c", "/tmp/", "DIR/FOO.c", true }, + + /* Upper case character range [A-C] is converted to [a-c] on a case + insensitive file system. */ + { "[A-C]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[A-C]dir/*.c", "/tmp/", "cdir/foo.c", true }, + { "[A-C]dir/*.c", "/tmp/", "Bdir/foo.c", true }, + { "[A-C]dir/*.c", "/tmp/", "ddir/foo.c", false }, + + /* A range that overlaps into the upper case characters [=-D] will be + split into two: [=-@] and [A-D]. With REG_ICASE the [A-D] will match + both [A-D] and [a-d]. */ + { "[=-D]dir/*.c", "/tmp/", "@dir/foo.c", true }, + { "[=-D]dir/*.c", "/tmp/", "=dir/foo.c", true }, + { "[=-D]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[=-D]dir/*.c", "/tmp/", "ddir/foo.c", true }, + { "[=-D]dir/*.c", "/tmp/", "Adir/foo.c", true }, + { "[=-D]dir/*.c", "/tmp/", "Ddir/foo.c", true }, + + /* A similar thing happens with ranges that overlap the end of the + upper case character range. */ + { "[W-_]dir/*.c", "/tmp/", "[dir/foo.c", true }, + { "[W-_]dir/*.c", "/tmp/", "_dir/foo.c", true }, + { "[W-_]dir/*.c", "/tmp/", "wdir/foo.c", true }, + { "[W-_]dir/*.c", "/tmp/", "zdir/foo.c", true }, + { "[W-_]dir/*.c", "/tmp/", "Wdir/foo.c", true }, + { "[W-_]dir/*.c", "/tmp/", "Zdir/foo.c", true }, + + /* A full upper case range [A-Z] is converted to [a-z]. */ + { "[A-Z]dir/*.c", "/tmp/", "mdir/foo.c", true }, + { "[A-Z]dir/*.c", "/tmp/", "1dir/foo.c", false }, + + /* Negated bracket expression with an upper case range. */ + { "[!A-C]dir/*.c", "/tmp/", "ddir/foo.c", true }, + { "[!A-C]dir/*.c", "/tmp/", "adir/foo.c", false }, + { "[!A-C]dir/*.c", "/tmp/", "Bdir/foo.c", false }, + + /* Upper case literal characters in bracket expressions are + converted to lower case. */ + { "[ABCD]dir/*.c", "/tmp/", "adir/foo.c", true }, + { "[ABCD]dir/*.c", "/tmp/", "Cdir/foo.c", true }, + { "[ABCD]dir/*.c", "/tmp/", "edir/foo.c", false }, + + { "[[:upper:]]dir/*.c", "/tmp/", "adir/foo.c", true }, +#endif /* HAVE_CASE_INSENSITIVE_FILE_SYSTEM */ + +#ifdef HAVE_DOS_BASED_FILE_SYSTEM + /* Tests for DOS-based file systems. On a DOS-based file system + the backslash is treated as a directory separator in addition to + the forward slash. */ + + /* Backslash in the pattern is treated as a directory separator. */ + { "dir\\*.c", "/tmp/", "dir\\foo.c", true }, + { "dir\\*.c", "/tmp/", "dir\\bar.c", true }, + { "dir\\*.c", "/tmp/", "other\\foo.c", false }, + + /* The regexp should match both backslash and forward slashes. */ + { "dir/*.c", "/tmp/", "dir\\foo.c", true }, + + /* Globstar with backslash as the directory separator. */ + { "dir\\**\\*.c", "/tmp/", "dir\\sub\\foo.c", true }, + { "dir\\**\\*.c", "/tmp/", "dir\\a\\b\\foo.c", true }, + + /* An absolute DOS path with a drive letter. */ + { "C:\\dir\\*.c", "", "C:\\dir\\foo.c", true }, + { "C:\\dir\\*.c", "", "C:\\other\\foo.c", false }, +#endif /* HAVE_DOS_BASED_FILE_SYSTEM */ + +#if defined HAVE_DOS_BASED_FILE_SYSTEM \ + && defined HAVE_CASE_INSENSITIVE_FILE_SYSTEM + /* Combine case insensitivity together with backslash directory + separators. The pattern has upper case and forward slash, the + filename has mixed case and backslash. */ + { "Dir/*.c", "/tmp/", "dir\\foo.c", true }, + { "dir/*.c", "/tmp/", "Dir\\foo.c", true }, + { "Dir\\sub\\*.c", "/tmp/", "Dir\\sub\\bar.c", true }, +#endif /* HAVE_DOS_BASED_FILE_SYSTEM && HAVE_CASE_INSENSITIVE_FILE_SYSTEM */ +}; + +/* The skip_gfile_matching unit tests. */ + +static void +test_skip_gfile_matching () +{ + int failure_count = 0; + bool first = true; + for (const auto &test : skip_gfile_tests) + { + std::string pattern (test.pattern); + std::string filename (test.suffix); + std::string fullname = std::string (test.prefix) + filename; + + if (run_verbose ()) + { + if (!first) + debug_printf ("\n"); + else + first = false; + debug_printf ("Pattern (%s)\n", pattern.c_str ()); + debug_printf ("Filename (%s)\n", filename.c_str ()); + debug_printf ("Fullname (%s)\n", fullname.c_str ()); + } + + std::string file_re = glob_to_regexp (pattern); + + if (run_verbose ()) + debug_printf ("Regexp (%s)\n", file_re.c_str ()); + + compiled_regex re (file_re.c_str (), file_glob_regexp_flags, + _("skip glob testing")); + + bool matched = do_skip_gfile_p (pattern, re, + [&] () { return filename.c_str (); }, + [&] () { return fullname.c_str (); }); + + bool success = matched == test.expect_match; + + if (run_verbose ()) + debug_printf ("Matched: %s%s\n", (matched ? "Yes" : "No"), + (success ? "" + : string_printf ("\t[Expected: %s]", + (test.expect_match + ? "Yes" : "No")).c_str ())); + + if (matched != test.expect_match) + failure_count++; + } + + if (run_verbose ()) + debug_printf ("Failed: %d\n", failure_count); + + SELF_CHECK (failure_count == 0); +} + +} /* namespace selftests */ + +#endif /* GDB_SELF_TEST */ + INIT_GDB_FILE (step_skip) { static struct cmd_list_element *skiplist = NULL; @@ -867,4 +1915,9 @@ Usage: save skip FILE\n\ Use the 'source' command in another debug session to restore them."), &save_cmdlist); set_cmd_completer (c, deprecated_filename_completer); + +#if GDB_SELF_TEST + selftests::register_test ("skip_gfile_matching", + selftests::test_skip_gfile_matching); +#endif } diff --git a/gdb/symtab.c b/gdb/symtab.c index 9b2d2cf6d59..e02744331c6 100644 --- a/gdb/symtab.c +++ b/gdb/symtab.c @@ -629,40 +629,6 @@ compare_filenames_for_search (const char *filename, const char *search_name) && STRIP_DRIVE_SPEC (filename) == &filename[len - search_len])); } -/* Same as compare_filenames_for_search, but for glob-style patterns. - Heads up on the order of the arguments. They match the order of - compare_filenames_for_search, but it's the opposite of the order of - arguments to gdb_filename_fnmatch. */ - -bool -compare_glob_filenames_for_search (const char *filename, - const char *search_name) -{ - /* We rely on the property of glob-style patterns with FNM_FILE_NAME that - all /s have to be explicitly specified. */ - int file_path_elements = count_path_elements (filename); - int search_path_elements = count_path_elements (search_name); - - if (search_path_elements > file_path_elements) - return false; - - if (IS_ABSOLUTE_PATH (search_name)) - { - return (search_path_elements == file_path_elements - && gdb_filename_fnmatch (search_name, filename, - FNM_FILE_NAME | FNM_NOESCAPE) == 0); - } - - { - const char *file_to_compare - = strip_leading_path_elements (filename, - file_path_elements - search_path_elements); - - return gdb_filename_fnmatch (search_name, file_to_compare, - FNM_FILE_NAME | FNM_NOESCAPE) == 0; - } -} - /* See symtab.h. */ void diff --git a/gdb/symtab.h b/gdb/symtab.h index 8f0cc728410..6e155192873 100644 --- a/gdb/symtab.h +++ b/gdb/symtab.h @@ -2782,9 +2782,6 @@ extern bool basenames_may_differ; bool compare_filenames_for_search (const char *filename, const char *search_name); -bool compare_glob_filenames_for_search (const char *filename, - const char *search_name); - /* Check in PSPACE for a symtab of a specific name; first in symtabs, then in psymtabs. *If* there is no '/' in the name, a match after a '/' in the symtab filename will also work. diff --git a/gdb/testsuite/gdb.base/skip-tree-1.c b/gdb/testsuite/gdb.base/skip-tree-1.c new file mode 100644 index 00000000000..9f6255abaa7 --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree-1.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +extern void func1 (void); + +static volatile int global_var_1; + +void +func1 (void) +{ + ++global_var_1; +} diff --git a/gdb/testsuite/gdb.base/skip-tree-2.c b/gdb/testsuite/gdb.base/skip-tree-2.c new file mode 100644 index 00000000000..cd4ad8bbca9 --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree-2.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +extern void func2 (void); + +static volatile int global_var_2; + +void +func2 (void) +{ + ++global_var_2; +} diff --git a/gdb/testsuite/gdb.base/skip-tree-3.c b/gdb/testsuite/gdb.base/skip-tree-3.c new file mode 100644 index 00000000000..b22df82c4aa --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree-3.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +extern void func3 (void); + +static volatile int global_var_3; + +void +func3 (void) +{ + ++global_var_3; +} diff --git a/gdb/testsuite/gdb.base/skip-tree-4.c b/gdb/testsuite/gdb.base/skip-tree-4.c new file mode 100644 index 00000000000..dc1167180b2 --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree-4.c @@ -0,0 +1,26 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +extern void func4 (void); + +static volatile int global_var_4; + +void +func4 (void) +{ + ++global_var_4; +} diff --git a/gdb/testsuite/gdb.base/skip-tree.c b/gdb/testsuite/gdb.base/skip-tree.c new file mode 100644 index 00000000000..4a254b860b1 --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree.c @@ -0,0 +1,38 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2026 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +extern void func1 (void); +extern void func2 (void); +extern void func3 (void); +extern void func4 (void); + +static volatile int global_var; + +int +main (void) +{ + ++global_var; + + func1 (); /* In aa/bb/file.c */ + func2 (); /* In cc/bb/file.c */ + func3 (); /* In dd/ee/file.c */ + func4 (); /* In dd/ee/ff/file.c */ + + ++global_var; /* End marker. */ + + return 0; +} diff --git a/gdb/testsuite/gdb.base/skip-tree.exp b/gdb/testsuite/gdb.base/skip-tree.exp new file mode 100644 index 00000000000..dc93c4cef84 --- /dev/null +++ b/gdb/testsuite/gdb.base/skip-tree.exp @@ -0,0 +1,199 @@ +# Copyright 2026 Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# More extensive testing of the glob matching facilities offered by +# 'skip -gfile'. This test sets up a directory tree of files and +# checks that '*' and '**' matching works as expected. + +# Identify the source files in the source tree. +standard_testfile .c -1.c -2.c -3.c -4.c + +# Create a directory tree into which some of the source files will be +# copied. +set root [standard_output_file "root"] +file mkdir $root/aa/bb +file mkdir $root/cc/bb +file mkdir $root/dd/ee/ff + +# Each SPEC is the name of a srcfile* global variable, and a relative +# filename within the directory tree we just created. The +# corresponding source file is copied from the source directory into +# the new directory tree, and the global srcfile* variable is updated. +# The initial srcfile is not copied over. +foreach spec {{srcfile2 aa/bb} \ + {srcfile3 cc/bb} \ + {srcfile4 dd/ee} \ + {srcfile5 dd/ee/ff}} { + set var [lindex $spec 0] + set dir [lindex $spec 1] + set src [set $var] + file copy $srcdir/$subdir/$src $root/$dir/file.c + set $var $dir/file.c +} + +# Find source line numbers in the initial srcfile. +set lineno_f1 [gdb_get_line_number "func1 ();" $srcfile] +set lineno_f2 [gdb_get_line_number "func2 ();" $srcfile] +set lineno_f3 [gdb_get_line_number "func3 ();" $srcfile] +set lineno_f4 [gdb_get_line_number "func4 ();" $srcfile] +set lineno_end [gdb_get_line_number "End marker" $srcfile] + +# Compile all the source files into DESTFILE. If RELATIVE_FILENAMES +# is true then the compilation is done using relative filenames to the +# source files. Otherwise, absolute source file names are used. +# +# Return true if the compilation is successful, otherwise false. +proc compile_test { destfile relative_filenames } { + # The file we will create. + set destfile [standard_output_file $destfile] + + if { $relative_filenames } { + # The main source file is always accessed via an absolute + # filename. But all the other source files are compiled via a + # relative filename. + set srclist [list $::srcdir/$::subdir/$::srcfile] + foreach var {srcfile2 srcfile3 srcfile4 srcfile5} { + set val [set ::$var] + lappend srclist ./$val + } + + with_cwd $::root { + if { [gdb_compile $srclist $destfile binfile {debug}] != "" } { + return false + } + } + } else { + # We create absolute paths for all source files. + set srclist [list $::srcdir/$::subdir/$::srcfile] + foreach var {srcfile2 srcfile3 srcfile4 srcfile5} { + set val [set ::$var] + lappend srclist $::root/$val + } + + if { [build_executable "failed to build" $destfile $srclist] } { + return false + } + } + + return true +} + +# Start GDB debugging TESTFILE. Install a 'skip -gfile' for FILE_GLOB +# then step past the 4 function calls within main. The SKIP_FUNC* +# arguments are true if the function should be skipped, or false if +# the function should be entered. +# +# The RELATIVE_SRC_FILENAMES is true if we expect to see relative +# source filenames in the output, or false if we expect to see +# absolute filenames. +proc run_test { file_glob skip_func1 skip_func2 skip_func3 skip_func4 \ + relative_src_filenames testfile } { + clean_restart $testfile + + if { ![runto_main] } { + return + } + + gdb_test "skip -gfile $file_glob" \ + "File\\(s\\) [string_to_regexp $file_glob] will be skipped when stepping\\." \ + "setup skip pattern" + + # Ensure we're in the right place ready to start testing. + gdb_test "step" \ + "$::lineno_f1\\s+func1 \\(\\);\[^\r\n\]+" \ + "step upto func1 call" + + for { set i 1 } { $i < 5 } { incr i } { + set j [expr { $i + 1 }] + + # Get the value of the appropriate SKIP_FUNC* argument. + set skip_func_p [set "skip_func$i"] + + if { !$skip_func_p } { + # Get the value of the appropriate SRCFILE* global. + set src [set "::srcfile$j"] + + # How we expect the source filename to appear in the output. + if { $relative_src_filenames } { + set file_re [string_to_regexp "./$src"] + } else { + set file_re [string_to_regexp "$::root/$src"] + } + + # Step the inferior, we expect to enter the function. + gdb_test "step" \ + [multi_line \ + "^func$i \\(\\) at $file_re:$::decimal" \ + "$::decimal\\s+\\+\\+global_var_$i;"] \ + "step into func$i" + + # Now finish the function, returning to the caller. + gdb_test "finish" ".*" \ + "finish from func$i" + } else { + # After skipping the function, on which line should we + # stop, and what does the line look like. + if { $j < 5 } { + set lineno [set "::lineno_f$j"] + set after_skip_re "func$j \\(\\);\[^\r\n\]+" + } else { + set lineno $::lineno_end + set after_skip_re "\\+\\+global_var;\[^\r\n\]+" + } + + # Step the inferior, we expect to skip the function. + gdb_test "step" \ + "$lineno\\s+$after_skip_re" \ + "skip func$i" + } + } +} + + # Should Function Be Skipped? + # Glob func1 func2 func3 func4 +set test_specs [list \ + [list xxx false false false false ] \ + [list file.? true true true true ] \ + [list aa/bb*/file*.*c* true false false false ] \ + [list *.c true true true true ] \ + [list bb/*.c true true false false ] \ + [list ee/*.c false false true false ] \ + [list ee/**/*.c false false true true ] \ + [list ee/ff/*.c false false false true ] \ + [list dd/**/**/*.c false false true true ] \ + [list **/**/**/*.c true true true true ] \ + [list $root/aa/bb/*.c true false false false ] \ + [list $root/**/*.c true true true true ] \ + [list $root/** true true true true ] \ +] + +foreach_with_prefix relative_compile_filenames { true false } { + if { $relative_compile_filenames } { + set filename $testfile-relative + } else { + set filename $testfile-absolute + } + + compile_test $filename $relative_compile_filenames + + foreach spec $test_specs { + set prefix [lindex $spec 0] + regsub "^[string_to_regexp $root]" $prefix "ROOT" prefix + + with_test_prefix "spec=$prefix" { + run_test {*}$spec $relative_compile_filenames $filename + } + } +} diff --git a/gdb/testsuite/gdb.base/skip.exp b/gdb/testsuite/gdb.base/skip.exp index 9d55c540f12..3807fb33325 100644 --- a/gdb/testsuite/gdb.base/skip.exp +++ b/gdb/testsuite/gdb.base/skip.exp @@ -54,6 +54,14 @@ gdb_test "skip -function foo -rfunction foo*" \ gdb_test "skip -file foo.c -gfile foo*.c" \ "Cannot specify both -file and -gfile\\." +# Some invalid -gfile patterns. +gdb_test "skip -gfile \[\]-W\]" \ + "skip glob regexp: invalid character range \\\]-W" +gdb_test "skip -gfile \[n-a\]" \ + "skip glob regexp: invalid character range n-a" +gdb_test "skip -gfile \[\[:punct:\]\]*.c" \ + "skip glob regexp: unsupported character class '\\\[:punct:\\\]'" + if {![runto_main]} { return }