/* Skipping uninteresting files and functions while stepping.

   Copyright (C) 2011-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/>.  */

#include "skip.h"
#include "event-top.h"
#include "value.h"
#include "valprint.h"
#include "ui-out.h"
#include "symtab.h"
#include "cli/cli-cmds.h"
#include "command.h"
#include "completer.h"
#include "stack.h"
#include "cli/cli-utils.h"
#include "arch-utils.h"
#include "linespec.h"
#include "objfiles.h"
#include "breakpoint.h"
#include "source.h"
#include "filenames.h"
#include "fnmatch.h"
#include "gdbsupport/gdb_regex.h"
#include <optional>
#include <list>
#include "cli/cli-style.h"
#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. */
static bool debug_skip = false;

/* Print a "skip" debug statement.  */

#define skip_debug_printf(fmt, ...) \
  debug_prefixed_printf_cond (debug_skip, "skip", fmt, ##__VA_ARGS__)

class skiplist_entry
{
public:
  /* Create a skiplist_entry object and add it to the chain.  */
  static void add_entry (bool file_is_glob,
			 std::string &&file,
			 bool function_is_regexp,
			 std::string &&function);

  /* Return true if the skip entry has a file or glob-style file
     pattern that matches FUNCTION_SAL.  */
  bool skip_file_p (const symtab_and_line &function_sal) const;

  /* Return true if the skip entry has a function or function regexp
     that matches FUNCTION_NAME.  */
  bool skip_function_p (const char *function_name) const;

  /* Getters.  */
  int number () const { return m_number; };
  bool enabled () const { return m_enabled; };
  bool file_is_glob () const { return m_file_is_glob; }
  const std::string &file () const { return m_file; }
  const std::string &function () const { return m_function; }
  bool function_is_regexp () const { return m_function_is_regexp; }

  /* Setters.  */
  void enable () { m_enabled = true; };
  void disable () { m_enabled = false; };

  /* Print a gdb command that can be used to recreate this skip.  */
  void print_recreate (ui_file *stream) const;

private:
  /* Key that grants access to the constructor.  */
  struct private_key {};
public:
  /* Public so we can construct with container::emplace_back.  Since
     it requires a private class key, it can't be called from outside.
     Use the add_entry static factory method to construct instead.  */
  skiplist_entry (bool file_is_glob, std::string &&file,
		  bool function_is_regexp, std::string &&function,
		  private_key);

  /* Disable copy.  */
  DISABLE_COPY_AND_ASSIGN (skiplist_entry);

private:
  /* Return true if we're stopped at a file to be skipped.  */
  bool do_skip_file_p (const symtab_and_line &function_sal) const;

  /* Return true if we're stopped at a globbed file to be skipped.  */
  bool do_skip_gfile_p (const symtab_and_line &function_sal) const;

private: /* data */
  int m_number = -1;

  /* True if FILE is a glob-style pattern.
     Otherwise it is the plain file name (possibly with directories).  */
  bool m_file_is_glob;

  /* The name of the file or empty if no name.  */
  std::string m_file;

  /* True if FUNCTION is a regexp.
     Otherwise it is a plain function name (possibly with arguments,
     for C++).  */
  bool m_function_is_regexp;

  /* The name of the function or empty if no name.  */
  std::string m_function;

  /* 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;
};

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,
				std::string &&function,
				private_key)
  : m_file_is_glob (file_is_glob),
    m_file (std::move (file)),
    m_function_is_regexp (function_is_regexp),
    m_function (std::move (function))
{
  gdb_assert (!m_file.empty () || !m_function.empty ());

  if (m_file_is_glob)
    {
      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)
    {
      gdb_assert (!m_function.empty ());
      m_compiled_function_regexp.emplace (m_function.c_str (),
					  REG_NOSUB | REG_EXTENDED,
					  _("regexp"));
    }
}

void
skiplist_entry::add_entry (bool file_is_glob, std::string &&file,
			   bool function_is_regexp, std::string &&function)
{
  skiplist_entries.emplace_back (file_is_glob,
				 std::move (file),
				 function_is_regexp,
				 std::move (function),
				 private_key {});

  /* Incremented after push_back, in case push_back throws.  */
  skiplist_entries.back ().m_number = ++highest_skiplist_entry_num;
}

/* A helper function for print_recreate that prints a correctly-quoted
   string to STREAM.  */

static void
print_quoted (ui_file *stream, const std::string &str)
{
  gdb_putc ('"', stream);
  for (char c : str)
    {
      if (ISSPACE (c) || c == '\\' || c == '\'' || c == '"')
	gdb_putc ('\\', stream);
      gdb_putc (c, stream);
    }
  gdb_putc ('"', stream);
}

void
skiplist_entry::print_recreate (ui_file *stream) const
{
  if (!m_file_is_glob && !m_file.empty ()
      && !m_function_is_regexp && m_function.empty ())
    gdb_printf (stream, "skip file %s\n", m_file.c_str ());
  else if (!m_file_is_glob && m_file.empty ()
      && !m_function_is_regexp && !m_function.empty ())
    gdb_printf (stream, "skip function %s\n", m_function.c_str ());
  else
    {
      gdb_printf (stream, "skip ");
      if (!m_file.empty ())
	{
	  if (m_file_is_glob)
	    gdb_printf (stream, "-gfile ");
	  else
	    gdb_printf (stream, "-file ");
	  print_quoted (stream, m_file);
	}
      if (!m_function.empty ())
	{
	  if (m_function_is_regexp)
	    gdb_printf (stream, "-rfunction ");
	  else
	    gdb_printf (stream, "-function ");
	  print_quoted (stream, m_function);
	}
      gdb_printf (stream, "\n");
    }
}

static void
skip_file_command (const char *arg, int from_tty)
{
  struct symtab *symtab;
  const char *filename = NULL;

  /* If no argument was given, try to default to the last
     displayed codepoint.  */
  if (arg == NULL)
    {
      symtab = get_last_displayed_symtab ();
      if (symtab == NULL)
	error (_("No default file now."));

      /* It is not a typo, symtab_to_filename_for_display would be needlessly
	 ambiguous.  */
      filename = symtab_to_fullname (symtab);
    }
  else
    filename = arg;

  skiplist_entry::add_entry (false, std::string (filename),
			     false, std::string ());

  gdb_printf (_("File %ps will be skipped when stepping.\n"),
	      styled_string (file_name_style.style (), filename));
}

/* Create a skiplist entry for the given function NAME and add it to the
   list.  */

static void
skip_function (const char *name)
{
  skiplist_entry::add_entry (false, std::string (), false, std::string (name));

  gdb_printf (_("Function %ps will be skipped when stepping.\n"),
	      styled_string (function_name_style.style (), name));
}

static void
skip_function_command (const char *arg, int from_tty)
{
  /* Default to the current function if no argument is given.  */
  if (arg == NULL)
    {
      frame_info_ptr fi = get_selected_frame (_("No default function now."));
      struct symbol *sym = get_frame_function (fi);
      const char *name = NULL;

      if (sym != NULL)
	name = sym->print_name ();
      else
	error (_("No function found containing current program point %s."),
	       paddress (get_current_arch (), get_frame_pc (fi)));
      skip_function (name);
      return;
    }

  skip_function (arg);
}

/* Storage for the "skip" command option values.  */

struct skip_opts
{
  /* For the -file option.  */
  std::string file;

  /* For the -gfile option.  */
  std::string gfile;

  /* For the -function option.  */
  std::string function;

  /* For the -rfunction option.  */
  std::string rfunction;
};

/* The options for the "skip" command.  */

static const gdb::option::option_def skip_option_defs[] = {
  gdb::option::filename_option_def<skip_opts> {
    "file",
    [] (skip_opts *opts) { return &opts->file; },
    nullptr, /* show_cmd_cb */
    nullptr, /* set_doc */
  },

  gdb::option::filename_option_def<skip_opts> {
    "gfile",
    [] (skip_opts *opts) { return &opts->gfile; },
    nullptr, /* show_cmd_cb */
    nullptr, /* set_doc */
  },

  gdb::option::string_option_def<skip_opts> {
    "function",
    [] (skip_opts *opts) { return &opts->function; },
    nullptr, /* show_cmd_cb */
    nullptr, /* set_doc */
  },

  gdb::option::string_option_def<skip_opts> {
    "rfunction",
    [] (skip_opts *opts) { return &opts->rfunction; },
    nullptr, /* show_cmd_cb */
    nullptr, /* set_doc */
  },
};

/* Create the option_def_group for the "skip" command.  */

static inline gdb::option::option_def_group
make_skip_options_def_group (skip_opts *opts)
{
  return {{skip_option_defs}, opts};
}

/* Completion for the "skip" command.  */

static void
skip_command_completer (struct cmd_list_element *cmd,
			completion_tracker &tracker,
			const char *text, const char * /* word */)
{
  /* We only need to handle option completion here.  The sub-commands of
     'skip' are handled automatically by the command system.  */
  const auto group = make_skip_options_def_group (nullptr);
  gdb::option::complete_options
    (tracker, &text, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_OPERAND, group);
}

/* Process "skip ..." that does not match "skip file" or "skip function".  */

static void
skip_command (const char *arg, int from_tty)
{
  if (arg == nullptr)
    {
      skip_function_command (arg, from_tty);
      return;
    }

  /* Parse command line options.  */
  skip_opts opts;
  const auto group = make_skip_options_def_group (&opts);
  gdb::option::process_options
    (&arg, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_OPERAND, group);

  /* Handle invalid argument combinations.  */
  if (!opts.file.empty () && !opts.gfile.empty ())
    error (_("Cannot specify both -file and -gfile."));

  if (!opts.function.empty () && !opts.rfunction.empty ())
    error (_("Cannot specify both -function and -rfunction."));

  /* If there's anything left on the command line then this is an error if
     a valid command line flag was also given, or we assume that it's a
     function name if the user entered 'skip blah'.  */
  if (*arg != '\0')
    {
      if (!opts.file.empty () || !opts.gfile.empty ()
	  || !opts.function.empty () || !opts.rfunction.empty ())
	error (_("Junk after arguments: %s"), arg);
      else if (*arg == '-')
	{
	  const char *after_arg = skip_to_space (arg);
	  error (_("Invalid skip option: %.*s"), (int) (after_arg - arg), arg);
	}
      else
	{
	  /* Assume the user entered "skip FUNCTION-NAME".  FUNCTION-NAME
	     may be `foo (int)', and therefore we pass the complete
	     original ARG to skip_function_command as if the user typed
	     "skip function arg".  */
	  skip_function_command (arg, from_tty);
	  return;
	}
    }

  /* This shouldn't happen as "skip" by itself gets punted to
     skip_function_command, and if the user entered "skip blah" then this
     will have been handled above.  */
  gdb_assert (!opts.file.empty () || !opts.gfile.empty ()
	      || !opts.function.empty () || !opts.rfunction.empty ());

  /* Create the skip list entry.  */
  std::string entry_file;
  if (!opts.file.empty ())
    entry_file = opts.file;
  else if (!opts.gfile.empty ())
    entry_file = opts.gfile;

  std::string entry_function;
  if (!opts.function.empty ())
    entry_function = opts.function;
  else if (!opts.rfunction.empty ())
    entry_function = opts.rfunction;

  skiplist_entry::add_entry (!opts.gfile.empty (),
			     std::move (entry_file),
			     !opts.rfunction.empty (),
			     std::move (entry_function));

  /* I18N concerns drive some of the choices here (we can't piece together
     the output too much).  OTOH we want to keep this simple.  Therefore the
     only polish we add to the output is to append "(s)" to "File" or
     "Function" if they're a glob/regexp.  */
  {
    std::string &file_to_print
      = !opts.file.empty () ? opts.file : opts.gfile;
    std::string &function_to_print
      = !opts.function.empty () ? opts.function : opts.rfunction;

    const char *file_text = !opts.gfile.empty () ? _("File(s)") : _("File");
    const char *lower_file_text = !opts.gfile.empty () ? _("file(s)") : _("file");
    const char *function_text
      = !opts.rfunction.empty () ? _("Function(s)") : _("Function");

    if (function_to_print.empty ())
      {
	gdb_printf (_("%s %ps will be skipped when stepping.\n"),
		    file_text,
		    styled_string (file_name_style.style (),
				   file_to_print.c_str ()));
      }
    else if (file_to_print.empty ())
      {
	gdb_printf (_("%s %ps will be skipped when stepping.\n"),
		    function_text,
		    styled_string (function_name_style.style (),
				   function_to_print.c_str ()));
      }
    else
      {
	gdb_printf (_("%s %ps in %s %ps will be skipped"
		      " when stepping.\n"),
		    function_text,
		    styled_string (function_name_style.style (),
				   function_to_print.c_str ()),
		    lower_file_text,
		    styled_string (file_name_style.style (),
				   file_to_print.c_str ()));
      }
  }
}

static void
info_skip_command (const char *arg, int from_tty)
{
  int num_printable_entries = 0;
  struct value_print_options opts;

  get_user_print_options (&opts);

  /* Count the number of rows in the table and see if we need space for a
     64-bit address anywhere.  */
  for (const skiplist_entry &e : skiplist_entries)
    if (arg == NULL || number_is_in_list (arg, e.number ()))
      num_printable_entries++;

  if (num_printable_entries == 0)
    {
      if (arg == NULL)
	current_uiout->message (_("Not skipping any files or functions.\n"));
      else
	current_uiout->message (
	  _("No skiplist entries found with number %s.\n"), arg);

      return;
    }

  ui_out_emit_table table_emitter (current_uiout, 6, num_printable_entries,
				   "SkiplistTable");

  current_uiout->table_header (5, ui_left, "number", "Num");   /* 1 */
  current_uiout->table_header (3, ui_left, "enabled", "Enb");  /* 2 */
  current_uiout->table_header (4, ui_right, "regexp", "Glob"); /* 3 */
  current_uiout->table_header (20, ui_left, "file", "File");   /* 4 */
  current_uiout->table_header (2, ui_right, "regexp", "RE");   /* 5 */
  current_uiout->table_header (40, ui_noalign, "function", "Function"); /* 6 */
  current_uiout->table_body ();

  for (const skiplist_entry &e : skiplist_entries)
    {
      QUIT;
      if (arg != NULL && !number_is_in_list (arg, e.number ()))
	continue;

      ui_out_emit_tuple tuple_emitter (current_uiout, "blklst-entry");
      current_uiout->field_signed ("number", e.number ()); /* 1 */

      if (e.enabled ())
	current_uiout->field_string ("enabled", "y"); /* 2 */
      else
	current_uiout->field_string ("enabled", "n"); /* 2 */

      if (e.file_is_glob ())
	current_uiout->field_string ("regexp", "y"); /* 3 */
      else
	current_uiout->field_string ("regexp", "n"); /* 3 */

      current_uiout->field_string ("file",
				   e.file ().empty () ? "<none>"
				   : e.file ().c_str (),
				   e.file ().empty ()
				   ? metadata_style.style ()
				   : file_name_style.style ()); /* 4 */
      if (e.function_is_regexp ())
	current_uiout->field_string ("regexp", "y"); /* 5 */
      else
	current_uiout->field_string ("regexp", "n"); /* 5 */

      current_uiout->field_string ("function",
				   e.function ().empty () ? "<none>"
				   : e.function ().c_str (),
				   e.function ().empty ()
				   ? metadata_style.style ()
				   : function_name_style.style ()); /* 6 */

      current_uiout->text ("\n");
    }
}

static void
skip_enable_command (const char *arg, int from_tty)
{
  bool found = false;

  for (skiplist_entry &e : skiplist_entries)
    if (arg == NULL || number_is_in_list (arg, e.number ()))
      {
	e.enable ();
	found = true;
      }

  if (!found)
    error (_("No skiplist entries found with number %s."), arg);
}

static void
skip_disable_command (const char *arg, int from_tty)
{
  bool found = false;

  for (skiplist_entry &e : skiplist_entries)
    if (arg == NULL || number_is_in_list (arg, e.number ()))
      {
	e.disable ();
	found = true;
      }

  if (!found)
    error (_("No skiplist entries found with number %s."), arg);
}

static void
skip_delete_command (const char *arg, int from_tty)
{
  bool found = false;

  for (auto it = skiplist_entries.begin (),
	 end = skiplist_entries.end ();
       it != end;)
    {
      const skiplist_entry &e = *it;

      if (arg == NULL || number_is_in_list (arg, e.number ()))
	{
	  it = skiplist_entries.erase (it);
	  found = true;
	}
      else
	++it;
    }

  if (!found)
    error (_("No skiplist entries found with number %s."), arg);
}

bool
skiplist_entry::do_skip_file_p (const symtab_and_line &function_sal) const
{
  bool result;

  /* Check first sole SYMTAB->FILENAME.  It may not be a substring of
     symtab_to_fullname as it may contain "./" etc.  */
  if (compare_filenames_for_search (function_sal.symtab->filename (),
				    m_file.c_str ()))
    result = true;

  /* Before we invoke realpath, which can get expensive when many
     files are involved, do a quick comparison of the basenames.  */
  else if (!basenames_may_differ
	   && filename_cmp (lbasename (function_sal.symtab->filename ()),
			    lbasename (m_file.c_str ())) != 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);

      result = compare_filenames_for_search (fullname, m_file.c_str ());
    }

  return result;
}

/* The implementation of skiplist_entry::do_skip_gfile_p.  This exists as a
   separate function so that this function can be unit tested.

   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.

   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)
    return false;

  /* 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 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
skiplist_entry::skip_file_p (const symtab_and_line &function_sal) const
{
  if (m_file.empty ())
    return false;

  if (function_sal.symtab == NULL)
    return false;

  skip_debug_printf ("checking if file %s matches %sglob %s",
		     function_sal.symtab->filename (),
		     (m_file_is_glob ? "" : "non-"), m_file.c_str ());

  bool result;
  if (m_file_is_glob)
    result = do_skip_gfile_p (function_sal);
  else
    result = do_skip_file_p (function_sal);

  skip_debug_printf (result ? "yes" : "no");

  return result;
}

bool
skiplist_entry::skip_function_p (const char *function_name) const
{
  if (m_function.empty ())
    return false;

  bool result;

  if (m_function_is_regexp)
    {
      skip_debug_printf ("checking if function %s matches regex %s",
			 function_name, m_function.c_str ());

      gdb_assert (m_compiled_function_regexp);
      result
	= (m_compiled_function_regexp->exec (function_name, 0, NULL, 0) == 0);
    }
  else
    {
      skip_debug_printf ("checking if function %s matches non-regex %s",
			 function_name, m_function.c_str ());
      result = (strcmp_iw (function_name, m_function.c_str ()) == 0);
    }

  skip_debug_printf (result ? "yes" : "no");

  return result;
}

/* See skip.h.  */

bool
function_name_is_marked_for_skip (const char *function_name,
				  const symtab_and_line &function_sal)
{
  if (function_name == NULL)
    return false;

  for (const skiplist_entry &e : skiplist_entries)
    {
      if (!e.enabled ())
	continue;

      bool skip_by_file = e.skip_file_p (function_sal);
      bool skip_by_function = e.skip_function_p (function_name);

      /* If both file and function must match, make sure we don't errantly
	 exit if only one of them match.  */
      if (!e.file ().empty () && !e.function ().empty ())
	{
	  if (skip_by_file && skip_by_function)
	    return true;
	}
      /* Only one of file/function is specified.  */
      else if (skip_by_file || skip_by_function)
	return true;
    }

  return false;
}

/* Completer for skip numbers.  */

static void
complete_skip_number (cmd_list_element *cmd,
		      completion_tracker &completer,
		      const char *text, const char *word)
{
  size_t word_len = strlen (word);

  for (const skiplist_entry &entry : skiplist_entries)
    {
      gdb::unique_xmalloc_ptr<char> name = xstrprintf ("%d", entry.number ());
      if (strncmp (word, name.get (), word_len) == 0)
	completer.add_completion (std::move (name));
    }
}

/* Implementation of 'save skip' command.  */

static void
save_skip_command (const char *filename, int from_tty)
{
  if (filename == nullptr || *filename == '\0')
    error (_("Argument required (file name in which to save)"));

  gdb::unique_xmalloc_ptr<char> expanded_filename (tilde_expand (filename));
  stdio_file fp;
  if (!fp.open (expanded_filename.get (), "w"))
    error (_("Unable to open file '%ps' for saving (%s)"),
	   styled_string (file_name_style.style (), expanded_filename.get ()),
	   safe_strerror (errno));

  for (const auto &entry : skiplist_entries)
    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;
  struct cmd_list_element *c;

  c = add_prefix_cmd ("skip", class_breakpoint, skip_command, _("\
Ignore a function while stepping.\n\
\n\
Usage: skip [FUNCTION-NAME]\n\
       skip [FILE-SPEC] [FUNCTION-SPEC]\n\
If no arguments are given, ignore the current function.\n\
\n\
FILE-SPEC is one of:\n\
       -file FILE-NAME\n\
       -gfile GLOB-FILE-PATTERN\n\
FUNCTION-SPEC is one of:\n\
       -function FUNCTION-NAME\n\
       -rfunction FUNCTION-NAME-REGULAR-EXPRESSION"),
		  &skiplist, 1, &cmdlist);
  set_cmd_completer_handle_brkchars (c, skip_command_completer);

  c = add_cmd ("file", class_breakpoint, skip_file_command, _("\
Ignore a file while stepping.\n\
Usage: skip file [FILE-NAME]\n\
If no filename is given, ignore the current file."),
	       &skiplist);
  set_cmd_completer (c, deprecated_filename_completer);

  c = add_cmd ("function", class_breakpoint, skip_function_command, _("\
Ignore a function while stepping.\n\
Usage: skip function [FUNCTION-NAME]\n\
If no function name is given, skip the current function."),
	       &skiplist);
  set_cmd_completer (c, location_completer);

  c = add_cmd ("enable", class_breakpoint, skip_enable_command, _("\
Enable skip entries.\n\
Usage: skip enable [NUMBER | RANGE]...\n\
You can specify numbers (e.g. \"skip enable 1 3\"),\n\
ranges (e.g. \"skip enable 4-8\"), or both (e.g. \"skip enable 1 3 4-8\").\n\n\
If you don't specify any numbers or ranges, we'll enable all skip entries."),
	       &skiplist);
  set_cmd_completer (c, complete_skip_number);
  add_alias_cmd ("skip", c, class_breakpoint, 0, &enablelist);

  c = add_cmd ("disable", class_breakpoint, skip_disable_command, _("\
Disable skip entries.\n\
Usage: skip disable [NUMBER | RANGE]...\n\
You can specify numbers (e.g. \"skip disable 1 3\"),\n\
ranges (e.g. \"skip disable 4-8\"), or both (e.g. \"skip disable 1 3 4-8\").\n\n\
If you don't specify any numbers or ranges, we'll disable all skip entries."),
	       &skiplist);
  set_cmd_completer (c, complete_skip_number);
  add_alias_cmd ("skip", c, class_breakpoint, 0, &disablelist);

  c = add_cmd ("delete", class_breakpoint, skip_delete_command, _("\
Delete skip entries.\n\
Usage: skip delete [NUMBER | RANGES]...\n\
You can specify numbers (e.g. \"skip delete 1 3\"),\n\
ranges (e.g. \"skip delete 4-8\"), or both (e.g. \"skip delete 1 3 4-8\").\n\n\
If you don't specify any numbers or ranges, we'll delete all skip entries."),
	       &skiplist);
  set_cmd_completer (c, complete_skip_number);
  add_alias_cmd ("skip", c, class_breakpoint, 0, &deletelist);

  add_info ("skip", info_skip_command, _("\
Display the status of skips.\n\
Usage: info skip [NUMBER | RANGES]...\n\
You can specify numbers (e.g. \"info skip 1 3\"),\n\
ranges (e.g. \"info skip 4-8\"), or both (e.g. \"info skip 1 3 4-8\").\n\n\
If you don't specify any numbers or ranges, we'll show all skips."));
  set_cmd_completer (c, complete_skip_number);

  add_setshow_boolean_cmd ("skip", class_maintenance,
			   &debug_skip, _("\
Set whether to print the debug output about skipping files and functions."),
			   _("\
Show whether the debug output about skipping files and functions is printed."),
			   _("\
When non-zero, debug output about skipping files and functions is displayed."),
			   NULL, NULL,
			   &setdebuglist, &showdebuglist);

  c = add_cmd ("skip", no_class, save_skip_command, _("\
Save current skips as a script.\n\
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
}
