diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index e44df54ddea..88ae024bbd8 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -241,6 +241,7 @@ SUBDIR_CLI_SRCS = \
 	cli/cli-dump.c \
 	cli/cli-interp.c \
 	cli/cli-logging.c \
+	cli/cli-option.c \
 	cli/cli-script.c \
 	cli/cli-setshow.c \
 	cli/cli-style.c \
@@ -1062,6 +1063,7 @@ COMMON_SFILES = \
 	macrotab.c \
 	main.c \
 	maint.c \
+	maint-test-options.c \
 	maint-test-settings.c \
 	mdebugread.c \
 	mem-break.c \
diff --git a/gdb/cli/cli-decode.c b/gdb/cli/cli-decode.c
index 0eae0dc0005..be38d9b287a 100644
--- a/gdb/cli/cli-decode.c
+++ b/gdb/cli/cli-decode.c
@@ -549,6 +549,7 @@ add_setshow_enum_cmd (const char *name,
   set_cmd_completer (show, nullptr);
 }
 
+/* See cli-decode.h.  */
 const char * const auto_boolean_enums[] = { "on", "off", "auto", NULL };
 
 /* Add an auto-boolean command named NAME to both the set and show
@@ -578,6 +579,9 @@ add_setshow_auto_boolean_cmd (const char *name,
   set_cmd_completer (show, nullptr);
 }
 
+/* See cli-decode.h.  */
+const char * const boolean_enums[] = { "on", "off", NULL };
+
 /* Add element named NAME to both the set and show command LISTs (the
    list for set/show or some sublist thereof).  CLASS is as in
    add_cmd.  VAR is address of the variable which will contain the
@@ -591,7 +595,6 @@ add_setshow_boolean_cmd (const char *name, enum command_class theclass, int *var
 			 struct cmd_list_element **set_list,
 			 struct cmd_list_element **show_list)
 {
-  static const char *boolean_enums[] = { "on", "off", NULL };
   cmd_list_element *set, *show;
 
   add_setshow_cmd_full (name, theclass, var_boolean, var,
diff --git a/gdb/cli/cli-decode.h b/gdb/cli/cli-decode.h
index c53683d95c0..13b4d26dd1d 100644
--- a/gdb/cli/cli-decode.h
+++ b/gdb/cli/cli-decode.h
@@ -261,6 +261,10 @@ extern void not_just_help_class_command (const char *arg, int from_tty);
 
 extern void print_doc_line (struct ui_file *, const char *);
 
+/* The enums of boolean commands.  */
+extern const char * const boolean_enums[];
+
+/* The enums of auto-boolean commands.  */
 extern const char * const auto_boolean_enums[];
 
 /* Verify whether a given cmd_list_element is a user-defined command.
diff --git a/gdb/cli/cli-option.c b/gdb/cli/cli-option.c
new file mode 100644
index 00000000000..432555a953f
--- /dev/null
+++ b/gdb/cli/cli-option.c
@@ -0,0 +1,732 @@
+/* CLI options framework, for GDB.
+
+   Copyright (C) 2017-2019 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   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 "defs.h"
+#include "cli/cli-option.h"
+#include "cli/cli-decode.h"
+#include "cli/cli-utils.h"
+#include "cli/cli-setshow.h"
+#include "command.h"
+#include <vector>
+
+namespace gdb {
+namespace option {
+
+/* An option's value.  Which field is active depends on the option's
+   type.  */
+union option_value
+{
+  /* For var_boolean options.  */
+  bool boolean;
+
+  /* For var_uinteger options.  */
+  unsigned int uinteger;
+
+  /* For var_zuinteger_unlimited options.  */
+  int integer;
+
+  /* For var_enum options.  */
+  const char *enumeration;
+};
+
+/* Holds an options definition and its value.  */
+struct option_def_and_value
+{
+  /* The option definition.  */
+  const option_def &option;
+
+  /* A context.  */
+  void *ctx;
+
+  /* The option's value, if any.  */
+  gdb::optional<option_value> value;
+};
+
+/* Info passed around when handling completion.  */
+struct parse_option_completion_info
+{
+  /* The completion word.  */
+  const char *word;
+
+  /* The tracker.  */
+  completion_tracker &tracker;
+};
+
+/* If ARGS starts with "-", look for a "--" delimiter.  If one is
+   found, then interpret everything up until the "--" as command line
+   options.  Otherwise, interpret unknown input as the beginning of
+   the command's operands.  */
+
+static const char *
+find_end_options_delimiter (const char *args)
+{
+  if (args[0] == '-')
+    {
+      const char *p = args;
+
+      p = skip_spaces (p);
+      while (*p)
+	{
+	  if (check_for_argument (&p, "--"))
+	    return p;
+	  else
+	    p = skip_to_space (p);
+	  p = skip_spaces (p);
+	}
+    }
+
+  return nullptr;
+}
+
+/* Complete TEXT/WORD on all options in OPTIONS_GROUP.  */
+
+static void
+complete_on_options (gdb::array_view<const option_def_group> options_group,
+		     completion_tracker &tracker,
+		     const char *text, const char *word)
+{
+  size_t textlen = strlen (text);
+  for (const auto &grp : options_group)
+    for (const auto &opt : grp.options)
+      if (strncmp (opt.name, text, textlen) == 0)
+	{
+	  tracker.add_completion
+	    (make_completion_match_str (opt.name, text, word));
+	}
+}
+
+/* See cli-option.h.  */
+
+void
+complete_on_all_options (completion_tracker &tracker,
+			 gdb::array_view<const option_def_group> options_group)
+{
+  static const char opt[] = "-";
+  complete_on_options (options_group, tracker, opt + 1, opt);
+}
+
+/* Dup STR and return a unique_xmalloc_ptr for the result.  */
+
+static gdb::unique_xmalloc_ptr<char>
+make_unique_xstrdup (const char *str)
+{
+  return gdb::unique_xmalloc_ptr<char> (xstrdup (str));
+}
+
+/* Parse ARGS, guided by OPTIONS_GROUP.  HAVE_DELIMITER is true if the
+   whole ARGS line included the "--" options-terminator delimiter.  */
+
+static gdb::optional<option_def_and_value>
+parse_option (gdb::array_view<const option_def_group> options_group,
+	      process_options_mode mode,
+	      bool have_delimiter,
+	      const char **args,
+	      parse_option_completion_info *completion = nullptr)
+{
+  if (*args == nullptr)
+    return {};
+  else if (**args != '-')
+    {
+      if (have_delimiter)
+	error (_("Unrecognized option at: %s"), *args);
+      return {};
+    }
+  else if (check_for_argument (args, "--"))
+    return {};
+
+  /* Skip the initial '-'.  */
+  const char *arg = *args + 1;
+
+  const char *after = skip_to_space (arg);
+  size_t len = after - arg;
+  const option_def *match = nullptr;
+  void *match_ctx;
+
+  for (const auto &grp : options_group)
+    {
+      for (const auto &o : grp.options)
+	{
+	  if (strncmp (o.name, arg, len) == 0)
+	    {
+	      if (match != nullptr)
+		{
+		  if (completion != nullptr && arg[len] == '\0')
+		    {
+		      complete_on_options (options_group,
+					   completion->tracker,
+					   arg, completion->word);
+		      return {};
+		    }
+
+		  error (_("Ambiguous option at: -%s"), arg);
+		}
+
+	      match = &o;
+	      match_ctx = grp.ctx;
+
+	      if ((isspace (arg[len]) || arg[len] == '\0')
+		  && strlen (o.name) == len)
+		break; /* Exact match.  */
+	    }
+	}
+    }
+
+  if (match == nullptr)
+    {
+      if (have_delimiter || mode != PROCESS_OPTIONS_UNKNOWN_IS_OPERAND)
+	error (_("Unrecognized option at: %s"), *args);
+
+      return {};
+    }
+
+  if (completion != nullptr && arg[len] == '\0')
+    {
+      complete_on_options (options_group, completion->tracker,
+			   arg, completion->word);
+      return {};
+    }
+
+  *args += 1 + len;
+  *args = skip_spaces (*args);
+  if (completion != nullptr)
+    completion->word = *args;
+
+  switch (match->type)
+    {
+    case var_boolean:
+      {
+	if (!match->have_argument)
+	  {
+	    option_value val;
+	    val.boolean = true;
+	    return option_def_and_value {*match, match_ctx, val};
+	  }
+
+	const char *val_str = *args;
+	int res;
+
+	if (**args == '\0' && completion != nullptr)
+	  {
+	    /* Complete on both "on/off" and more options.  */
+
+	    if (mode == PROCESS_OPTIONS_REQUIRE_DELIMITER)
+	      {
+		complete_on_enum (completion->tracker,
+				  boolean_enums, val_str, val_str);
+		complete_on_all_options (completion->tracker, options_group);
+	      }
+	    return option_def_and_value {*match, match_ctx};
+	  }
+	else if (**args == '-')
+	  {
+	    /* Treat:
+		 "cmd -boolean-option -another-opt..."
+	       as:
+		 "cmd -boolean-option on -another-opt..."
+	     */
+	    res = 1;
+	  }
+	else if (**args == '\0')
+	  {
+	    /* Treat:
+		 (1) "cmd -boolean-option "
+	       as:
+		 (1) "cmd -boolean-option on"
+	     */
+	    res = 1;
+	  }
+	else
+	  {
+	    res = parse_cli_boolean_value (args);
+	    if (res < 0)
+	      {
+		const char *end = skip_to_space (*args);
+		if (completion != nullptr)
+		  {
+		    if (*end == '\0')
+		      {
+			complete_on_enum (completion->tracker,
+					  boolean_enums, val_str, val_str);
+			return option_def_and_value {*match, match_ctx};
+		      }
+		  }
+
+		if (have_delimiter)
+		  error (_("Value given for `-%s' is not a boolean: %.*s"),
+			 match->name, (int) (end - val_str), val_str);
+		/* The user didn't separate options from operands
+		   using "--", so treat this unrecognized value as the
+		   start of the operands.  This makes "frame apply all
+		   -past-main CMD" work.  */
+		return option_def_and_value {*match, match_ctx};
+	      }
+	    else if (completion != nullptr && **args == '\0')
+	      {
+		/* While "cmd -boolean [TAB]" only offers "on" and
+		   "off", the boolean option actually accepts "1",
+		   "yes", etc. as boolean values.  We complete on all
+		   of those instead of BOOLEAN_ENUMS here to make
+		   these work:
+
+		    "p -object 1[TAB]" -> "p -object 1 "
+		    "p -object ye[TAB]" -> "p -object yes "
+
+		   Etc.  Note that it's important that the space is
+		   auto-appended.  Otherwise, if we only completed on
+		   on/off here, then it might look to the user like
+		   "1" isn't valid, like:
+		   "p -object 1[TAB]" -> "p -object 1" (i.e., nothing happens).
+		*/
+		static const char *const all_boolean_enums[] = {
+		  "on", "off",
+		  "yes", "no",
+		  "enable", "disable",
+		  "0", "1",
+		  nullptr,
+		};
+		complete_on_enum (completion->tracker, all_boolean_enums,
+				  val_str, val_str);
+		return {};
+	      }
+	  }
+
+	option_value val;
+	val.boolean = res;
+	return option_def_and_value {*match, match_ctx, val};
+      }
+    case var_uinteger:
+    case var_zuinteger_unlimited:
+      {
+	if (completion != nullptr)
+	  {
+	    if (**args == '\0')
+	      {
+		/* Convenience to let the user know what the option
+		   can accept.  Note there's no common prefix between
+		   the strings on purpose, so that readline doesn't do
+		   a partial match.  */
+		completion->tracker.add_completion
+		  (make_unique_xstrdup ("NUMBER"));
+		completion->tracker.add_completion
+		  (make_unique_xstrdup ("unlimited"));
+		return {};
+	      }
+	    else if (startswith ("unlimited", *args))
+	      {
+		completion->tracker.add_completion
+		  (make_unique_xstrdup ("unlimited"));
+		return {};
+	      }
+	  }
+
+	if (match->type == var_zuinteger_unlimited)
+	  {
+	    option_value val;
+	    val.integer = parse_cli_var_zuinteger_unlimited (args, false);
+	    return option_def_and_value {*match, match_ctx, val};
+	  }
+	else
+	  {
+	    option_value val;
+	    val.uinteger = parse_cli_var_uinteger (match->type, args, false);
+	    return option_def_and_value {*match, match_ctx, val};
+	  }
+      }
+    case var_enum:
+      {
+	if (completion != nullptr)
+	  {
+	    const char *after_arg = skip_to_space (*args);
+	    if (*after_arg == '\0')
+	      {
+		complete_on_enum (completion->tracker,
+				  match->enums, *args, *args);
+		*args = after_arg;
+
+		option_value val;
+		val.enumeration = nullptr;
+		return option_def_and_value {*match, match_ctx, val};
+	      }
+	  }
+
+	if (check_for_argument (args, "--"))
+	  {
+	    /* Treat e.g., "backtrace -entry-values --" as if there
+	       was no argument after "-entry-values".  This makes
+	       parse_cli_var_enum throw an error with a suggestion of
+	       what are the valid options.  */
+	    args = nullptr;
+	  }
+
+	option_value val;
+	val.enumeration = parse_cli_var_enum (args, match->enums);
+	return option_def_and_value {*match, match_ctx, val};
+      }
+
+    default:
+      /* Not yet.  */
+      gdb_assert_not_reached (_("option type not supported"));
+    }
+
+  return {};
+}
+
+/* See cli-option.h.  */
+
+bool
+complete_options (completion_tracker &tracker,
+		  const char **args,
+		  process_options_mode mode,
+		  gdb::array_view<const option_def_group> options_group)
+{
+  const char *text = *args;
+
+  tracker.set_use_custom_word_point (true);
+
+  const char *delimiter = find_end_options_delimiter (text);
+  bool have_delimiter = delimiter != nullptr;
+
+  if (text[0] == '-' && (!have_delimiter || *delimiter == '\0'))
+    {
+      parse_option_completion_info completion_info {nullptr, tracker};
+
+      while (1)
+	{
+	  *args = skip_spaces (*args);
+	  completion_info.word = *args;
+
+	  if (strcmp (*args, "-") == 0)
+	    {
+	      complete_on_options (options_group, tracker, *args + 1,
+				   completion_info.word);
+	    }
+	  else if (strcmp (*args, "--") == 0)
+	    {
+	      tracker.add_completion (make_unique_xstrdup (*args));
+	    }
+	  else if (**args == '-')
+	    {
+	      gdb::optional<option_def_and_value> ov
+		= parse_option (options_group, mode, have_delimiter,
+				args, &completion_info);
+	      if (!ov && !tracker.have_completions ())
+		{
+		  tracker.advance_custom_word_point_by (*args - text);
+		  return mode == PROCESS_OPTIONS_REQUIRE_DELIMITER;
+		}
+
+	      if (ov
+		  && ov->option.type == var_boolean
+		  && !ov->value.has_value ())
+		{
+		  /* Looked like a boolean option, but we failed to
+		     parse the value.  If this command requires a
+		     delimiter, this value can't be the start of the
+		     operands, so return true.  Otherwise, if the
+		     command doesn't require a delimiter return false
+		     so that the caller tries to complete on the
+		     operand.  */
+		  tracker.advance_custom_word_point_by (*args - text);
+		  return mode == PROCESS_OPTIONS_REQUIRE_DELIMITER;
+		}
+
+	      /* If we parsed an option with an argument, and reached
+		 the end of the input string with no trailing space,
+		 return true, so that our callers don't try to
+		 complete anything by themselves.  E.g., this makes it
+		 so that with:
+
+		  (gdb) frame apply all -limit 10[TAB]
+
+		 we don't try to complete on command names.  */
+	      if (ov
+		  && !tracker.have_completions ()
+		  && **args == '\0'
+		  && *args > text && !isspace ((*args)[-1]))
+		{
+		  tracker.advance_custom_word_point_by
+		    (*args - text);
+		  return true;
+		}
+	    }
+	  else
+	    {
+	      tracker.advance_custom_word_point_by
+		(completion_info.word - text);
+
+	      /* If the command requires a delimiter, but we haven't
+		 seen one, then return true, so that the caller
+		 doesn't try to complete on whatever follows options,
+		 which for these commands should only be done if
+		 there's a delimiter.  */
+	      if (mode == PROCESS_OPTIONS_REQUIRE_DELIMITER
+		  && !have_delimiter)
+		{
+		  /* If we reached the end of the input string, then
+		     offer all options, since that's all the user can
+		     type (plus "--").  */
+		  if (completion_info.word[0] == '\0')
+		    complete_on_all_options (tracker, options_group);
+		  return true;
+		}
+	      else
+		return false;
+	    }
+
+	  if (tracker.have_completions ())
+	    {
+	      tracker.advance_custom_word_point_by
+		(completion_info.word - text);
+	      return true;
+	    }
+	}
+    }
+  else if (delimiter != nullptr)
+    {
+      tracker.advance_custom_word_point_by (delimiter - text);
+      *args = delimiter;
+      return false;
+    }
+
+  return false;
+}
+
+/* See cli-option.h.  */
+
+bool
+process_options (const char **args,
+		 process_options_mode mode,
+		 gdb::array_view<const option_def_group> options_group)
+{
+  if (*args == nullptr)
+    return false;
+
+  /* If ARGS starts with "-", look for a "--" sequence.  If one is
+     found, then interpret everything up until the "--" as
+     'gdb::option'-style command line options.  Otherwise, interpret
+     ARGS as possibly the command's operands.  */
+  bool have_delimiter = find_end_options_delimiter (*args) != nullptr;
+
+  if (mode == PROCESS_OPTIONS_REQUIRE_DELIMITER && !have_delimiter)
+    return false;
+
+  bool processed_any = false;
+
+  while (1)
+    {
+      *args = skip_spaces (*args);
+
+      auto ov = parse_option (options_group, mode, have_delimiter, args);
+      if (!ov)
+	{
+	  if (processed_any)
+	    return true;
+	  return false;
+	}
+
+      processed_any = true;
+
+      switch (ov->option.type)
+	{
+	case var_boolean:
+	  {
+	    bool value = ov->value.has_value () ? ov->value->boolean : true;
+	    *ov->option.var_address.boolean (ov->option, ov->ctx) = value;
+	  }
+	  break;
+	case var_uinteger:
+	  *ov->option.var_address.uinteger (ov->option, ov->ctx)
+	    = ov->value->uinteger;
+	  break;
+	case var_zuinteger_unlimited:
+	  *ov->option.var_address.integer (ov->option, ov->ctx)
+	    = ov->value->integer;
+	  break;
+	case var_enum:
+	  *ov->option.var_address.enumeration (ov->option, ov->ctx)
+	    = ov->value->enumeration;
+	  break;
+	default:
+	  gdb_assert_not_reached ("unhandled option type");
+	}
+    }
+}
+
+/* Helper for build_help.  Return a fragment of a help string showing
+   OPT's possible values.  Returns NULL if OPT doesn't take an
+   argument.  */
+
+static const char *
+get_val_type_str (const option_def &opt, std::string &buffer)
+{
+  if (!opt.have_argument)
+    return nullptr;
+
+  switch (opt.type)
+    {
+    case var_boolean:
+      return "[on|off]";
+    case var_uinteger:
+    case var_zuinteger_unlimited:
+      return "NUMBER|unlimited";
+    case var_enum:
+      {
+	buffer = "";
+	for (size_t i = 0; opt.enums[i] != nullptr; i++)
+	  {
+	    if (i != 0)
+	      buffer += "|";
+	    buffer += opt.enums[i];
+	  }
+	return buffer.c_str ();
+      }
+    default:
+      return nullptr;
+    }
+}
+
+/* Helper for build_help.  Appends an indended version of DOC into
+   HELP.  */
+
+static void
+append_indended_doc (const char *doc, std::string &help)
+{
+  const char *p = doc;
+  const char *n = strchr (p, '\n');
+
+  while (n != nullptr)
+    {
+      help += "    ";
+      help.append (p, n - p + 1);
+      p = n + 1;
+      n = strchr (p, '\n');
+    }
+  help += "    ";
+  help += p;
+  help += '\n';
+}
+
+/* Fill HELP with an auto-generated "help" string fragment for
+   OPTIONS.  */
+
+static void
+build_help_option (gdb::array_view<const option_def> options,
+		   std::string &help)
+{
+  std::string buffer;
+
+  for (const auto &o : options)
+    {
+      if (o.set_doc == nullptr)
+	continue;
+
+      help += "  -";
+      help += o.name;
+
+      const char *val_type_str = get_val_type_str (o, buffer);
+      if (val_type_str != nullptr)
+	{
+	  help += ' ';
+	  help += val_type_str;
+	}
+      help += "\n";
+      append_indended_doc (o.set_doc, help);
+      if (o.help_doc != nullptr)
+	append_indended_doc (o.help_doc, help);
+      help += '\n';
+    }
+}
+
+/* See cli-option.h.  */
+
+std::string
+build_help (const char *help_tmpl,
+	    gdb::array_view<const option_def_group> options_group)
+{
+  std::string help_str;
+
+  const char *p = strstr (help_tmpl, "%OPTIONS%");
+  help_str.assign (help_tmpl, p);
+
+  for (const auto &grp : options_group)
+    for (const auto &opt : grp.options)
+      build_help_option (opt, help_str);
+
+  p += strlen ("%OPTIONS%");
+  help_str.append (p);
+
+  return help_str;
+}
+
+/* See cli-option.h.  */
+
+void
+add_setshow_cmds_for_options (command_class cmd_class,
+			      void *data,
+			      gdb::array_view<const option_def> options,
+			      struct cmd_list_element **set_list,
+			      struct cmd_list_element **show_list)
+{
+  for (const auto &option : options)
+    {
+      if (option.type == var_boolean)
+	{
+	  add_setshow_boolean_cmd (option.name, cmd_class,
+				   option.var_address.boolean (option, data),
+				   option.set_doc, option.show_doc,
+				   option.help_doc,
+				   nullptr, option.show_cmd_cb,
+				   set_list, show_list);
+	}
+      else if (option.type == var_uinteger)
+	{
+	  add_setshow_uinteger_cmd (option.name, cmd_class,
+				    option.var_address.uinteger (option, data),
+				    option.set_doc, option.show_doc,
+				    option.help_doc,
+				    nullptr, option.show_cmd_cb,
+				    set_list, show_list);
+	}
+      else if (option.type == var_zuinteger_unlimited)
+	{
+	  add_setshow_zuinteger_unlimited_cmd
+	    (option.name, cmd_class,
+	     option.var_address.integer (option, data),
+	     option.set_doc, option.show_doc,
+	     option.help_doc,
+	     nullptr, option.show_cmd_cb,
+	     set_list, show_list);
+	}
+      else if (option.type == var_enum)
+	{
+	  add_setshow_enum_cmd (option.name, cmd_class,
+				option.enums,
+				option.var_address.enumeration (option, data),
+				option.set_doc, option.show_doc,
+				option.help_doc,
+				nullptr, option.show_cmd_cb,
+				set_list, show_list);
+	}
+      else
+	gdb_assert_not_reached (_("option type not handled"));
+    }
+}
+
+} /* namespace option */
+} /* namespace gdb */
diff --git a/gdb/cli/cli-option.h b/gdb/cli/cli-option.h
new file mode 100644
index 00000000000..437fc09a61b
--- /dev/null
+++ b/gdb/cli/cli-option.h
@@ -0,0 +1,335 @@
+/* CLI options framework, for GDB.
+
+   Copyright (C) 2017-2019 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   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/>.  */
+
+#ifndef CLI_OPTION_H
+#define CLI_OPTION_H 1
+
+#include "common/gdb_optional.h"
+#include "common/array-view.h"
+#include "completer.h"
+#include <string>
+#include "command.h"
+
+namespace gdb {
+namespace option {
+
+/* A type-erased option definition.  The actual type of the option is
+   stored in the TYPE field.  Client code cannot define objects of
+   this type directly (the ctor is protected).  Instead, one of the
+   wrapper types below that extends this (boolean_option_def,
+   flag_option_def, uinteger_option_def, etc.) should be defined.  */
+struct option_def
+{
+  /* The ctor is protected because you're supposed to construct using
+     one of bool_option_def, etc. below.  */
+protected:
+  typedef void *(erased_var_address_ftype) ();
+
+  /* Construct an option.  NAME_ is the option's name.  VAR_TYPE_
+     defines the option's type.  ERASED_VAR_ADDRESS_ is a pointer to
+     the option's control variable.  SHOW_CMD_CB_ is a pointer to
+     callback for the "show" command that is installed for this
+     option.  SET_DOC_, SHOW_DOC_, HELP_DOC_ are used to create the
+     option's "set/show" commands.  */
+  constexpr option_def (const char *name_,
+			var_types var_type_,
+			erased_var_address_ftype *erased_var_address_,
+			show_value_ftype *show_cmd_cb_,
+			const char *set_doc_,
+			const char *show_doc_,
+			const char *help_doc_)
+    : name (name_), type (var_type_),
+      erased_var_address (erased_var_address_),
+      var_address {},
+      show_cmd_cb (show_cmd_cb_),
+      set_doc (set_doc_), show_doc (show_doc_), help_doc (help_doc_)
+  {}
+
+public:
+  /* The option's name.  */
+  const char *name;
+
+  /* The option's type.  */
+  var_types type;
+
+  /* A function that gets the controlling variable's address, type
+     erased.  */
+  erased_var_address_ftype *erased_var_address;
+
+  /* Get the controlling variable's address.  Each type of variable
+     uses a different union member.  We do this instead of having a
+     single hook that return a "void *", for better type safety.  This
+     way, actual instances of concrete option_def types
+     (boolean_option_def, etc.) fail to compile if you pass in a
+     function with incorrect return type.  CTX here is the aggregate
+     object that groups the option variables from which the callback
+     returns the address of some member.  */
+  union
+    {
+      int *(*boolean) (const option_def &, void *ctx);
+      unsigned int *(*uinteger) (const option_def &, void *ctx);
+      int *(*integer) (const option_def &, void *ctx);
+      const char **(*enumeration) (const option_def &, void *ctx);
+    }
+  var_address;
+
+  /* Pointer to null terminated list of enumerated values (like argv).
+     Only used by var_enum options.  */
+  const char *const *enums = nullptr;
+
+  /* True if the option takes an argument.  */
+  bool have_argument = true;
+
+  /* The "show" callback to use in the associated "show" command.
+     E.g., "show print elements".  */
+  show_value_ftype *show_cmd_cb;
+
+  /* The set/show/help strings.  These are shown in both the help of
+     commands that use the option group this option belongs to (e.g.,
+     "help print"), and in the associated commands (e.g., "set/show
+     print elements", "help set print elements").  */
+  const char *set_doc;
+  const char *show_doc;
+  const char *help_doc;
+
+  /* Convenience method that returns THIS as an option_def.  Useful
+     when you're putting an option_def subclass in an option_def
+     array_view.  */
+  const option_def &def () const
+  {
+    return *this;
+  }
+};
+
+namespace detail
+{
+
+/* Get the address of the option's value, cast to the right type.
+   RetType is the restored type of the variable, and Context is the
+   restored type of the context pointer.  */
+template<typename RetType, typename Context>
+static inline RetType *
+get_var_address (const option_def &option, void *ctx)
+{
+  using unerased_ftype = RetType *(Context *);
+  unerased_ftype *fun = (unerased_ftype *) option.erased_var_address;
+  return fun ((Context *) ctx);
+}
+
+/* Convenience identity helper that just returns SELF.  */
+
+template<typename T>
+static T *
+return_self (T *self)
+{
+  return self;
+}
+
+} /* namespace detail */
+
+/* Follows the definitions of the option types that client code should
+   define.  Note that objects of these types are placed in option_def
+   arrays, by design, so they must not have data fields of their
+   own.  */
+
+/* A var_boolean command line option.  */
+
+template<typename Context>
+struct boolean_option_def : option_def
+{
+  boolean_option_def (const char *long_option_,
+		      int *(*var_address_cb_) (Context *),
+		      show_value_ftype *show_cmd_cb_,
+		      const char *set_doc_,
+		      const char *show_doc_ = nullptr,
+		      const char *help_doc_ = nullptr)
+    : option_def (long_option_, var_boolean,
+		  (erased_var_address_ftype *) var_address_cb_,
+		  show_cmd_cb_,
+		  set_doc_, show_doc_, help_doc_)
+  {
+    var_address.boolean = detail::get_var_address<int, Context>;
+  }
+};
+
+/* A flag command line option.  This is a var_boolean option under the
+   hood, but unlike boolean options, flag options don't take an on/off
+   argument.  */
+
+template<typename Context = int>
+struct flag_option_def : boolean_option_def<Context>
+{
+  flag_option_def (const char *long_option_,
+		     int *(*var_address_cb_) (Context *),
+		     const char *set_doc_,
+		     const char *help_doc_ = nullptr)
+    : boolean_option_def<Context> (long_option_,
+				   var_address_cb_,
+				   NULL,
+				   set_doc_, NULL, help_doc_)
+  {
+    this->have_argument = false;
+  }
+
+  flag_option_def (const char *long_option_,
+		     const char *set_doc_,
+		     const char *help_doc_ = nullptr)
+    : boolean_option_def<Context> (long_option_,
+				   gdb::option::detail::return_self,
+				   NULL,
+				   set_doc_, nullptr, help_doc_)
+  {
+    this->have_argument = false;
+  }
+};
+
+/* A var_uinteger command line option.  */
+
+template<typename Context>
+struct uinteger_option_def : option_def
+{
+  uinteger_option_def (const char *long_option_,
+		       unsigned int *(*var_address_cb_) (Context *),
+		       show_value_ftype *show_cmd_cb_,
+		       const char *set_doc_,
+		       const char *show_doc_ = nullptr,
+		       const char *help_doc_ = nullptr)
+    : option_def (long_option_, var_uinteger,
+		  (erased_var_address_ftype *) var_address_cb_,
+		  show_cmd_cb_,
+		  set_doc_, show_doc_, help_doc_)
+  {
+    var_address.uinteger = detail::get_var_address<unsigned int, Context>;
+  }
+};
+
+/* A var_zuinteger_unlimited command line option.  */
+
+template<typename Context>
+struct zuinteger_unlimited_option_def : option_def
+{
+  zuinteger_unlimited_option_def (const char *long_option_,
+				  int *(*var_address_cb_) (Context *),
+				  show_value_ftype *show_cmd_cb_,
+				  const char *set_doc_,
+				  const char *show_doc_ = nullptr,
+				  const char *help_doc_ = nullptr)
+    : option_def (long_option_, var_zuinteger_unlimited,
+		  (erased_var_address_ftype *) var_address_cb_,
+		  show_cmd_cb_,
+		  set_doc_, show_doc_, help_doc_)
+  {
+    var_address.integer = detail::get_var_address<int, Context>;
+  }
+};
+
+/* An var_enum command line option.  */
+
+template<typename Context>
+struct enum_option_def : option_def
+{
+  enum_option_def (const char *long_option_,
+		   const char *const *enumlist,
+		   const char **(*var_address_cb_) (Context *),
+		   show_value_ftype *show_cmd_cb_,
+		   const char *set_doc_,
+		   const char *show_doc_ = nullptr,
+		   const char *help_doc_ = nullptr)
+    : option_def (long_option_, var_enum,
+		  (erased_var_address_ftype *) var_address_cb_,
+		  show_cmd_cb_,
+		  set_doc_, show_doc_, help_doc_)
+  {
+    var_address.enumeration = detail::get_var_address<const char *, Context>;
+    this->enums = enumlist;
+  }
+};
+
+/* A group of options that all share the same context pointer to pass
+   to the options' get-current-value callbacks.  */
+struct option_def_group
+{
+  /* The list of options.  */
+  gdb::array_view<const option_def> options;
+
+  /* The context pointer to pass to the options' get-current-value
+     callbacks.  */
+  void *ctx;
+};
+
+/* Modes of operation for process_options.  */
+enum process_options_mode
+{
+  /* In this mode, options are only processed if we find a "--"
+     delimiter.  Throws an error if unknown options are found.  */
+  PROCESS_OPTIONS_REQUIRE_DELIMITER,
+
+  /* In this mode, a "--" delimiter is not required.  Throws an error
+     if unknown options are found, regardless of whether a delimiter
+     is present.  */
+  PROCESS_OPTIONS_UNKNOWN_IS_ERROR,
+
+  /* In this mode, a "--" delimiter is not required.  If an unknown
+     option is found, assume it is the command's argument/operand.  */
+  PROCESS_OPTIONS_UNKNOWN_IS_OPERAND,
+};
+
+/* Process ARGS, using OPTIONS_GROUP as valid options.  Returns true
+   if the string has been fully parsed and there are no operands to
+   handle by the caller.  Return false if options were parsed, and
+   *ARGS now points at the first operand.  */
+extern bool process_options
+  (const char **args,
+   process_options_mode mode,
+   gdb::array_view<const option_def_group> options_group);
+
+/* Complete ARGS on options listed by OPTIONS_GROUP.  Returns true if
+   the string has been fully parsed and there are no operands to
+   handle by the caller.  Return false if options were parsed, and
+   *ARGS now points at the first operand.  */
+extern bool complete_options
+  (completion_tracker &tracker,
+   const char **args,
+   process_options_mode mode,
+   gdb::array_view<const option_def_group> options_group);
+
+/* Complete on all options listed by OPTIONS_GROUP.  */
+extern void
+  complete_on_all_options (completion_tracker &tracker,
+			   gdb::array_view<const option_def_group> options_group);
+
+/* Return a string with the result of replacing %OPTIONS% in HELP_TMLP
+   with an auto-generated "help" string fragment for all the options
+   in OPTIONS_GROUP.  */
+extern std::string build_help
+  (const char *help_tmpl,
+   gdb::array_view<const option_def_group> options_group);
+
+/* Install set/show commands for options defined in OPTIONS.  DATA is
+   a pointer to the structure that holds the data associated with the
+   OPTIONS array.  */
+extern void add_setshow_cmds_for_options (command_class cmd_class, void *data,
+					  gdb::array_view<const option_def> options,
+					  struct cmd_list_element **set_list,
+					  struct cmd_list_element **show_list);
+
+} /* namespace option */
+} /* namespace gdb */
+
+#endif /* CLI_OPTION_H */
diff --git a/gdb/cli/cli-setshow.c b/gdb/cli/cli-setshow.c
index 76bc2a3d751..5ec380eaa8d 100644
--- a/gdb/cli/cli-setshow.c
+++ b/gdb/cli/cli-setshow.c
@@ -78,33 +78,48 @@ parse_auto_binary_operation (const char *arg)
 /* See cli-setshow.h.  */
 
 int
-parse_cli_boolean_value (const char *arg)
+parse_cli_boolean_value (const char **arg)
 {
-  int length;
-
-  if (!arg || !*arg)
-    return 1;
+  const char *p = skip_to_space (*arg);
+  size_t length = p - *arg;
 
-  length = strlen (arg);
+  /* Note that "o" is ambiguous.  */
 
-  while (arg[length - 1] == ' ' || arg[length - 1] == '\t')
-    length--;
+  if ((length == 2 && strncmp (*arg, "on", length) == 0)
+      || strncmp (*arg, "1", length) == 0
+      || strncmp (*arg, "yes", length) == 0
+      || strncmp (*arg, "enable", length) == 0)
+    {
+      *arg = skip_spaces (*arg + length);
+      return 1;
+    }
+  else if ((length >= 2 && strncmp (*arg, "off", length) == 0)
+	   || strncmp (*arg, "0", length) == 0
+	   || strncmp (*arg, "no", length) == 0
+	   || strncmp (*arg, "disable", length) == 0)
+    {
+      *arg = skip_spaces (*arg + length);
+      return 0;
+    }
+  else
+    return -1;
+}
 
-  /* Note that "o" is ambiguous.  */
+/* See cli-setshow.h.  */
 
-  if ((length == 2 && strncmp (arg, "on", length) == 0)
-      || strncmp (arg, "1", length) == 0
-      || strncmp (arg, "yes", length) == 0
-      || strncmp (arg, "enable", length) == 0)
+int
+parse_cli_boolean_value (const char *arg)
+{
+  if (!arg || !*arg)
     return 1;
-  else if ((length >= 2 && strncmp (arg, "off", length) == 0)
-	   || strncmp (arg, "0", length) == 0
-	   || strncmp (arg, "no", length) == 0
-	   || strncmp (arg, "disable", length) == 0)
-    return 0;
-  else
+
+  int b = parse_cli_boolean_value (&arg);
+  if (b >= 0 && *arg != '\0')
     return -1;
+
+  return b;
 }
+
 
 void
 deprecated_show_value_hack (struct ui_file *ignore_file,
@@ -134,21 +149,136 @@ deprecated_show_value_hack (struct ui_file *ignore_file,
 
 /* Returns true if ARG is "unlimited".  */
 
-static int
-is_unlimited_literal (const char *arg)
+static bool
+is_unlimited_literal (const char **arg)
 {
-  arg = skip_spaces (arg);
+  *arg = skip_spaces (*arg);
 
-  const char *p = skip_to_space (arg);
+  const char *p = skip_to_space (*arg);
 
-  size_t len = p - arg;
+  size_t len = p - *arg;
 
-  if (len > 0 && strncmp ("unlimited", arg, len) == 0)
-    return true;
+  if (len > 0 && strncmp ("unlimited", *arg, len) == 0)
+    {
+      *arg += len;
+      return true;
+    }
 
   return false;
 }
 
+/* See cli-setshow.h.  */
+
+unsigned int
+parse_cli_var_uinteger (var_types var_type, const char **arg,
+			bool expression)
+{
+  LONGEST val;
+
+  if (*arg == nullptr)
+    {
+      if (var_type == var_uinteger)
+	error_no_arg (_("integer to set it to, or \"unlimited\"."));
+      else
+	error_no_arg (_("integer to set it to."));
+    }
+
+  if (var_type == var_uinteger && is_unlimited_literal (arg))
+    val = 0;
+  else if (expression)
+    val = parse_and_eval_long (*arg);
+  else
+    val = get_ulongest (arg);
+
+  if (var_type == var_uinteger && val == 0)
+    val = UINT_MAX;
+  else if (val < 0
+	   /* For var_uinteger, don't let the user set the value
+	      to UINT_MAX directly, as that exposes an
+	      implementation detail to the user interface.  */
+	   || (var_type == var_uinteger && val >= UINT_MAX)
+	   || (var_type == var_zuinteger && val > UINT_MAX))
+    error (_("integer %s out of range"), plongest (val));
+
+  return val;
+}
+
+/* See cli-setshow.h.  */
+
+int
+parse_cli_var_zuinteger_unlimited (const char **arg, bool expression)
+{
+  LONGEST val;
+
+  if (arg == nullptr)
+    error_no_arg (_("integer to set it to, or \"unlimited\"."));
+
+  if (is_unlimited_literal (arg))
+    val = -1;
+  else if (expression)
+    val = parse_and_eval_long (*arg);
+  else
+    val = get_ulongest (arg);
+
+  if (val > INT_MAX)
+    error (_("integer %s out of range"), plongest (val));
+  else if (val < -1)
+    error (_("only -1 is allowed to set as unlimited"));
+
+  return val;
+}
+
+/* See cli-setshow.h.  */
+
+const char *
+parse_cli_var_enum (const char **args, const char *const *enums)
+{
+  /* If no argument was supplied, print an informative error
+     message.  */
+  if (args == NULL || *args == NULL || **args == '\0')
+    {
+      std::string msg;
+
+      for (size_t i = 0; enums[i]; i++)
+	{
+	  if (i != 0)
+	    msg += ", ";
+	  msg += enums[i];
+	}
+      error (_("Requires an argument. Valid arguments are %s."),
+	     msg.c_str ());
+    }
+
+  const char *p = skip_to_space (*args);
+  size_t len = p - *args;
+
+  int nmatches = 0;
+  const char *match = NULL;
+  for (size_t i = 0; enums[i]; i++)
+    if (strncmp (*args, enums[i], len) == 0)
+      {
+	if (enums[i][len] == '\0')
+	  {
+	    match = enums[i];
+	    nmatches = 1;
+	    break; /* Exact match.  */
+	  }
+	else
+	  {
+	    match = enums[i];
+	    nmatches++;
+	  }
+      }
+
+  if (nmatches == 0)
+    error (_("Undefined item: \"%.*s\"."), (int) len, *args);
+
+  if (nmatches > 1)
+    error (_("Ambiguous item \"%.*s\"."), (int) len, *args);
+
+  *args += len;
+  return match;
+}
 
 /* Do a "set" command.  ARG is NULL if no argument, or the
    text of the argument, and FROM_TTY is nonzero if this command is
@@ -295,30 +425,7 @@ do_set_command (const char *arg, int from_tty, struct cmd_list_element *c)
     case var_uinteger:
     case var_zuinteger:
       {
-	LONGEST val;
-
-	if (arg == NULL)
-	  {
-	    if (c->var_type == var_uinteger)
-	      error_no_arg (_("integer to set it to, or \"unlimited\"."));
-	    else
-	      error_no_arg (_("integer to set it to."));
-	  }
-
-	if (c->var_type == var_uinteger && is_unlimited_literal (arg))
-	  val = 0;
-	else
-	  val = parse_and_eval_long (arg);
-
-	if (c->var_type == var_uinteger && val == 0)
-	  val = UINT_MAX;
-	else if (val < 0
-		 /* For var_uinteger, don't let the user set the value
-		    to UINT_MAX directly, as that exposes an
-		    implementation detail to the user interface.  */
-		 || (c->var_type == var_uinteger && val >= UINT_MAX)
-		 || (c->var_type == var_zuinteger && val > UINT_MAX))
-	  error (_("integer %s out of range"), plongest (val));
+	unsigned int val = parse_cli_var_uinteger (c->var_type, &arg, true);
 
 	if (*(unsigned int *) c->var != val)
 	  {
@@ -341,7 +448,7 @@ do_set_command (const char *arg, int from_tty, struct cmd_list_element *c)
 	      error_no_arg (_("integer to set it to."));
 	  }
 
-	if (c->var_type == var_integer && is_unlimited_literal (arg))
+	if (c->var_type == var_integer && is_unlimited_literal (&arg))
 	  val = 0;
 	else
 	  val = parse_and_eval_long (arg);
@@ -366,59 +473,9 @@ do_set_command (const char *arg, int from_tty, struct cmd_list_element *c)
       }
     case var_enum:
       {
-	int i;
-	int len;
-	int nmatches;
-	const char *match = NULL;
-	const char *p;
-
-	/* If no argument was supplied, print an informative error
-	   message.  */
-	if (arg == NULL)
-	  {
-	    std::string msg;
-
-	    for (i = 0; c->enums[i]; i++)
-	      {
-		if (i != 0)
-		  msg += ", ";
-		msg += c->enums[i];
-	      }
-	    error (_("Requires an argument. Valid arguments are %s."), 
-		   msg.c_str ());
-	  }
-
-	p = strchr (arg, ' ');
+	const char *match = parse_cli_var_enum (&arg, c->enums);
 
-	if (p)
-	  len = p - arg;
-	else
-	  len = strlen (arg);
-
-	nmatches = 0;
-	for (i = 0; c->enums[i]; i++)
-	  if (strncmp (arg, c->enums[i], len) == 0)
-	    {
-	      if (c->enums[i][len] == '\0')
-		{
-		  match = c->enums[i];
-		  nmatches = 1;
-		  break; /* Exact match.  */
-		}
-	      else
-		{
-		  match = c->enums[i];
-		  nmatches++;
-		}
-	    }
-
-	if (nmatches <= 0)
-	  error (_("Undefined item: \"%s\"."), arg);
-
-	if (nmatches > 1)
-	  error (_("Ambiguous item \"%s\"."), arg);
-
-	arg = skip_spaces (arg + len);
+	arg = skip_spaces (arg);
 	if (*arg != '\0')
 	  error (_("Garbage after item: \"%s\""), arg);
 
@@ -432,20 +489,7 @@ do_set_command (const char *arg, int from_tty, struct cmd_list_element *c)
       break;
     case var_zuinteger_unlimited:
       {
-	LONGEST val;
-
-	if (arg == NULL)
-	  error_no_arg (_("integer to set it to, or \"unlimited\"."));
-
-	if (is_unlimited_literal (arg))
-	  val = -1;
-	else
-	  val = parse_and_eval_long (arg);
-
-	if (val > INT_MAX)
-	  error (_("integer %s out of range"), plongest (val));
-	else if (val < -1)
-	  error (_("only -1 is allowed to set as unlimited"));
+	int val = parse_cli_var_zuinteger_unlimited (&arg, true);
 
 	if (*(int *) c->var != val)
 	  {
diff --git a/gdb/cli/cli-setshow.h b/gdb/cli/cli-setshow.h
index e4d13c3510d..c00a0989145 100644
--- a/gdb/cli/cli-setshow.h
+++ b/gdb/cli/cli-setshow.h
@@ -23,6 +23,33 @@ struct cmd_list_element;
    Returns 1 for true, 0 for false, and -1 if invalid.  */
 extern int parse_cli_boolean_value (const char *arg);
 
+/* Same as above, but work with a pointer to pointer.  ARG is advanced
+   past a successfully parsed value.  */
+extern int parse_cli_boolean_value (const char **arg);
+
+/* Parse ARG, an option to a var_uinteger or var_zuinteger variable.
+   Either returns the parsed value on success or throws an error.  If
+   EXPRESSION is true, *ARG is parsed as an expression; otherwise, it
+   is parsed with get_ulongest.  It's not possible to parse the
+   integer as an expression when there may be valid input after the
+   integer, such as when parsing command options.  E.g., "print
+   -elements NUMBER -obj --".  In such case, parsing as an expression
+   would parse "-obj --" as part of the expression as well.  */
+extern unsigned int parse_cli_var_uinteger (var_types var_type,
+					    const char **arg,
+					    bool expression);
+
+/* Like parse_cli_var_uinteger, for var_zuinteger_unlimited.  */
+extern int parse_cli_var_zuinteger_unlimited (const char **arg,
+					      bool expression);
+
+/* Parse ARG, an option to a var_enum variable.  ENUM is a
+   null-terminated array of possible values. Either returns the parsed
+   value on success or throws an error.  ARG is advanced past the
+   parsed value.  */
+const char *parse_cli_var_enum (const char **args,
+				const char *const *enums);
+
 extern void do_set_command (const char *arg, int from_tty,
 			    struct cmd_list_element *c);
 extern void do_show_command (const char *arg, int from_tty,
diff --git a/gdb/cli/cli-utils.c b/gdb/cli/cli-utils.c
index 23296cee9c3..306b69e3d8f 100644
--- a/gdb/cli/cli-utils.c
+++ b/gdb/cli/cli-utils.c
@@ -27,6 +27,57 @@ static std::string extract_arg_maybe_quoted (const char **arg);
 
 /* See documentation in cli-utils.h.  */
 
+ULONGEST
+get_ulongest (const char **pp, int trailer)
+{
+  LONGEST retval = 0;	/* default */
+  const char *p = *pp;
+
+  if (*p == '$')
+    {
+      value *val = value_from_history_ref (p, &p);
+
+      if (val != NULL)	/* Value history reference */
+	{
+	  if (TYPE_CODE (value_type (val)) == TYPE_CODE_INT)
+	    retval = value_as_long (val);
+	  else
+	    error (_("History value must have integer type."));
+	}
+      else	/* Convenience variable */
+	{
+	  /* Internal variable.  Make a copy of the name, so we can
+	     null-terminate it to pass to lookup_internalvar().  */
+	  const char *start = ++p;
+	  while (isalnum (*p) || *p == '_')
+	    p++;
+	  std::string varname (start, p - start);
+	  if (!get_internalvar_integer (lookup_internalvar (varname.c_str ()),
+				       &retval))
+	    error (_("Convenience variable $%s does not have integer value."),
+		   varname.c_str ());
+	}
+    }
+  else
+    {
+      retval = strtoulst (p, pp, 0);
+      if (p == *pp)
+	{
+	  /* There is no number here.  (e.g. "cond a == b").  */
+	  error (_("Expected integer at: %s"), p);
+	}
+      p = *pp;
+    }
+
+  if (!(isspace (*p) || *p == '\0' || *p == trailer))
+    error (_("Trailing junk at: %s"), p);
+  p = skip_spaces (p);
+  *pp = p;
+  return retval;
+}
+
+/* See documentation in cli-utils.h.  */
+
 int
 get_number_trailer (const char **pp, int trailer)
 {
diff --git a/gdb/cli/cli-utils.h b/gdb/cli/cli-utils.h
index 9425fb4c09d..41c23561d6f 100644
--- a/gdb/cli/cli-utils.h
+++ b/gdb/cli/cli-utils.h
@@ -39,6 +39,10 @@ extern int get_number (const char **);
 
 extern int get_number (char **);
 
+/* Like get_number_trailer, but works with ULONGEST, and throws on
+   error instead of returning 0.  */
+extern ULONGEST get_ulongest (const char **pp, int trailer = '\0');
+
 /* Extract from ARGS the arguments [-q] [-t TYPEREGEXP] [--] NAMEREGEXP.
 
    The caller is responsible to initialize *QUIET to false, *REGEXP
@@ -193,6 +197,14 @@ extern std::string extract_arg (const char **arg);
    argument.  */
 extern int check_for_argument (const char **str, const char *arg, int arg_len);
 
+/* Same as above, but ARG's length is computed.  */
+
+static inline int
+check_for_argument (const char **str, const char *arg)
+{
+  return check_for_argument (str, arg, strlen (arg));
+}
+
 /* Same, for non-const STR.  */
 
 static inline int
@@ -202,6 +214,12 @@ check_for_argument (char **str, const char *arg, int arg_len)
 			     arg, arg_len);
 }
 
+static inline int
+check_for_argument (char **str, const char *arg)
+{
+  return check_for_argument (str, arg, strlen (arg));
+}
+
 /* A helper function that looks for a set of flags at the start of a
    string.  The possible flags are given as a null terminated string.
    A flag in STR must either be at the end of the string,
diff --git a/gdb/maint-test-options.c b/gdb/maint-test-options.c
new file mode 100644
index 00000000000..e375522bcc7
--- /dev/null
+++ b/gdb/maint-test-options.c
@@ -0,0 +1,460 @@
+/* Maintenance commands for testing the options framework.
+
+   Copyright (C) 2019 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   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 "defs.h"
+#include "gdbcmd.h"
+#include "cli/cli-option.h"
+
+/* This file defines a couple "maintenance test-options" subcommands
+   to exercise TAB-completion and option processing:
+
+    (gdb) maint test-options require-delimiter
+    (gdb) maint test-options unknown-is-error
+    (gdb) maint test-options unknown-is-operand
+
+   And a third one to help with TAB-completion testing.
+
+    (gdb) maint show test-options-completion-result
+
+   Each of the test-options subcommands exercise
+   gdb::option::process_options with a different enum
+   process_options_mode value.  Examples for command they model:
+
+   - "print" and "compile print", are like "require-delimiter",
+      because they accept random expressions as argument.
+
+   - "backtrace" and "frame/thread apply" are like
+     "unknown-is-operand", because "-" is a valid command.
+
+   - "compile file" and "compile code" are like "unknown-is-error".
+
+   These commands allow exercising all aspects of option processing
+   without having to pick some existing command.  That should be more
+   stable going forward than relying on an existing user command,
+   since if we picked say "print", that command or its options could
+   change in future, and then we'd be left with having to pick some
+   other command or option to exercise some non-command-specific
+   option processing detail.  Also, actual user commands have side
+   effects that we're not interested in when we're focusing on unit
+   testing the options machinery.  BTW, a maintenance command is used
+   as a sort of unit test driver instead of actual "maint selftest"
+   unit tests, since we need to go all the way via via including
+   readline, for proper testing of TAB completion.
+
+   These maintenance commands support options of all the different
+   available kinds of commands (boolean, enum, flag, uinteger):
+
+    (gdb) maint test-options require-delimiter -[TAB]
+    -bool      -enum      -flag      -uinteger   -xx1       -xx2
+
+    (gdb) maint test-options require-delimiter -bool o[TAB]
+    off  on
+    (gdb) maint test-options require-delimiter -enum [TAB]
+    xxx  yyy  zzz
+    (gdb) maint test-options require-delimiter -uinteger [TAB]
+    NUMBER     unlimited
+
+   '-xx1' and '-xx2' are flag options too.  They exist in order to
+   test ambiguous option names, like '-xx'.
+
+  Invoking the commands makes them print out the options parsed:
+
+   (gdb) maint test-options unknown-is-error -flag -enum yyy cmdarg
+   -flag 1 -xx1 0 -xx2 0 -bool 0 -enum yyy -uint 0 -zuint-unl 0 -- cmdarg
+
+   (gdb) maint test-options require-delimiter -flag -enum yyy cmdarg
+   -flag 0 -xx1 0 -xx2 0 -bool 0 -enum xxx -uint 0  -zuint-unl 0 -- -flag -enum yyy cmdarg
+   (gdb) maint test-options require-delimiter -flag -enum yyy cmdarg --
+   Unrecognized option at: cmdarg --
+   (gdb) maint test-options require-delimiter -flag -enum yyy -- cmdarg
+   -flag 1 -xx1 0 -xx2 0 -bool 0 -enum yyy -uint 0 -zuint-unl 0 -- cmdarg
+
+  The "maint show test-options-completion-result" command exists in
+  order to do something similar for completion:
+
+   (gdb) maint test-options unknown-is-error -flag -b 0 -enum yyy OPERAND[TAB]
+   (gdb) maint show test-options-completion-result
+   0 OPERAND
+
+   (gdb) maint test-options unknown-is-error -flag -b 0 -enum yyy[TAB]
+   (gdb) maint show test-options-completion-result
+   1
+
+   (gdb) maint test-options require-dash -unknown[TAB]
+   (gdb) maint show test-options-completion-result
+   1
+
+  Here, "1" means the completion function processed the whole input
+  line, and that the command shouldn't do anything with the arguments,
+  since there are no operands.  While "0" indicates that there are
+  operands after options.  The text after "0" is the operands.
+
+  This level of detail is particularly important because getting the
+  completion function's entry point to return back to the caller the
+  right pointer into the operand is quite tricky in several scenarios.  */
+
+/* Enum values for the "maintenance test-options" commands.  */
+const char test_options_enum_values_xxx[] = "xxx";
+const char test_options_enum_values_yyy[] = "yyy";
+const char test_options_enum_values_zzz[] = "zzz";
+static const char *const test_options_enum_values_choices[] =
+{
+  test_options_enum_values_xxx,
+  test_options_enum_values_yyy,
+  test_options_enum_values_zzz,
+  NULL
+};
+
+/* Option data for the "maintenance test-options" commands.  */
+
+struct test_options_opts
+{
+  int flag_opt = 0;
+  int xx1_opt = 0;
+  int xx2_opt = 0;
+  int boolean_opt = 0;
+  const char *enum_opt = test_options_enum_values_xxx;
+  unsigned int uint_opt = 0;
+  int zuint_unl_opt = 0;
+};
+
+/* Option definitions for the "maintenance test-options" commands.  */
+
+static const gdb::option::option_def test_options_option_defs[] = {
+
+  /* A flag option.  */
+  gdb::option::flag_option_def<test_options_opts> {
+    "flag",
+    [] (test_options_opts *opts) { return &opts->flag_opt; },
+    N_("A flag option."),
+  },
+
+  /* A couple flags with similar names, for "ambiguous option names"
+     testing.  */
+  gdb::option::flag_option_def<test_options_opts> {
+    "xx1",
+    [] (test_options_opts *opts) { return &opts->xx1_opt; },
+    N_("A flag option."),
+  },
+  gdb::option::flag_option_def<test_options_opts> {
+    "xx2",
+    [] (test_options_opts *opts) { return &opts->xx2_opt; },
+    N_("A flag option."),
+  },
+
+  /* A boolean option.  */
+  gdb::option::boolean_option_def<test_options_opts> {
+    "bool",
+    [] (test_options_opts *opts) { return &opts->boolean_opt; },
+    nullptr, /* show_cmd_cb */
+    N_("A boolean option."),
+  },
+
+  /* An enum option.  */
+  gdb::option::enum_option_def<test_options_opts> {
+    "enum",
+    test_options_enum_values_choices,
+    [] (test_options_opts *opts) { return &opts->enum_opt; },
+    nullptr, /* show_cmd_cb */
+    N_("An enum option."),
+  },
+
+  /* A uinteger option.  */
+  gdb::option::uinteger_option_def<test_options_opts> {
+    "uinteger",
+    [] (test_options_opts *opts) { return &opts->uint_opt; },
+    nullptr, /* show_cmd_cb */
+    N_("A uinteger option."),
+    nullptr, /* show_doc */
+    N_("A help doc that spawns\nmultiple lines."),
+  },
+
+  /* A zuinteger_unlimited option.  */
+  gdb::option::zuinteger_unlimited_option_def<test_options_opts> {
+    "zuinteger-unlimited",
+    [] (test_options_opts *opts) { return &opts->zuint_unl_opt; },
+    nullptr, /* show_cmd_cb */
+    N_("A zuinteger-unlimited option."),
+    nullptr, /* show_doc */
+    nullptr, /* help_doc */
+  },
+};
+
+/* Create an option_def_group for the test_options_opts options, with
+   OPTS as context.  */
+
+static inline gdb::option::option_def_group
+make_test_options_options_def_group (test_options_opts *opts)
+{
+  return {{test_options_option_defs}, opts};
+}
+
+/* Implementation of the "maintenance test-options
+   require-delimiter/unknown-is-error/unknown-is-operand" commands.
+   Each of the commands maps to a different enum process_options_mode
+   enumerator.  The test strategy is simply processing the options in
+   a number of scenarios, and printing back the parsed result.  */
+
+static void
+maintenance_test_options_command_mode (const char *args,
+				       gdb::option::process_options_mode mode)
+{
+  test_options_opts opts;
+
+  gdb::option::process_options (&args, mode,
+				make_test_options_options_def_group (&opts));
+
+  if (args == nullptr)
+    args = "";
+  else
+    args = skip_spaces (args);
+
+  printf_unfiltered (_("-flag %d -xx1 %d -xx2 %d -bool %d "
+		       "-enum %s -uint %s -zuint-unl %s -- %s\n"),
+		     opts.flag_opt,
+		     opts.xx1_opt,
+		     opts.xx2_opt,
+		     opts.boolean_opt,
+		     opts.enum_opt,
+		     (opts.uint_opt == UINT_MAX
+		      ? "unlimited"
+		      : pulongest (opts.uint_opt)),
+		     (opts.zuint_unl_opt == -1
+		      ? "unlimited"
+		      : plongest (opts.zuint_unl_opt)),
+		     args);
+}
+
+/* Variables used by the "maintenance show
+   test-options-completion-result" command.  These variables are
+   stored by the completer of the "maint test-options"
+   subcommands.  */
+
+/* The result of gdb::option::complete_options.  */
+static int maintenance_test_options_command_completion_result;
+/* The text at the word point after gdb::option::complete_options
+   returns.  */
+static std::string maintenance_test_options_command_completion_text;
+
+/* The "maintenance show test-options-completion-result" command.  */
+
+static void
+maintenance_show_test_options_completion_result
+  (struct ui_file *file, int from_tty,
+   struct cmd_list_element *c, const char *value)
+{
+  if (maintenance_test_options_command_completion_result)
+    fprintf_filtered (file, "1\n");
+  else
+    fprintf_filtered
+      (file, _("0 %s\n"),
+       maintenance_test_options_command_completion_text.c_str ());
+}
+
+/* Implementation of completer for the "maintenance test-options
+   require-delimiter/unknown-is-error/unknown-is-operand" commands.
+   Each of the commands maps to a different enum process_options_mode
+   enumerator.  */
+
+static void
+maintenance_test_options_completer_mode (completion_tracker &tracker,
+					 const char *text,
+					 gdb::option::process_options_mode mode)
+{
+  try
+    {
+      maintenance_test_options_command_completion_result
+	= gdb::option::complete_options
+	   (tracker, &text, mode,
+	    make_test_options_options_def_group (nullptr));
+      maintenance_test_options_command_completion_text = text;
+    }
+  catch (const gdb_exception_error &ex)
+    {
+      maintenance_test_options_command_completion_result = 1;
+      throw;
+    }
+}
+
+/* Implementation of the "maintenance test-options require-delimiter"
+   command.  */
+
+static void
+maintenance_test_options_require_delimiter_command (const char *args,
+						    int from_tty)
+{
+  maintenance_test_options_command_mode
+    (args, gdb::option::PROCESS_OPTIONS_REQUIRE_DELIMITER);
+}
+
+/* Implementation of the "maintenance test-options
+   unknown-is-error" command.  */
+
+static void
+maintenance_test_options_unknown_is_error_command (const char *args,
+						   int from_tty)
+{
+  maintenance_test_options_command_mode
+    (args, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_ERROR);
+}
+
+/* Implementation of the "maintenance test-options
+   unknown-is-operand" command.  */
+
+static void
+maintenance_test_options_unknown_is_operand_command (const char *args,
+						     int from_tty)
+{
+  maintenance_test_options_command_mode
+    (args, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_OPERAND);
+}
+
+/* Completer for the "maintenance test-options require-delimiter"
+   command.  */
+
+static void
+maintenance_test_options_require_delimiter_command_completer
+  (cmd_list_element *ignore, completion_tracker &tracker,
+   const char *text, const char *word)
+{
+  maintenance_test_options_completer_mode
+    (tracker, text, gdb::option::PROCESS_OPTIONS_REQUIRE_DELIMITER);
+}
+
+/* Completer for the "maintenance test-options unknown-is-error"
+   command.  */
+
+static void
+maintenance_test_options_unknown_is_error_command_completer
+  (cmd_list_element *ignore, completion_tracker &tracker,
+   const char *text, const char *word)
+{
+  maintenance_test_options_completer_mode
+    (tracker, text, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_ERROR);
+}
+
+/* Completer for the "maintenance test-options unknown-is-operand"
+   command.  */
+
+static void
+maintenance_test_options_unknown_is_operand_command_completer
+  (cmd_list_element *ignore, completion_tracker &tracker,
+   const char *text, const char *word)
+{
+  maintenance_test_options_completer_mode
+    (tracker, text, gdb::option::PROCESS_OPTIONS_UNKNOWN_IS_OPERAND);
+}
+
+/* Command list for maint test-options.  */
+struct cmd_list_element *maintenance_test_options_list;
+
+/* The "maintenance test-options" prefix command.  */
+
+static void
+maintenance_test_options_command (const char *arg, int from_tty)
+{
+  printf_unfiltered
+    (_("\"maintenance test-options\" must be followed "
+       "by the name of a subcommand.\n"));
+  help_list (maintenance_test_options_list, "maintenance test-options ",
+	     all_commands, gdb_stdout);
+}
+
+
+void
+_initialize_maint_test_options ()
+{
+  cmd_list_element *cmd;
+
+  add_prefix_cmd ("test-options", no_class, maintenance_test_options_command,
+		  _("\
+Generic command for testing the options infrastructure."),
+		  &maintenance_test_options_list,
+		  "maintenance test-options ", 0,
+		  &maintenancelist);
+
+  const auto def_group = make_test_options_options_def_group (nullptr);
+
+  static const std::string help_require_delim_str
+    = gdb::option::build_help (_("\
+Command used for testing options processing.\n\
+Usage: maint test-options require-delimiter [[OPTION]... --] [OPERAND]...\n\
+\n\
+Options:\n\
+\n\
+%OPTIONS%\n\
+If you specify any command option, you must use a double dash (\"--\")\n\
+to mark the end of option processing."),
+			       def_group);
+
+  static const std::string help_unknown_is_error_str
+    = gdb::option::build_help (_("\
+Command used for testing options processing.\n\
+Usage: maint test-options unknown-is-error [OPTION]... [OPERAND]...\n\
+\n\
+Options:\n\
+\n\
+%OPTIONS%"),
+			       def_group);
+
+  static const std::string help_unknown_is_operand_str
+    = gdb::option::build_help (_("\
+Command used for testing options processing.\n\
+Usage: maint test-options unknown-is-operand [OPTION]... [OPERAND]...\n\
+\n\
+Options:\n\
+\n\
+%OPTIONS%"),
+			       def_group);
+
+  cmd = add_cmd ("require-delimiter", class_maintenance,
+		 maintenance_test_options_require_delimiter_command,
+		 help_require_delim_str.c_str (),
+		 &maintenance_test_options_list);
+  set_cmd_completer_handle_brkchars
+    (cmd, maintenance_test_options_require_delimiter_command_completer);
+
+  cmd = add_cmd ("unknown-is-error", class_maintenance,
+		 maintenance_test_options_unknown_is_error_command,
+		 help_unknown_is_error_str.c_str (),
+		 &maintenance_test_options_list);
+  set_cmd_completer_handle_brkchars
+    (cmd, maintenance_test_options_unknown_is_error_command_completer);
+
+  cmd = add_cmd ("unknown-is-operand", class_maintenance,
+		 maintenance_test_options_unknown_is_operand_command,
+		 help_unknown_is_operand_str.c_str (),
+		 &maintenance_test_options_list);
+  set_cmd_completer_handle_brkchars
+    (cmd, maintenance_test_options_unknown_is_operand_command_completer);
+
+  add_setshow_zinteger_cmd ("test-options-completion-result", class_maintenance,
+			    &maintenance_test_options_command_completion_result,
+			    _("\
+Set maintenance test-options completion result."), _("\
+Show maintenance test-options completion result."), _("\
+Show the results of completing\n\
+\"maint test-options require-delimiter\",\n\
+\"maint test-options unknown-is-error\", or\n\
+\"maint test-options unknown-is-operand\"."),
+			    NULL,
+			    maintenance_show_test_options_completion_result,
+			    &maintenance_set_cmdlist,
+			    &maintenance_show_cmdlist);
+}
diff --git a/gdb/testsuite/gdb.base/options.c b/gdb/testsuite/gdb.base/options.c
new file mode 100644
index 00000000000..fd19922b54e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/options.c
@@ -0,0 +1,33 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2019 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/>.  */
+
+int xxx1= 123;
+
+struct S
+{
+  int a;
+  int b;
+  int c;
+};
+
+struct S g_s = {1, 2, 3};
+
+int
+main ()
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/options.exp b/gdb/testsuite/gdb.base/options.exp
new file mode 100644
index 00000000000..924d7aa544e
--- /dev/null
+++ b/gdb/testsuite/gdb.base/options.exp
@@ -0,0 +1,554 @@
+# This testcase is part of GDB, the GNU debugger.
+
+# Copyright 2019 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/>.
+
+# Test the gdb::option framework.
+
+# The test uses the "maintenance test-options" subcommands to exercise
+# TAB-completion and option processing.
+
+load_lib completion-support.exp
+
+clean_restart
+
+if { ![readline_is_used] } {
+    untested "no tab completion support without readline"
+    return -1
+}
+
+# Check the completion result, as returned by the "maintenance show
+# test-options-completion-result" command.  TEST is used as test name.
+proc check_completion_result {expected test} {
+    gdb_test "maintenance show test-options-completion-result" \
+	"$expected" \
+	"$test: res=$expected"
+}
+
+# Like test_gdb_complete_unique, but the expected output is expected
+# to be the input line.  I.e., the line is already complete.  We're
+# just checking whether GDB recognizes the option and auto-appends a
+# space.
+proc test_completer_recognizes {res input_line} {
+    set expected_re [string_to_regexp $input_line]
+    test_gdb_complete_unique $input_line $expected_re
+    check_completion_result $res $input_line
+}
+
+# Wrapper around test_gdb_complete_multiple that also checks the
+# completion result is RES.
+proc res_test_gdb_complete_multiple {res cmd_prefix completion_word args} {
+    test_gdb_complete_multiple $cmd_prefix $completion_word {*}$args
+    check_completion_result $res "$cmd_prefix$completion_word"
+}
+
+# Wrapper around test_gdb_complete_none that also checks the
+# completion result is RES.
+proc res_test_gdb_complete_none { res input_line } {
+    test_gdb_complete_none $input_line
+    check_completion_result $res "$input_line"
+}
+
+# Wrapper around test_gdb_complete_unique that also checks the
+# completion result is RES.
+proc res_test_gdb_complete_unique { res input_line args} {
+    test_gdb_complete_unique $input_line {*}$args
+    check_completion_result $res "$input_line"
+}
+
+# Make a full command name from VARIANT.  VARIANT is either
+# "require-delimiter", "unknown-is-error" or "unknown-is-operand".
+proc make_cmd {variant} {
+    return "maint test-options $variant"
+}
+
+# Return a string for the expected result of running "maint
+# test-options xxx", with no flag/option set.  OPERAND is the expected
+# operand.
+proc expect_none {operand} {
+    return "-flag 0 -xx1 0 -xx2 0 -bool 0 -enum xxx -uint 0 -zuint-unl 0 -- $operand"
+}
+
+# Return a string for the expected result of running "maint
+# test-options xxx", with -flag set.  OPERAND is the expected operand.
+proc expect_flag {operand} {
+    return "-flag 1 -xx1 0 -xx2 0 -bool 0 -enum xxx -uint 0 -zuint-unl 0 -- $operand"
+}
+
+# Return a string for the expected result of running "maint
+# test-options xxx", with -bool set.  OPERAND is the expected operand.
+proc expect_bool {operand} {
+    return "-flag 0 -xx1 0 -xx2 0 -bool 1 -enum xxx -uint 0 -zuint-unl 0 -- $operand"
+}
+
+# Return a string for the expected result of running "maint
+# test-options xxx", with one of the integer options set to $VAL.
+# OPTION determines which option to expect set.  OPERAND is the
+# expected operand.
+proc expect_integer {option val operand} {
+    if {$option == "uinteger"} {
+	return "-flag 0 -xx1 0 -xx2 0 -bool 0 -enum xxx -uint $val -zuint-unl 0 -- $operand"
+    } elseif {$option == "zuinteger-unlimited"} {
+	return "-flag 0 -xx1 0 -xx2 0 -bool 0 -enum xxx -uint 0 -zuint-unl $val -- $operand"
+    } else {
+	error "unsupported option: $option"
+    }
+}
+
+set all_options {
+    "-bool"
+    "-enum"
+    "-flag"
+    "-uinteger"
+    "-xx1"
+    "-xx2"
+    "-zuinteger-unlimited"
+}
+
+# Miscelaneous tests.
+proc_with_prefix test-misc {variant} {
+    global all_options
+
+    set cmd [make_cmd $variant]
+
+    # Call test command with no arguments at all.
+    gdb_test "$cmd" [expect_none ""]
+
+    # Now with a single dash.
+    if {$variant == "require-delimiter"} {
+	gdb_test "$cmd -" [expect_none "-"]
+    } else {
+	gdb_test "$cmd -" "Ambiguous option at: -"
+    }
+
+    # Completing at "-" should list all options.
+    res_test_gdb_complete_multiple "1" "$cmd " "-" "" $all_options
+
+    # Now with a double dash.
+    gdb_test "$cmd --" [expect_none ""]
+
+    # "--" is recognized by options completer, gdb auto-appends a
+    # space.
+    test_completer_recognizes 1 "$cmd --"
+
+    # Now with a double dash, plus a dash as operand.
+    gdb_test "$cmd -- -" [expect_none "-"]
+    res_test_gdb_complete_none "0 -" "$cmd -- -"
+
+    # Completing an unambiguous option just appends an empty space.
+    test_completer_recognizes 1 "$cmd -flag"
+
+    # Try running an ambiguous option.
+    if {$variant == "require-delimiter"} {
+	gdb_test "$cmd -xx" [expect_none "-xx"]
+    } else {
+	gdb_test "$cmd -xx" "Ambiguous option at: -xx"
+    }
+
+    # Check that options are not case insensitive.
+    gdb_test "$cmd -flag --" [expect_flag ""]
+
+    # Check how the different modes behave on unknown option, with a
+    # delimiter.
+    gdb_test "$cmd -FLAG --" \
+	"Unrecognized option at: -FLAG --"
+
+    # Check how the different modes behave on unknown option, without
+    # a delimiter.
+    if {$variant == "unknown-is-error"} {
+	gdb_test "$cmd -FLAG" \
+	    "Unrecognized option at: -FLAG"
+    } else {
+	gdb_test "$cmd -FLAG" [expect_none "-FLAG"]
+    }
+
+    # Test parsing stops at a negative integer.
+    gdb_test "$cmd -1 --" \
+	"Unrecognized option at: -1 --"
+    gdb_test "$cmd -2 --" \
+	"Unrecognized option at: -2 --"
+}
+
+# Flag option tests.
+proc_with_prefix test-flag {variant} {
+    global all_options
+
+    set cmd [make_cmd $variant]
+
+    # Completing a flag just appends a space.
+    test_completer_recognizes 1 "$cmd -flag"
+
+    # Add a dash, and all options should be shown.
+    test_gdb_complete_multiple "$cmd  -flag " "-" "" $all_options
+
+    # Basic smoke tests of accepted / not accepted values.
+
+    # Check all the different variants a bool option may be specified.
+    if {$variant == "require-delimiter"} {
+	gdb_test "$cmd -flag 999" [expect_none "-flag 999"]
+    } else {
+	gdb_test "$cmd -flag 999" [expect_flag "999"]
+    }
+    gdb_test "$cmd -flag -- 999" [expect_flag "999"]
+
+    # If the "--" separator is present, then GDB errors out if the
+    # flag option is passed some value -- check that too.
+    gdb_test "$cmd -flag xxx 999 --" "Unrecognized option at: xxx 999 --"
+    gdb_test "$cmd -flag o 999 --" "Unrecognized option at: o 999 --"
+    gdb_test "$cmd -flag 1 999 --" "Unrecognized option at: 1 999 --"
+
+    # Extract twice the same flag, separated by one space.
+    gdb_test "$cmd -flag -flag -- non flags args" \
+	[expect_flag "non flags args"]
+
+    # Extract twice the same flag, separated by one space.
+    gdb_test "$cmd -xx1     -xx2 -xx1  -xx2 -xx1    -- non flags args" \
+	"-flag 0 -xx1 1 -xx2 1 -bool 0 -enum xxx -uint 0 -zuint-unl 0 -- non flags args"
+
+    # Extract 2 known flags in front of unknown flags.
+    gdb_test "$cmd -xx1 -xx2 -a -b -c -xx1 --" \
+	"Unrecognized option at: -a -b -c -xx1 --"
+
+    # Check that combined flags are not recognised.
+    gdb_test "$cmd -xx1 -xx1xx2 -xx1 --" \
+	"Unrecognized option at: -xx1xx2 -xx1 --"
+
+    # Make sure the completer don't confuse a flag option with a
+    # boolean option.  Specifically, "o" should not complete to
+    # "on/off".
+
+    if {$variant == "require-delimiter"} {
+	res_test_gdb_complete_none "1" "$cmd -flag o"
+
+	gdb_test "$cmd -flag o" [expect_none "-flag o"]
+    } else {
+	res_test_gdb_complete_none "0 o" "$cmd -flag o"
+
+	gdb_test "$cmd -flag o" [expect_flag "o"]
+    }
+}
+
+# Boolean option tests.
+proc_with_prefix test-boolean {variant} {
+    global all_options
+
+    set cmd [make_cmd $variant]
+
+    # Boolean option's values are optional -- "on" is implied.  Check
+    # that:
+    #
+    # - For require-delimiter commands, completing after a boolean
+    #   option lists all other options, plus "on/off".  This is
+    #   because operands won't be processed until we see a "--"
+    #   delimiter.
+    #
+    # - For !require-delimiter commands, completing after a boolean
+    #   option completes as an operand, since that will tend to be
+    #   more common than typing "on/off".
+    #   E.g., "frame apply all -past-main COMMAND".
+
+    if {$variant == "require-delimiter"} {
+	res_test_gdb_complete_multiple 1 "$cmd -bool " "" "" {
+	    "-bool"
+	    "-enum"
+	    "-flag"
+	    "-uinteger"
+	    "-xx1"
+	    "-xx2"
+	    "-zuinteger-unlimited"
+	    "off"
+	    "on"
+	}
+    } else {
+	res_test_gdb_complete_none "0 " "$cmd -bool "
+    }
+
+    # Add another dash, and "on/off" are no longer offered:
+    res_test_gdb_complete_multiple 1 "$cmd -bool " "-" ""  $all_options
+
+    # Basic smoke tests of accepted / not accepted values.
+
+    # The command accepts all of "1/0/enable/disable/yes/no" too, even
+    # though like the "set" command, we don't offer those as
+    # completion candidates if you complete right after the boolean
+    # command's name, like:
+    #
+    #  (gdb) maint test-options require-delimiter -bool [TAB]
+    #  off        on
+    #
+    # However, the completer does recognize them if you start typing
+    # the boolean value.
+    foreach value {"0" "1"} {
+	test_completer_recognizes 1 "$cmd -bool $value"
+    }
+    foreach value {"of" "off"} {
+	res_test_gdb_complete_unique 1 \
+	    "$cmd -bool $value" \
+	    "$cmd -bool off"
+    }
+    foreach value {"y" "ye" "yes"} {
+	res_test_gdb_complete_unique 1 \
+	    "$cmd -bool $value" \
+	    "$cmd -bool yes"
+    }
+    foreach value {"n" "no"} {
+	res_test_gdb_complete_unique 1 \
+	    "$cmd -bool $value" \
+	    "$cmd -bool no"
+    }
+    foreach value {
+	"e"
+	"en"
+	"ena"
+	"enab"
+	"enabl"
+	"enable"
+    } {
+	res_test_gdb_complete_unique 1 \
+	    "$cmd -bool $value" \
+	    "$cmd -bool enable"
+    }
+    foreach value {
+	"d"
+	"di"
+	"dis"
+	"disa"
+	"disab"
+	"disabl"
+	"disable"
+    } {
+	res_test_gdb_complete_unique 1 \
+	    "$cmd -bool $value" \
+	    "$cmd -bool disable"
+    }
+
+    if {$variant == "require-delimiter"} {
+	res_test_gdb_complete_none "1" "$cmd -bool xxx"
+    } else {
+	res_test_gdb_complete_none "0 xxx" "$cmd -bool xxx"
+    }
+
+    # The command accepts abbreviations of "enable/disable/yes/no",
+    # even though we don't offer those for completion.
+    foreach value {
+	"1"
+	"y" "ye" "yes"
+	"e"
+	"en"
+	"ena"
+	"enab"
+	"enabl"
+	"enable"} {
+	gdb_test "$cmd -bool $value --" [expect_bool ""]
+    }
+    foreach value {
+	"0"
+	"of" "off"
+	"n" "no"
+	"d"
+	"di"
+	"dis"
+	"disa"
+	"disab"
+	"disabl"
+	"disable"} {
+	gdb_test "$cmd -bool $value --" [expect_none ""]
+    }
+
+    if {$variant == "require-delimiter"} {
+	gdb_test "$cmd -bool 999" [expect_none "-bool 999"]
+    } else {
+	gdb_test "$cmd -bool 999" [expect_bool "999"]
+    }
+    gdb_test "$cmd -bool -- 999" [expect_bool "999"]
+
+    # Since "on" is implied after a boolean option, for
+    # !require-delimiter commands, anything that is not
+    # yes/no/1/0/on/off/enable/disable should be considered as the raw
+    # input after the last option.  Also check "o", which might look
+    # like "on" or "off", but it's treated the same.
+
+    foreach arg {"xxx" "o"} {
+	if {$variant == "require-delimiter"} {
+	    gdb_test "$cmd -bool $arg" [expect_none "-bool $arg"]
+	} else {
+	    gdb_test "$cmd -bool $arg" [expect_bool "$arg"]
+	}
+    }
+    # Also try -1.  "unknown-is-error" commands error out saying that
+    # that's not a valid option.
+    if {$variant == "require-delimiter"} {
+	gdb_test "$cmd -bool -1" \
+	     [expect_none "-bool -1"]
+    } elseif {$variant == "unknown-is-error"} {
+	gdb_test "$cmd -bool -1" \
+	    "Unrecognized option at: -1"
+    } else {
+	gdb_test "$cmd -bool -1" [expect_bool "-1"]
+    }
+
+    # OTOH, if the "--" separator is present, then GDB errors out if
+    # the boolean option is passed an invalid value -- check that too.
+    gdb_test "$cmd -bool -1 999 --" \
+	"Unrecognized option at: -1 999 --"
+    gdb_test "$cmd -bool xxx 999 --" \
+	"Value given for `-bool' is not a boolean: xxx"
+    gdb_test "$cmd -bool o 999 --" \
+	"Value given for `-bool' is not a boolean: o"
+
+    # Completing after a boolean option + "o" does list "on/off",
+    # though.
+    if {$variant == "require-delimiter"} {
+	res_test_gdb_complete_multiple 1 "$cmd -bool " "o" "" {
+	    "off"
+	    "on"
+	}
+    } else {
+	res_test_gdb_complete_multiple "0 o" "$cmd -bool " "o" "" {
+	    "off"
+	    "on"
+	}
+    }
+}
+
+# Uinteger option tests.  OPTION is which integer option we're
+# testing.  Can be "uinteger" or "zuinteger-unlimited".
+proc_with_prefix test-uinteger {variant option} {
+    global all_options
+
+    set cmd "[make_cmd $variant] -$option"
+
+    # Test completing a uinteger option:
+    res_test_gdb_complete_multiple 1 "$cmd " "" "" {
+	"NUMBER"
+	"unlimited"
+    }
+
+    # NUMBER above is just a placeholder, make sure we don't complete
+    # it as a valid option.
+    res_test_gdb_complete_none 1 "$cmd NU"
+
+    # "unlimited" is valid though.
+    res_test_gdb_complete_unique 1 \
+	"$cmd u" \
+	"$cmd unlimited"
+
+    # Basic smoke test of accepted / not accepted values.
+    gdb_test "$cmd 1 -- 999" [expect_integer $option "1" "999"]
+    gdb_test "$cmd unlimited -- 999" \
+	[expect_integer $option "unlimited" "999"]
+    if {$option == "zuinteger-unlimited"} {
+	gdb_test "$cmd -1 --" [expect_integer $option "unlimited" ""]
+	gdb_test "$cmd 0 --" [expect_integer $option "0" ""]
+    } else {
+	gdb_test "$cmd -1 --" "integer -1 out of range"
+	gdb_test "$cmd 0 --" [expect_integer $option "unlimited" ""]
+    }
+    gdb_test "$cmd xxx --" \
+	"Expected integer at: xxx --"
+    gdb_test "$cmd unlimitedx --" \
+	"Expected integer at: unlimitedx --"
+
+    # Don't offer completions until we're past the
+    # -uinteger/-zuinteger-unlimited argument.
+    res_test_gdb_complete_none 1 "$cmd 1"
+
+    # A number of invalid values.
+    foreach value {"x" "x " "1a" "1a " "1-" "1- " "unlimitedx"} {
+	res_test_gdb_complete_none 1 "$cmd $value"
+    }
+
+    # Try "-1".
+    if {$option == "uinteger"} {
+	# -1 is invalid uinteger.
+	foreach value {"-1" "-1 "} {
+	    res_test_gdb_complete_none 1 "$cmd $value"
+	}
+    } else {
+	# -1 is valid for zuinteger-unlimited.
+	res_test_gdb_complete_none 1 "$cmd -1"
+	if {$variant == "require-delimiter"} {
+	    res_test_gdb_complete_multiple 1 "$cmd -1 " "" "-" $all_options
+	} else {
+	    res_test_gdb_complete_none "0 " "$cmd -1 "
+	}
+    }
+
+    # Check that after a fully parsed option:
+    #
+    #  - for require-delimiter commands, completion offers all
+    #    options.
+    #
+    #  - for !require-delimiter commands, completion offers nothing
+    #    and returns false.
+    if {$variant == "require-delimiter"} {
+	res_test_gdb_complete_multiple 1 "$cmd 1 " "" "-" $all_options
+    } else {
+	res_test_gdb_complete_none "0 " "$cmd 1 "
+    }
+
+    # Test completing non-option arguments after "-uinteger 1 ".
+    foreach operand {"x" "x " "1a" "1a " "1-" "1- "} {
+	if {$variant == "require-delimiter"} {
+	    res_test_gdb_complete_none 1 "$cmd 1 $operand"
+	} else {
+	    res_test_gdb_complete_none "0 $operand" "$cmd 1 $operand"
+	}
+    }
+    # These look like options, but they aren't.
+    foreach operand {"-1" "-1 "} {
+	if {$variant == "unknown-is-operand"} {
+	    res_test_gdb_complete_none "0 $operand" "$cmd 1 $operand"
+	} else {
+	    res_test_gdb_complete_none 1 "$cmd 1 $operand"
+	}
+    }
+}
+
+# Enum option tests.
+proc_with_prefix test-enum {variant} {
+    set cmd [make_cmd $variant]
+
+    res_test_gdb_complete_multiple 1 "$cmd -enum " "" "" {
+	"xxx"
+	"yyy"
+	"zzz"
+    }
+
+    # Check that "-" where a value is expected does not show the
+    # command's options.  I.e., an enum's value is not optional.
+    # Check both completion and running the command.
+    res_test_gdb_complete_none 1 "$cmd -enum -"
+    gdb_test "$cmd -enum --"\
+	"Requires an argument. Valid arguments are xxx, yyy, zzz\\."
+
+    # Try passing an undefined item to an enum option.
+    gdb_test "$cmd -enum www --" "Undefined item: \"www\"."
+}
+
+# Run the options framework tests first.
+foreach_with_prefix cmd {
+    "require-delimiter"
+    "unknown-is-error"
+    "unknown-is-operand"
+} {
+    test-misc $cmd
+    test-flag $cmd
+    test-boolean $cmd
+    foreach subcmd {"uinteger" "zuinteger-unlimited" } {
+	test-uinteger $cmd $subcmd
+    }
+    test-enum $cmd
+}
