Bläddra i källkod

Add a rich command-line argument parsing library. (#2978)

This library is designed around supporting the kinds of use cases we
expect in the toolchain and other Carbon tools. It supports subcommands,
options, and prints help.

There is still a decent chunk of work to be done to finish polishing
this, but it should give us a solid starting point.

For details about the library and a brief roadmap, see the main comment
in `command_line.h` which provides a comprehensive overview of the
library and a roadmap of the remaining work.

Porting the driver to this went fairly well, but did require some
changes. That port is separated into a follow-up PR #2979.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Co-authored-by: Lucile Rose Nihlen <luci.the.rose@gmail.com>
Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Chandler Carruth 2 år sedan
förälder
incheckning
db91db9097
4 ändrade filer med 3313 tillägg och 0 borttagningar
  1. 23 0
      common/BUILD
  2. 1496 0
      common/command_line.cpp
  3. 856 0
      common/command_line.h
  4. 938 0
      common/command_line_test.cpp

+ 23 - 0
common/BUILD

@@ -12,6 +12,29 @@ cc_library(
     ],
 )
 
+cc_library(
+    name = "command_line",
+    srcs = ["command_line.cpp"],
+    hdrs = ["command_line.h"],
+    deps = [
+        ":check",
+        ":ostream",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "command_line_test",
+    srcs = ["command_line_test.cpp"],
+    deps = [
+        ":command_line",
+        "//testing/base:gtest_main",
+        "//testing/base:test_raw_ostream",
+        "@com_google_googletest//:gtest",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
 cc_library(
     name = "check",
     srcs = [

+ 1496 - 0
common/command_line.cpp

@@ -0,0 +1,1496 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "common/command_line.h"
+
+#include <initializer_list>
+#include <memory>
+
+#include "llvm/Support/FormatVariadic.h"
+
+namespace Carbon::CommandLine {
+
+auto operator<<(llvm::raw_ostream& output, ParseResult result)
+    -> llvm::raw_ostream& {
+  switch (result) {
+    case ParseResult::Error:
+      return output << "Error";
+    case ParseResult::MetaSuccess:
+      return output << "MetaSuccess";
+    case ParseResult::Success:
+      return output << "Success";
+  }
+  CARBON_FATAL() << "Corrupt parse result!";
+}
+
+auto operator<<(llvm::raw_ostream& output, ArgKind kind) -> llvm::raw_ostream& {
+  switch (kind) {
+    case ArgKind::Flag:
+      return output << "Boolean";
+    case ArgKind::Integer:
+      return output << "Integer";
+    case ArgKind::String:
+      return output << "String";
+    case ArgKind::OneOf:
+      return output << "OneOf";
+    case ArgKind::MetaActionOnly:
+      return output << "MetaActionOnly";
+    case ArgKind::Invalid:
+      return output << "Invalid";
+  }
+  CARBON_FATAL() << "Corrupt argument kind!";
+}
+
+auto operator<<(llvm::raw_ostream& output, CommandKind kind)
+    -> llvm::raw_ostream& {
+  switch (kind) {
+    case CommandKind::Invalid:
+      return output << "Invalid";
+    case CommandKind::RequiresSubcommand:
+      return output << "RequiresSubcommand";
+    case CommandKind::Action:
+      return output << "Action";
+    case CommandKind::MetaAction:
+      return output << "MetaAction";
+  }
+  CARBON_FATAL() << "Corrupt command kind!";
+}
+Arg::Arg(const ArgInfo& info) : info(info) {}
+
+Arg::~Arg() {
+  switch (kind) {
+    case Kind::Flag:
+    case Kind::Integer:
+    case Kind::String:
+    case Kind::MetaActionOnly:
+    case Kind::Invalid:
+      // Nothing to do!
+      break;
+    case Kind::OneOf:
+      value_strings.~decltype(value_strings)();
+      value_action.~ValueActionT();
+      if (has_default) {
+        default_action.~DefaultActionT();
+      }
+      break;
+  }
+}
+
+Command::Command(const CommandInfo& info, Command* parent)
+    : info(info), parent(parent) {}
+
+class MetaPrinter {
+ public:
+  explicit MetaPrinter(llvm::raw_ostream& out) : out_(out) {}
+
+  // Registers this meta printer with a command through the provided builder.
+  //
+  // This adds meta subcommands or options to print both help and version
+  // information for the command.
+  void RegisterWithCommand(const Command& command, CommandBuilder& builder);
+
+  void PrintHelp(const Command& command) const;
+  void PrintVersion(const Command& command) const;
+  void PrintSubcommands(const Command& command) const;
+
+ private:
+  // The indent is calibrated to allow a short and long option after a two
+  // character indent on the prior line to be visually recognized as separate
+  // from the hanging indent.
+  //
+  // Visual guide:                               |  -x, --extract
+  //                                             |          Hanging indented.
+  static constexpr llvm::StringRef BlockIndent = "          ";
+
+  // Width limit for parent command options in usage rendering.
+  static constexpr int MaxParentOptionUsageWidth = 8;
+
+  // Width limit for the leaf command options in usage rendering.
+  static constexpr int MaxLeafOptionUsageWidth = 16;
+
+  static constexpr CommandInfo HelpCommandInfo = {
+      .name = "help",
+      .help = R"""(
+Prints help information for the command, including a description, command line
+usage, and details of each subcommand and option that can be provided.
+)""",
+      .help_short = R"""(
+Prints help information.
+)""",
+  };
+  static constexpr ArgInfo HelpArgInfo = {
+      .name = "help",
+      .value_name = "(full|short)",
+      .help = R"""(
+Prints help information for the command, including a description, command line
+usage, and details of each option that can be provided.
+)""",
+      .help_short = HelpCommandInfo.help_short,
+  };
+
+  // Provide a customized description for help on a subcommand to avoid
+  // confusion with the top-level help.
+  static constexpr CommandInfo SubHelpCommandInfo = {
+      .name = "help",
+      .help = R"""(
+Prints help information for the subcommand, including a description, command
+line usage, and details of each further subcommand and option that can be
+provided.
+)""",
+      .help_short = R"""(
+Prints subcommand help information.
+)""",
+  };
+  static constexpr ArgInfo SubHelpArgInfo = {
+      .name = "help",
+      .value_name = "(full|short)",
+      .help = R"""(
+Prints help information for the subcommand, including a description, command
+line usage, and details of each option that can be provided.
+)""",
+      .help_short = SubHelpCommandInfo.help_short,
+  };
+
+  static constexpr CommandInfo VersionCommandInfo = {
+      .name = "version",
+      .help = R"""(
+Prints the version of this command.
+)""",
+  };
+  static constexpr ArgInfo VersionArgInfo = {
+      .name = "version",
+      .help = VersionCommandInfo.help,
+  };
+
+  // A general helper for rendering a text block.
+  void PrintTextBlock(llvm::StringRef indent, llvm::StringRef text) const;
+
+  // Helpers for version and build information printing.
+  void PrintRawVersion(const Command& command, llvm::StringRef indent) const;
+  void PrintRawBuildInfo(const Command& command, llvm::StringRef indent) const;
+
+  // Helpers for printing components of help and usage output for arguments,
+  // including options and positional arguments.
+  void PrintArgValueUsage(const Arg& arg) const;
+  void PrintOptionUsage(const Arg& option) const;
+  void PrintOptionShortName(const Arg& arg) const;
+  void PrintArgShortValues(const Arg& arg) const;
+  void PrintArgLongValues(const Arg& arg, llvm::StringRef indent) const;
+  void PrintArgHelp(const Arg& arg, llvm::StringRef indent) const;
+
+  // Helpers for printing command usage summaries.
+  void PrintRawUsageCommandAndOptions(
+      const Command& command,
+      int max_option_width = MaxLeafOptionUsageWidth) const;
+  void PrintRawUsage(const Command& command, llvm::StringRef indent) const;
+  void PrintUsage(const Command& command) const;
+
+  // Helpers to print various sections of `PrintHelp` that only occur within
+  // that output.
+  void PrintHelpSubcommands(const Command& command) const;
+  void PrintHelpPositionalArgs(const Command& command) const;
+  void PrintHelpOptions(const Command& command) const;
+
+  llvm::raw_ostream& out_;
+
+  // A flag that may be configured during command line parsing to select between
+  // long and short form help output.
+  bool short_help_ = false;
+};
+
+void MetaPrinter::RegisterWithCommand(const Command& command,
+                                      CommandBuilder& builder) {
+  bool is_subcommand = command.parent;
+  bool has_subcommands = !command.subcommands.empty();
+
+  // If this command has subcommands, we prefer that model for access meta
+  // actions, but still silently support using the flags. But we never want to
+  // *add* subcommands if they aren't already being used.
+  if (has_subcommands) {
+    builder.AddSubcommand(
+        is_subcommand ? SubHelpCommandInfo : HelpCommandInfo,
+        [&](CommandBuilder& sub_b) {
+          sub_b.Meta([this, &command]() { PrintHelp(command); });
+        });
+
+    // Only add version printing support if there is a version string
+    // configured for this command.
+    if (!command.info.version.empty()) {
+      builder.AddSubcommand(VersionCommandInfo, [&](CommandBuilder& sub_b) {
+        sub_b.Meta([this, &command]() { PrintVersion(command); });
+      });
+    }
+  }
+  builder.AddOneOfOption(
+      is_subcommand ? SubHelpArgInfo : HelpArgInfo, [&](auto& arg_b) {
+        arg_b.HelpHidden(has_subcommands);
+        arg_b.SetOneOf(
+            {
+                arg_b.OneOfValue("full", false).Default(true),
+                arg_b.OneOfValue("short", true),
+            },
+            &short_help_);
+        arg_b.MetaAction([this, &command]() { PrintHelp(command); });
+      });
+
+  // Only add version printing support if there is a version string configured
+  // for this command.
+  if (!command.info.version.empty()) {
+    builder.AddMetaActionOption(VersionArgInfo, [&](auto& arg_b) {
+      arg_b.HelpHidden(has_subcommands);
+      arg_b.MetaAction([this, &command]() { PrintVersion(command); });
+    });
+  }
+}
+
+void MetaPrinter::PrintHelp(const Command& command) const {
+  // TODO: begin using the short setting to customize the output.
+  (void)short_help_;
+
+  const CommandInfo& info = command.info;
+  if (!info.version.empty()) {
+    // We use the version string as a header for the command help when present.
+    PrintRawVersion(command, /*indent=*/"");
+    out_ << "\n";
+  }
+  if (!command.info.help.empty()) {
+    PrintTextBlock("", info.help);
+    out_ << "\n";
+  }
+  if (!info.build_info.empty()) {
+    out_ << "Build info:\n";
+    PrintRawBuildInfo(command, /*indent=*/"  ");
+    out_ << "\n";
+  }
+
+  PrintUsage(command);
+  PrintHelpSubcommands(command);
+  PrintHelpPositionalArgs(command);
+  PrintHelpOptions(command);
+
+  if (!info.help_epilogue.empty()) {
+    out_ << "\n";
+    PrintTextBlock("", info.help_epilogue);
+  }
+
+  // End with a blank line for the long help to make it easier to separate from
+  // anything that follows in the shell.
+  out_ << "\n";
+}
+
+void MetaPrinter::PrintVersion(const Command& command) const {
+  CARBON_CHECK(!command.info.version.empty())
+      << "Printing should not be enabled without a version string configured.";
+  PrintRawVersion(command, /*indent=*/"");
+  if (!command.info.build_info.empty()) {
+    out_ << "\n";
+    // If there is build info to print, we also render that without any indent.
+    PrintRawBuildInfo(command, /*indent=*/"");
+  }
+}
+
+void MetaPrinter::PrintSubcommands(const Command& command) const {
+  for (const auto& subcommand :
+       llvm::ArrayRef(command.subcommands).drop_back()) {
+    out_ << "'" << subcommand->info.name << "', ";
+  }
+  if (command.subcommands.size() > 1) {
+    out_ << "or ";
+  }
+  out_ << "'" << command.subcommands.back()->info.name << "'";
+}
+
+void MetaPrinter::PrintRawVersion(const Command& command,
+                                  llvm::StringRef indent) const {
+  // Newlines are trimmed from the version string an a closing newline added but
+  // no other formatting is performed.
+  out_ << indent << command.info.version.trim('\n') << "\n";
+}
+void MetaPrinter::PrintRawBuildInfo(const Command& command,
+                                    llvm::StringRef indent) const {
+  // Print the build info line-by-line without any wrapping in case it
+  // contains line-oriented formatted text, but drop leading and trailing blank
+  // lines.
+  llvm::SmallVector<llvm::StringRef, 128> lines;
+  command.info.build_info.trim('\n').split(lines, "\n");
+  for (auto line : lines) {
+    out_ << indent << line << "\n";
+  }
+}
+
+void MetaPrinter::PrintTextBlock(llvm::StringRef indent,
+                                 llvm::StringRef text) const {
+  // Strip leading and trailing newlines to make it easy to use multiline raw
+  // string literals that will naturally have those.
+  text = text.trim('\n');
+  // For empty text, print nothing at all. The caller formatting will work to
+  // handle this gracefully.
+  if (text.empty()) {
+    return;
+  }
+
+  // Remove line breaks from the text that would typically be removed when
+  // rendering it as Markdown. The goal is to preserve:
+  //
+  // - Blank lines as paragraph separators.
+  // - Line breaks after list items or other structural components in Markdown.
+  // - Fenced regions exactly as they appear.
+  //
+  // And within paragraphs (including those nested in lists), reflow the
+  // paragraph intelligently to the column width. There are TODOs below about
+  // both lists and reflowing.
+  llvm::SmallVector<llvm::StringRef, 128> input_lines;
+  text.split(input_lines, "\n");
+
+  for (int i = 0, size = input_lines.size(); i < size;) {
+    if (input_lines[i].empty()) {
+      // Blank lines are preserved.
+      out_ << "\n";
+      ++i;
+      continue;
+    }
+
+    if (input_lines[i].starts_with("```")) {
+      // Fenced regions are preserved verbatim.
+      llvm::StringRef fence =
+          input_lines[i].slice(0, input_lines[i].find_first_not_of("`"));
+      do {
+        out_ << indent << input_lines[i] << "\n";
+        ++i;
+      } while (i < size && !input_lines[i].starts_with(fence));
+      if (i >= size) {
+        // Don't error on malformed text blocks, just print what we've got.
+        break;
+      }
+      // Including the close of the fence.
+      out_ << indent << input_lines[i] << "\n";
+      ++i;
+      continue;
+    }
+
+    if (input_lines[i].starts_with("    ")) {
+      // Indented code blocks ar preserved verbatim, but we don't support tabs
+      // in the indent for simplicity.
+      do {
+        out_ << indent << input_lines[i] << "\n";
+        ++i;
+      } while (i < size && input_lines[i].starts_with("    "));
+      continue;
+    }
+
+    // TODO: Detect other Markdown structures, especially lists and tables.
+
+    // Otherwise, collect all of the lines until the end or the next blank line
+    // as a block of text.
+    //
+    // TODO: This is where we should re-flow.
+    llvm::StringRef space = indent;
+    do {
+      out_ << space << input_lines[i].trim();
+      space = " ";
+      ++i;
+    } while (i < size && !input_lines[i].empty());
+    out_ << "\n";
+  }
+}
+
+void MetaPrinter::PrintArgValueUsage(const Arg& arg) const {
+  if (!arg.info.value_name.empty()) {
+    out_ << arg.info.value_name;
+    return;
+  }
+  if (arg.kind == Arg::Kind::OneOf) {
+    out_ << "(";
+    llvm::ListSeparator sep("|");
+    for (llvm::StringRef value_string : arg.value_strings) {
+      out_ << sep << value_string;
+    }
+    out_ << ")";
+    return;
+  }
+  out_ << "...";
+}
+
+void MetaPrinter::PrintOptionUsage(const Arg& option) const {
+  if (option.kind == Arg::Kind::Flag) {
+    out_ << "--" << (option.default_flag ? "no-" : "") << option.info.name;
+    return;
+  }
+  out_ << "--" << option.info.name;
+  if (option.kind != Arg::Kind::MetaActionOnly) {
+    out_ << (option.has_default ? "[" : "") << "=";
+    PrintArgValueUsage(option);
+    if (option.has_default) {
+      out_ << "]";
+    }
+  }
+}
+
+void MetaPrinter::PrintOptionShortName(const Arg& arg) const {
+  CARBON_CHECK(!arg.info.short_name.empty()) << "No short name to use.";
+  out_ << "-" << arg.info.short_name;
+}
+
+void MetaPrinter::PrintArgShortValues(const Arg& arg) const {
+  CARBON_CHECK(arg.kind == Arg::Kind::OneOf)
+      << "Only one-of arguments have interesting value snippets to print.";
+  llvm::ListSeparator sep;
+  for (llvm::StringRef value_string : arg.value_strings) {
+    out_ << sep << value_string;
+  }
+}
+void MetaPrinter::PrintArgLongValues(const Arg& arg,
+                                     llvm::StringRef indent) const {
+  out_ << indent << "Possible values:\n";
+  // TODO: It would be good to add help text for each value and then print it
+  // here.
+  for (int i : llvm::seq<int>(0, arg.value_strings.size())) {
+    llvm::StringRef value_string = arg.value_strings[i];
+    out_ << indent << "- " << value_string;
+    if (arg.has_default && i == arg.default_value_index) {
+      out_ << " (default)";
+    }
+    out_ << "\n";
+  }
+}
+
+void MetaPrinter::PrintArgHelp(const Arg& arg, llvm::StringRef indent) const {
+  // Print out the main help text.
+  PrintTextBlock(indent, arg.info.help);
+
+  // Then print out any help based on the values.
+  switch (arg.kind) {
+    case Arg::Kind::Integer:
+      if (arg.has_default) {
+        out_ << "\n";
+        out_ << indent << "Default value: " << arg.default_integer << "\n";
+      }
+      break;
+    case Arg::Kind::String:
+      if (arg.has_default) {
+        out_ << "\n";
+        out_ << indent << "Default value: " << arg.default_string << "\n";
+      }
+      break;
+    case Arg::Kind::OneOf:
+      out_ << "\n";
+      PrintArgLongValues(arg, indent);
+      break;
+    case Arg::Kind::Flag:
+    case Arg::Kind::MetaActionOnly:
+      // No value help.
+      break;
+    case Arg::Kind::Invalid:
+      CARBON_FATAL() << "Argument configured without any action or kind!";
+  }
+}
+
+void MetaPrinter::PrintRawUsageCommandAndOptions(const Command& command,
+                                                 int max_option_width) const {
+  // Recursively print parent usage first with a compressed width.
+  if (command.parent) {
+    PrintRawUsageCommandAndOptions(*command.parent, MaxParentOptionUsageWidth);
+    out_ << " ";
+  }
+
+  out_ << command.info.name;
+
+  // Buffer the options rendering so we can limit its length.
+  std::string buffer_str;
+  llvm::raw_string_ostream buffer_out(buffer_str);
+  MetaPrinter buffer_printer(buffer_out);
+  bool have_short_flags = false;
+  for (const auto& arg : command.options) {
+    if (static_cast<int>(buffer_str.size()) > max_option_width) {
+      break;
+    }
+    // We can summarize positive boolean flags with a short name using a
+    // sequence of short names in a single rendered argument.
+    if (arg->kind == Arg::Kind::Flag && !arg->default_flag &&
+        !arg->info.short_name.empty()) {
+      if (!have_short_flags) {
+        have_short_flags = true;
+        buffer_out << "-";
+      }
+      buffer_out << arg->info.short_name;
+    }
+  }
+  llvm::StringRef space = have_short_flags ? " " : "";
+  for (const auto& option : command.options) {
+    if (static_cast<int>(buffer_str.size()) > max_option_width) {
+      break;
+    }
+    if (option->is_help_hidden || option->meta_action) {
+      // Skip hidden and options with meta actions attached.
+      continue;
+    }
+    if (option->kind == Arg::Kind::Flag && !option->default_flag &&
+        !option->info.short_name.empty()) {
+      // Handled with short names above.
+      continue;
+    }
+    buffer_out << space;
+    buffer_printer.PrintOptionUsage(*option);
+    space = " ";
+  }
+  if (!buffer_str.empty()) {
+    if (static_cast<int>(buffer_str.size()) <= max_option_width) {
+      out_ << " [" << buffer_str << "]";
+    } else {
+      out_ << " [OPTIONS]";
+    }
+  }
+}
+
+void MetaPrinter::PrintRawUsage(const Command& command,
+                                llvm::StringRef indent) const {
+  if (!command.info.usage.empty()) {
+    PrintTextBlock(indent, command.info.usage);
+    return;
+  }
+
+  if (command.kind != Command::Kind::RequiresSubcommand) {
+    // We're a valid leaf command, so synthesize a full usage line.
+    out_ << indent;
+    PrintRawUsageCommandAndOptions(command);
+
+    if (!command.positional_args.empty()) {
+      bool open_optional = false;
+      for (int i : llvm::seq<int>(0, command.positional_args.size())) {
+        out_ << " ";
+        if (i != 0 && command.positional_args[i - 1]->is_append) {
+          out_ << "-- ";
+        }
+        const auto& arg = command.positional_args[i];
+        if (!arg->is_required && !open_optional) {
+          out_ << "[";
+          open_optional = true;
+        }
+        out_ << "<" << arg->info.name << ">";
+        if (arg->is_append) {
+          out_ << "...";
+        }
+      }
+      if (open_optional) {
+        out_ << "]";
+      }
+    }
+    out_ << "\n";
+  }
+
+  // If we have subcommands, also recurse into them so each one can print their
+  // usage lines.
+  for (const auto& subcommand : command.subcommands) {
+    if (subcommand->is_help_hidden ||
+        subcommand->kind == Command::Kind::MetaAction) {
+      continue;
+    }
+    PrintRawUsage(*subcommand, indent);
+  }
+}
+
+void MetaPrinter::PrintUsage(const Command& command) const {
+  if (!command.parent) {
+    out_ << "Usage:\n";
+  } else {
+    out_ << "Subcommand '" << command.info.name << "' usage:\n";
+  }
+  PrintRawUsage(command, "  ");
+}
+
+void MetaPrinter::PrintHelpSubcommands(const Command& command) const {
+  bool first_subcommand = true;
+  for (const auto& subcommand : command.subcommands) {
+    if (subcommand->is_help_hidden) {
+      continue;
+    }
+    if (first_subcommand) {
+      first_subcommand = false;
+      if (!command.parent) {
+        out_ << "\nSubcommands:";
+      } else {
+        out_ << "\nSubcommand '" << command.info.name << "' subcommands:";
+      }
+    }
+    out_ << "\n";
+    out_ << "  " << subcommand->info.name << "\n";
+    PrintTextBlock(BlockIndent, subcommand->info.help);
+  }
+}
+
+void MetaPrinter::PrintHelpPositionalArgs(const Command& command) const {
+  bool first_positional_arg = true;
+  for (const auto& positional_arg : command.positional_args) {
+    if (positional_arg->is_help_hidden) {
+      continue;
+    }
+    if (first_positional_arg) {
+      first_positional_arg = false;
+      if (!command.parent) {
+        out_ << "\nPositional arguments:";
+      } else {
+        out_ << "\nSubcommand '" << command.info.name
+             << "' positional arguments:";
+      }
+    }
+    out_ << "\n";
+    out_ << "  " << positional_arg->info.name << "\n";
+    PrintArgHelp(*positional_arg, BlockIndent);
+  }
+}
+
+void MetaPrinter::PrintHelpOptions(const Command& command) const {
+  bool first_option = true;
+  for (const auto& option : command.options) {
+    if (option->is_help_hidden) {
+      continue;
+    }
+    if (first_option) {
+      first_option = false;
+      if (!command.parent && command.subcommands.empty()) {
+        // Only one command level.
+        out_ << "\nOptions:";
+      } else if (!command.parent) {
+        out_ << "\nCommand options:";
+      } else {
+        out_ << "\nSubcommand '" << command.info.name << "' options:";
+      }
+    }
+    out_ << "\n";
+    out_ << "  ";
+    if (!option->info.short_name.empty()) {
+      PrintOptionShortName(*option);
+      out_ << ", ";
+    } else {
+      out_ << "    ";
+    }
+    PrintOptionUsage(*option);
+    out_ << "\n";
+    PrintArgHelp(*option, BlockIndent);
+  }
+}
+
+class Parser {
+ public:
+  explicit Parser(llvm::raw_ostream& out, llvm::raw_ostream& errors,
+                  const CommandInfo& command_info,
+                  llvm::function_ref<void(CommandBuilder&)> build);
+
+  auto Parse(llvm::ArrayRef<llvm::StringRef> unparsed_args) -> ParseResult;
+
+ private:
+  friend CommandBuilder;
+
+  // For the option and subcommand maps, we use somewhat large small size
+  // buffers (16) as there is no real size pressure on these and its nice to
+  // avoid heap allocation in the small cases.
+  using OptionMapT =
+      llvm::SmallDenseMap<llvm::StringRef, llvm::PointerIntPair<Arg*, 1, bool>,
+                          16>;
+  using SubcommandMapT = llvm::SmallDenseMap<llvm::StringRef, Command*, 16>;
+
+  // This table is sized to be 128 so that it can hold ASCII characters. We
+  // don't need any more than this and using a direct table indexed by the
+  // character's numeric value makes for a convenient map.
+  using ShortOptionTableT = std::array<OptionMapT::mapped_type*, 128>;
+
+  void PopulateMaps(const Command& command);
+
+  void SetOptionDefault(const Arg& option);
+
+  auto ParseNegatedFlag(const Arg& flag, std::optional<llvm::StringRef> value)
+      -> bool;
+  auto ParseFlag(const Arg& flag, std::optional<llvm::StringRef> value) -> bool;
+  auto ParseIntegerArgValue(const Arg& arg, llvm::StringRef value) -> bool;
+  auto ParseStringArgValue(const Arg& arg, llvm::StringRef value) -> bool;
+  auto ParseOneOfArgValue(const Arg& arg, llvm::StringRef value) -> bool;
+  auto ParseArg(const Arg& arg, bool short_spelling,
+                std::optional<llvm::StringRef> value, bool negated_name = false)
+      -> bool;
+
+  auto SplitValue(llvm::StringRef& unparsed_arg)
+      -> std::optional<llvm::StringRef>;
+  auto ParseLongOption(llvm::StringRef unparsed_arg) -> bool;
+  auto ParseShortOptionSeq(llvm::StringRef unparsed_arg) -> bool;
+  auto FinalizeParsedOptions() -> bool;
+
+  auto ParsePositionalArg(llvm::StringRef unparsed_arg) -> bool;
+  auto ParseSubcommand(llvm::StringRef unparsed_arg) -> bool;
+
+  auto ParsePositionalSuffix(llvm::ArrayRef<llvm::StringRef> unparsed_args)
+      -> bool;
+
+  auto FinalizeParse() -> ParseResult;
+
+  // When building a command, it registers arguments and potentially subcommands
+  // that are meta actions to print things to standard out, so we build a meta
+  // printer for that here.
+  MetaPrinter meta_printer_;
+
+  // Most parsing output goes to an error stream, and we also provide an
+  // error-oriented meta printer for when that is useful during parsing.
+  llvm::raw_ostream& errors_;
+  MetaPrinter error_meta_printer_;
+
+  Command root_command_;
+
+  const Command* command_;
+
+  OptionMapT option_map_;
+  ShortOptionTableT short_option_table_;
+  SubcommandMapT subcommand_map_;
+
+  int positional_arg_index_ = 0;
+  bool appending_to_positional_arg_ = false;
+
+  ActionT arg_meta_action_;
+};
+
+void Parser::PopulateMaps(const Command& command) {
+  option_map_.clear();
+  for (const auto& option : command.options) {
+    option_map_.insert({option->info.name, {option.get(), false}});
+  }
+  short_option_table_.fill(nullptr);
+  for (auto& map_entry : option_map_) {
+    const Arg* option = map_entry.second.getPointer();
+    if (option->info.short_name.empty()) {
+      continue;
+    }
+    CARBON_CHECK(option->info.short_name.size() == 1)
+        << "Short option names must have exactly one character.";
+    unsigned char short_char = option->info.short_name[0];
+    CARBON_CHECK(short_char < short_option_table_.size())
+        << "Short option name outside of the expected range.";
+    short_option_table_[short_char] = &map_entry.second;
+  }
+  subcommand_map_.clear();
+  for (const auto& subcommand : command.subcommands) {
+    subcommand_map_.insert({subcommand->info.name, subcommand.get()});
+  }
+}
+
+void Parser::SetOptionDefault(const Arg& option) {
+  CARBON_CHECK(option.has_default) << "No default value available!";
+  switch (option.kind) {
+    case Arg::Kind::Flag:
+      *option.flag_storage = option.default_flag;
+      break;
+    case Arg::Kind::Integer:
+      *option.integer_storage = option.default_integer;
+      break;
+    case Arg::Kind::String:
+      *option.string_storage = option.default_string;
+      break;
+    case Arg::Kind::OneOf:
+      option.default_action(option);
+      break;
+    case Arg::Kind::MetaActionOnly:
+      CARBON_FATAL() << "Can't set a default value for a meta action!";
+    case Arg::Kind::Invalid:
+      CARBON_FATAL() << "Option configured without any action or kind!";
+  }
+}
+
+auto Parser::ParseNegatedFlag(const Arg& flag,
+                              std::optional<llvm::StringRef> value) -> bool {
+  if (flag.kind != Arg::Kind::Flag) {
+    errors_ << "ERROR: Cannot use a negated flag name by prefixing it with "
+               "'no-' when it isn't a boolean flag argument.\n";
+    return false;
+  }
+  if (value) {
+    errors_ << "ERROR: Cannot specify a value when using a flag name prefixed "
+               "with 'no-' -- that prefix implies a value of 'false'.\n";
+    return false;
+  }
+  *flag.flag_storage = false;
+  return true;
+}
+
+auto Parser::ParseFlag(const Arg& flag, std::optional<llvm::StringRef> value)
+    -> bool {
+  CARBON_CHECK(flag.kind == Arg::Kind::Flag) << "Incorrect kind: " << flag.kind;
+  if (!value || *value == "true") {
+    *flag.flag_storage = true;
+  } else if (*value == "false") {
+    *flag.flag_storage = false;
+  } else {
+    errors_ << "ERROR: Invalid value specified for the boolean flag '--"
+            << flag.info.name << "': " << *value << "\n";
+
+    return false;
+  }
+  return true;
+}
+
+auto Parser::ParseIntegerArgValue(const Arg& arg, llvm::StringRef value)
+    -> bool {
+  CARBON_CHECK(arg.kind == Arg::Kind::Integer)
+      << "Incorrect kind: " << arg.kind;
+  int integer_value;
+  // Note that this method returns *true* on error!
+  if (value.getAsInteger(/*Radix=*/0, integer_value)) {
+    errors_ << "ERROR: Cannot parse value for option '--" << arg.info.name
+            << "' as an integer: " << value << "\n";
+    return false;
+  }
+  if (!arg.is_append) {
+    *arg.integer_storage = integer_value;
+  } else {
+    arg.integer_sequence->push_back(integer_value);
+  }
+  return true;
+}
+
+auto Parser::ParseStringArgValue(const Arg& arg, llvm::StringRef value)
+    -> bool {
+  CARBON_CHECK(arg.kind == Arg::Kind::String) << "Incorrect kind: " << arg.kind;
+  if (!arg.is_append) {
+    *arg.string_storage = value;
+  } else {
+    arg.string_sequence->push_back(value);
+  }
+  return true;
+}
+
+auto Parser::ParseOneOfArgValue(const Arg& arg, llvm::StringRef value) -> bool {
+  CARBON_CHECK(arg.kind == Arg::Kind::OneOf) << "Incorrect kind: " << arg.kind;
+  if (!arg.value_action(arg, value)) {
+    errors_ << "ERROR: Option '--" << arg.info.name << "=";
+    llvm::printEscapedString(value, errors_);
+    errors_ << "' has an invalid value '";
+    llvm::printEscapedString(value, errors_);
+    errors_ << "'; valid values are: ";
+    for (auto value_string : arg.value_strings.drop_back()) {
+      errors_ << "'" << value_string << "', ";
+    }
+    if (arg.value_strings.size() > 1) {
+      errors_ << "or ";
+    }
+    errors_ << "'" << arg.value_strings.back() << "'\n";
+    return false;
+  }
+  return true;
+}
+
+auto Parser::ParseArg(const Arg& arg, bool short_spelling,
+                      std::optional<llvm::StringRef> value, bool negated_name)
+    -> bool {
+  // If this argument has a meta action, replace the current meta action with
+  // it.
+  if (arg.meta_action) {
+    arg_meta_action_ = arg.meta_action;
+  }
+
+  // Boolean flags have special parsing logic.
+  if (negated_name) {
+    return ParseNegatedFlag(arg, value);
+  }
+  if (arg.kind == Arg::Kind::Flag) {
+    return ParseFlag(arg, value);
+  }
+
+  auto name =
+      llvm::formatv(short_spelling ? "'-{0}' (short for '--{1}')" : "'--{1}'",
+                    arg.info.short_name, arg.info.name);
+
+  if (!value) {
+    // We can't have a positional argument without a value, so we know this is
+    // an option and handle it as such.
+    if (arg.kind == Arg::Kind::MetaActionOnly) {
+      // Nothing further to do here, this is only a meta-action.
+      return true;
+    }
+    if (!arg.has_default) {
+      errors_ << "ERROR: Option " << name
+              << " requires a value to be provided and none was.\n";
+      return false;
+    }
+    SetOptionDefault(arg);
+    return true;
+  }
+
+  // There is a value to parse as part of the argument.
+  switch (arg.kind) {
+    case Arg::Kind::Integer:
+      return ParseIntegerArgValue(arg, *value);
+    case Arg::Kind::String:
+      return ParseStringArgValue(arg, *value);
+    case Arg::Kind::OneOf:
+      return ParseOneOfArgValue(arg, *value);
+    case Arg::Kind::MetaActionOnly:
+      errors_ << "ERROR: Option " << name
+              << " cannot be used with a value, and '" << *value
+              << "' was provided.\n";
+      // TODO: improve message
+      return false;
+    case Arg::Kind::Flag:
+    case Arg::Kind::Invalid:
+      CARBON_FATAL() << "Invalid kind!";
+  }
+}
+
+auto Parser::SplitValue(llvm::StringRef& unparsed_arg)
+    -> std::optional<llvm::StringRef> {
+  // Split out a value if present.
+  std::optional<llvm::StringRef> value;
+  auto index = unparsed_arg.find('=');
+  if (index != llvm::StringRef::npos) {
+    value = unparsed_arg.substr(index + 1);
+    unparsed_arg = unparsed_arg.substr(0, index);
+  }
+  return value;
+}
+
+auto Parser::ParseLongOption(llvm::StringRef unparsed_arg) -> bool {
+  CARBON_CHECK(unparsed_arg.starts_with("--") && unparsed_arg.size() > 2)
+      << "Must only be called on a potential long option.";
+
+  // Walk past the double dash.
+  unparsed_arg = unparsed_arg.drop_front(2);
+  bool negated_name = unparsed_arg.consume_front("no-");
+  std::optional<llvm::StringRef> value = SplitValue(unparsed_arg);
+
+  auto option_it = option_map_.find(unparsed_arg);
+  if (option_it == option_map_.end()) {
+    errors_ << "ERROR: Unknown option '--" << (negated_name ? "no-" : "")
+            << unparsed_arg << "'\n";
+    // TODO: improve error
+    return false;
+  }
+
+  // Mark this option as parsed.
+  option_it->second.setInt(true);
+
+  // Parse this specific option and any value.
+  const Arg& option = *option_it->second.getPointer();
+  return ParseArg(option, /*short_spelling=*/false, value, negated_name);
+}
+
+auto Parser::ParseShortOptionSeq(llvm::StringRef unparsed_arg) -> bool {
+  CARBON_CHECK(unparsed_arg.starts_with("-") && unparsed_arg.size() > 1)
+      << "Must only be called on a potential short option sequence.";
+
+  unparsed_arg = unparsed_arg.drop_front();
+  std::optional<llvm::StringRef> value = SplitValue(unparsed_arg);
+  if (value && unparsed_arg.size() != 1) {
+    errors_ << "ERROR: Cannot provide a value to the group of multiple short "
+               "options '-"
+            << unparsed_arg
+            << "=...'; values must be provided to a single option, using "
+               "either the short or long spelling.\n";
+    return false;
+  }
+
+  for (unsigned char c : unparsed_arg) {
+    auto* arg_entry = short_option_table_[c];
+    if (!arg_entry) {
+      errors_ << "ERROR: Unknown short option '" << c << "'\n";
+      return false;
+    }
+    // Mark this argument as parsed.
+    arg_entry->setInt(true);
+
+    // Parse the argument, including the value if this is the last.
+    const Arg& arg = *arg_entry->getPointer();
+    if (!ParseArg(arg, /*short_spelling=*/true, value)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+auto Parser::FinalizeParsedOptions() -> bool {
+  llvm::SmallVector<const Arg*> missing_options;
+  for (const auto& option_entry : option_map_) {
+    const Arg* option = option_entry.second.getPointer();
+    if (!option_entry.second.getInt()) {
+      // If the argument has a default value and isn't a meta-action, we need to
+      // act on that when it isn't passed.
+      if (option->has_default && !option->meta_action) {
+        SetOptionDefault(*option);
+      }
+      // Remember any missing required arguments, we'll diagnose those.
+      if (option->is_required) {
+        missing_options.push_back(option);
+      }
+    }
+  }
+  if (missing_options.empty()) {
+    return true;
+  }
+
+  // Sort the missing arguments by name to provide a stable and deterministic
+  // error message. We know there can't be duplicate names because these came
+  // from a may keyed on the name, so this provides a total ordering.
+  std::sort(missing_options.begin(), missing_options.end(),
+            [](const Arg* lhs, const Arg* rhs) {
+              return lhs->info.name < rhs->info.name;
+            });
+
+  for (const Arg* option : missing_options) {
+    errors_ << "ERROR: Required option '--" << option->info.name
+            << "' not provided.\n";
+  }
+
+  return false;
+}
+
+auto Parser::ParsePositionalArg(llvm::StringRef unparsed_arg) -> bool {
+  if (static_cast<size_t>(positional_arg_index_) >=
+      command_->positional_args.size()) {
+    errors_ << "ERROR: Completed parsing all "
+            << command_->positional_args.size()
+            << " configured positional arguments, and found an additional "
+               "positional argument: '"
+            << unparsed_arg << "'\n";
+    return false;
+  }
+
+  const Arg& arg = *command_->positional_args[positional_arg_index_];
+
+  // Mark that we'll keep appending here until a `--` marker. When already
+  // appending this is redundant but harmless.
+  appending_to_positional_arg_ = arg.is_append;
+  if (!appending_to_positional_arg_) {
+    // If we're not continuing to append to a current positional arg,
+    // increment the positional arg index to find the next argument we
+    // should use here.
+    ++positional_arg_index_;
+  }
+
+  return ParseArg(arg, /*short_spelling=*/false, unparsed_arg);
+}
+
+auto Parser::ParseSubcommand(llvm::StringRef unparsed_arg) -> bool {
+  auto subcommand_it = subcommand_map_.find(unparsed_arg);
+  if (subcommand_it == subcommand_map_.end()) {
+    errors_ << "ERROR: Invalid subcommand '" << unparsed_arg
+            << "'. Available subcommands: ";
+    error_meta_printer_.PrintSubcommands(*command_);
+    errors_ << "\n";
+    return false;
+  }
+
+  // Before we recurse into the subcommand, verify that all the required
+  // arguments for this command were in fact parsed.
+  if (!FinalizeParsedOptions()) {
+    return false;
+  }
+
+  // Recurse into the subcommand, tracking the active command.
+  command_ = subcommand_it->second;
+  PopulateMaps(*command_);
+  return true;
+}
+
+auto Parser::FinalizeParse() -> ParseResult {
+  // If an argument action is provided, we run that and consider the parse
+  // meta-successful rather than verifying required arguments were provided and
+  // the (sub)command action.
+  if (arg_meta_action_) {
+    arg_meta_action_();
+    return ParseResult::MetaSuccess;
+  }
+
+  // Verify we're not missing any arguments.
+  if (!FinalizeParsedOptions()) {
+    return ParseResult::Error;
+  }
+
+  // If we were appending to a positional argument, mark that as complete.
+  llvm::ArrayRef positional_args = command_->positional_args;
+  if (appending_to_positional_arg_) {
+    CARBON_CHECK(static_cast<size_t>(positional_arg_index_) <
+                 positional_args.size())
+        << "Appending to a positional argument with an invalid index: "
+        << positional_arg_index_;
+    ++positional_arg_index_;
+  }
+
+  // See if any positional args are required and unparsed.
+  auto unparsed_positional_args = positional_args.slice(positional_arg_index_);
+  if (!unparsed_positional_args.empty()) {
+    // There are un-parsed positional arguments, make sure they aren't required.
+    const Arg& missing_arg = *unparsed_positional_args.front();
+    if (missing_arg.is_required) {
+      errors_ << "ERROR: Not all required positional arguments were provided. "
+                 "First missing and required positional argument: '"
+              << missing_arg.info.name << "'\n";
+      return ParseResult::Error;
+    }
+    for (const auto& arg_ptr : unparsed_positional_args) {
+      CARBON_CHECK(!arg_ptr->is_required)
+          << "Cannot have required positional parameters after an optional "
+             "one.";
+    }
+  }
+
+  switch (command_->kind) {
+    case Command::Kind::Invalid:
+      CARBON_FATAL() << "Should never have a parser with an invalid command!";
+    case Command::Kind::RequiresSubcommand:
+      errors_ << "ERROR: No subcommand specified. Available subcommands: ";
+      error_meta_printer_.PrintSubcommands(*command_);
+      errors_ << "\n";
+      return ParseResult::Error;
+    case Command::Kind::Action:
+      // All arguments have been successfully parsed, run any action for the
+      // most specific selected command. Only the leaf command's action is run.
+      command_->action();
+      return ParseResult::Success;
+    case Command::Kind::MetaAction:
+      command_->action();
+      return ParseResult::MetaSuccess;
+  }
+}
+
+auto Parser::ParsePositionalSuffix(
+    llvm::ArrayRef<llvm::StringRef> unparsed_args) -> bool {
+  CARBON_CHECK(!command_->positional_args.empty())
+      << "Cannot do positional suffix parsing without positional arguments!";
+  CARBON_CHECK(!unparsed_args.empty() && unparsed_args.front() == "--")
+      << "Must be called with a suffix of arguments starting with a `--` that "
+         "switches to positional suffix parsing.";
+  // Once we're in the positional suffix, we can track empty positional
+  // arguments.
+  bool empty_positional = false;
+  while (!unparsed_args.empty()) {
+    llvm::StringRef unparsed_arg = unparsed_args.front();
+    unparsed_args = unparsed_args.drop_front();
+
+    if (unparsed_arg != "--") {
+      if (!ParsePositionalArg(unparsed_arg)) {
+        return false;
+      }
+      empty_positional = false;
+      continue;
+    }
+
+    if (appending_to_positional_arg_ || empty_positional) {
+      ++positional_arg_index_;
+      if (static_cast<size_t>(positional_arg_index_) >=
+          command_->positional_args.size()) {
+        errors_
+            << "ERROR: Completed parsing all "
+            << command_->positional_args.size()
+            << " configured positional arguments, but found a subsequent `--` "
+               "and have no further positional arguments to parse beyond it.\n";
+        return false;
+      }
+    }
+    appending_to_positional_arg_ = false;
+    empty_positional = true;
+  }
+
+  return true;
+}
+
+Parser::Parser(llvm::raw_ostream& out, llvm::raw_ostream& errors,
+               const CommandInfo& command_info,
+               llvm::function_ref<void(CommandBuilder&)> build)
+    : meta_printer_(out),
+      errors_(errors),
+      error_meta_printer_(errors),
+      root_command_(command_info) {
+  // Run the command building lambda on a builder for the root command.
+  CommandBuilder builder(root_command_, meta_printer_);
+  build(builder);
+  builder.Finalize();
+  command_ = &root_command_;
+}
+
+auto Parser::Parse(llvm::ArrayRef<llvm::StringRef> unparsed_args)
+    -> ParseResult {
+  PopulateMaps(*command_);
+
+  while (!unparsed_args.empty()) {
+    llvm::StringRef unparsed_arg = unparsed_args.front();
+
+    // Peak at the front for an exact `--` argument that switches to a
+    // positional suffix parsing without dropping this argument.
+    if (unparsed_arg == "--") {
+      if (command_->positional_args.empty()) {
+        errors_ << "ERROR: Cannot meaningfully end option and subcommand "
+                   "arguments with a `--` argument when there are no "
+                   "positional arguments to parse.\n";
+        return ParseResult::Error;
+      }
+      if (static_cast<size_t>(positional_arg_index_) >=
+          command_->positional_args.size()) {
+        errors_ << "ERROR: Switched to purely positional arguments with a `--` "
+                   "argument despite already having parsed all positional "
+                   "arguments for this command.\n";
+        return ParseResult::Error;
+      }
+      if (!ParsePositionalSuffix(unparsed_args)) {
+        return ParseResult::Error;
+      }
+      // No more unparsed arguments to handle.
+      break;
+    }
+
+    // Now that we're not switching parse modes, drop the current unparsed
+    // argument and parse it.
+    unparsed_args = unparsed_args.drop_front();
+
+    if (unparsed_arg.starts_with("--")) {
+      // Note that the exact argument "--" has been handled above already.
+      if (!ParseLongOption(unparsed_arg)) {
+        return ParseResult::Error;
+      }
+      continue;
+    }
+
+    if (unparsed_arg.starts_with("-") && unparsed_arg.size() > 1) {
+      if (!ParseShortOptionSeq(unparsed_arg)) {
+        return ParseResult::Error;
+      }
+      continue;
+    }
+
+    CARBON_CHECK(command_->positional_args.empty() ||
+                 command_->subcommands.empty())
+        << "Cannot have both positional arguments and subcommands!";
+    if (command_->positional_args.empty() && command_->subcommands.empty()) {
+      errors_ << "ERROR: Found unexpected positional argument or subcommand: '"
+              << unparsed_arg << "'\n";
+      return ParseResult::Error;
+    }
+
+    if (!command_->positional_args.empty()) {
+      if (!ParsePositionalArg(unparsed_arg)) {
+        return ParseResult::Error;
+      }
+      continue;
+    }
+    if (!ParseSubcommand(unparsed_arg)) {
+      return ParseResult::Error;
+    }
+  }
+
+  return FinalizeParse();
+}
+
+void ArgBuilder::Required(bool is_required) { arg_.is_required = is_required; }
+
+void ArgBuilder::HelpHidden(bool is_help_hidden) {
+  arg_.is_help_hidden = is_help_hidden;
+}
+
+ArgBuilder::ArgBuilder(Arg& arg) : arg_(arg) {}
+
+void FlagBuilder::Default(bool flag_value) {
+  arg_.has_default = true;
+  arg_.default_flag = flag_value;
+}
+
+void FlagBuilder::Set(bool* flag) { arg_.flag_storage = flag; }
+
+void IntegerArgBuilder::Default(int integer_value) {
+  arg_.has_default = true;
+  arg_.default_integer = integer_value;
+}
+
+void IntegerArgBuilder::Set(int* integer) {
+  arg_.is_append = false;
+  arg_.integer_storage = integer;
+}
+
+void IntegerArgBuilder::Append(llvm::SmallVectorImpl<int>* sequence) {
+  arg_.is_append = true;
+  arg_.integer_sequence = sequence;
+}
+
+void StringArgBuilder::Default(llvm::StringRef string_value) {
+  arg_.has_default = true;
+  arg_.default_string = string_value;
+}
+
+void StringArgBuilder::Set(llvm::StringRef* string) {
+  arg_.is_append = false;
+  arg_.string_storage = string;
+}
+
+void StringArgBuilder::Append(
+    llvm::SmallVectorImpl<llvm::StringRef>* sequence) {
+  arg_.is_append = true;
+  arg_.string_sequence = sequence;
+}
+
+static auto IsValidName(llvm::StringRef name) -> bool {
+  if (name.size() <= 1) {
+    return false;
+  }
+  if (!llvm::isAlnum(name.front())) {
+    return false;
+  }
+  if (!llvm::isAlnum(name.back())) {
+    return false;
+  }
+  for (char c : name.drop_front().drop_back()) {
+    if (c != '-' && c != '_' && !llvm::isAlnum(c)) {
+      return false;
+    }
+  }
+  // We disallow names starting with "no-" as we will parse those for boolean
+  // flags.
+  return !name.starts_with("no-");
+}
+
+void CommandBuilder::AddFlag(const ArgInfo& info,
+                             llvm::function_ref<void(FlagBuilder&)> build) {
+  FlagBuilder builder(AddArgImpl(info, Arg::Kind::Flag));
+  // All boolean flags have an implicit default of `false`, although it can be
+  // overridden in the build callback.
+  builder.Default(false);
+  build(builder);
+}
+
+void CommandBuilder::AddIntegerOption(
+    const ArgInfo& info, llvm::function_ref<void(IntegerArgBuilder&)> build) {
+  IntegerArgBuilder builder(AddArgImpl(info, Arg::Kind::Integer));
+  build(builder);
+}
+
+void CommandBuilder::AddStringOption(
+    const ArgInfo& info, llvm::function_ref<void(StringArgBuilder&)> build) {
+  StringArgBuilder builder(AddArgImpl(info, Arg::Kind::String));
+  build(builder);
+}
+
+void CommandBuilder::AddOneOfOption(
+    const ArgInfo& info, llvm::function_ref<void(OneOfArgBuilder&)> build) {
+  OneOfArgBuilder builder(AddArgImpl(info, Arg::Kind::OneOf));
+  build(builder);
+}
+
+void CommandBuilder::AddMetaActionOption(
+    const ArgInfo& info, llvm::function_ref<void(ArgBuilder&)> build) {
+  ArgBuilder builder(AddArgImpl(info, Arg::Kind::MetaActionOnly));
+  build(builder);
+}
+
+void CommandBuilder::AddIntegerPositionalArg(
+    const ArgInfo& info, llvm::function_ref<void(IntegerArgBuilder&)> build) {
+  AddPositionalArgImpl(info, Arg::Kind::Integer, [build](Arg& arg) {
+    IntegerArgBuilder builder(arg);
+    build(builder);
+  });
+}
+
+void CommandBuilder::AddStringPositionalArg(
+    const ArgInfo& info, llvm::function_ref<void(StringArgBuilder&)> build) {
+  AddPositionalArgImpl(info, Arg::Kind::String, [build](Arg& arg) {
+    StringArgBuilder builder(arg);
+    build(builder);
+  });
+}
+
+void CommandBuilder::AddOneOfPositionalArg(
+    const ArgInfo& info, llvm::function_ref<void(OneOfArgBuilder&)> build) {
+  AddPositionalArgImpl(info, Arg::Kind::OneOf, [build](Arg& arg) {
+    OneOfArgBuilder builder(arg);
+    build(builder);
+  });
+}
+
+void CommandBuilder::AddSubcommand(
+    const CommandInfo& info, llvm::function_ref<void(CommandBuilder&)> build) {
+  CARBON_CHECK(IsValidName(info.name))
+      << "Invalid subcommand name: " << info.name;
+  CARBON_CHECK(subcommand_names_.insert(info.name).second)
+      << "Added a duplicate subcommand: " << info.name;
+  CARBON_CHECK(command_.positional_args.empty())
+      << "Cannot add subcommands to a command with a positional argument.";
+
+  command_.subcommands.emplace_back(new Command(info, &command_));
+  CommandBuilder builder(*command_.subcommands.back(), meta_printer_);
+  build(builder);
+  builder.Finalize();
+}
+
+void CommandBuilder::HelpHidden(bool is_help_hidden) {
+  command_.is_help_hidden = is_help_hidden;
+}
+
+void CommandBuilder::RequiresSubcommand() {
+  CARBON_CHECK(!command_.subcommands.empty())
+      << "Cannot require subcommands unless there are subcommands.";
+  CARBON_CHECK(command_.positional_args.empty())
+      << "Cannot require subcommands and have a positional argument.";
+  CARBON_CHECK(command_.kind == Kind::Invalid)
+      << "Already established the kind of this command as: " << command_.kind;
+  command_.kind = Kind::RequiresSubcommand;
+}
+
+void CommandBuilder::Do(ActionT action) {
+  CARBON_CHECK(command_.kind == Kind::Invalid)
+      << "Already established the kind of this command as: " << command_.kind;
+  command_.kind = Kind::Action;
+  command_.action = std::move(action);
+}
+
+void CommandBuilder::Meta(ActionT action) {
+  CARBON_CHECK(command_.kind == Kind::Invalid)
+      << "Already established the kind of this command as: " << command_.kind;
+  command_.kind = Kind::MetaAction;
+  command_.action = std::move(action);
+}
+
+CommandBuilder::CommandBuilder(Command& command, MetaPrinter& meta_printer)
+    : command_(command), meta_printer_(meta_printer) {}
+
+auto CommandBuilder::AddArgImpl(const ArgInfo& info, Arg::Kind kind) -> Arg& {
+  CARBON_CHECK(IsValidName(info.name))
+      << "Invalid argument name: " << info.name;
+  CARBON_CHECK(arg_names_.insert(info.name).second)
+      << "Added a duplicate argument name: " << info.name;
+
+  command_.options.emplace_back(new Arg(info));
+  Arg& arg = *command_.options.back();
+  arg.kind = kind;
+  return arg;
+}
+
+void CommandBuilder::AddPositionalArgImpl(
+    const ArgInfo& info, Arg::Kind kind, llvm::function_ref<void(Arg&)> build) {
+  CARBON_CHECK(IsValidName(info.name))
+      << "Invalid argument name: " << info.name;
+  CARBON_CHECK(command_.subcommands.empty())
+      << "Cannot add a positional argument to a command with subcommands.";
+
+  command_.positional_args.emplace_back(new Arg(info));
+  Arg& arg = *command_.positional_args.back();
+  arg.kind = kind;
+  build(arg);
+
+  CARBON_CHECK(!arg.is_help_hidden)
+      << "Cannot have a help-hidden positional argument.";
+
+  if (arg.is_required && command_.positional_args.size() > 1) {
+    CARBON_CHECK((*std::prev(command_.positional_args.end(), 2))->is_required)
+        << "A required positional argument cannot be added after an optional "
+           "one.";
+  }
+}
+
+void CommandBuilder::Finalize() {
+  meta_printer_.RegisterWithCommand(command_, *this);
+}
+
+auto Parse(llvm::ArrayRef<llvm::StringRef> unparsed_args,
+           llvm::raw_ostream& out, llvm::raw_ostream& errors,
+           const CommandInfo& command_info,
+           llvm::function_ref<void(CommandBuilder&)> build) -> ParseResult {
+  // Build a parser, which includes building the command description provided by
+  // the user.
+  Parser parser(out, errors, command_info, build);
+
+  // Now parse the arguments provided using that parser.
+  return parser.Parse(unparsed_args);
+}
+
+}  // namespace Carbon::CommandLine

+ 856 - 0
common/command_line.h

@@ -0,0 +1,856 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_COMMON_COMMAND_LINE_H_
+#define CARBON_COMMON_COMMAND_LINE_H_
+
+#include <initializer_list>
+#include <memory>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+#include "common/check.h"
+#include "common/ostream.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/PointerIntPair.h"
+#include "llvm/ADT/STLForwardCompat.h"
+#include "llvm/ADT/Sequence.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Allocator.h"
+
+// # Command-line argument parsing library.
+//
+// This is a collection of tools to describe both simple and reasonably complex
+// command line interfaces, and parse arguments based on those descriptions. It
+// optimizes for command line tools that will parse arguments exactly once per
+// execution, and specifically tools that make heavy use of subcommand-style
+// command line interfaces.
+//
+// ## Terminology used by this library
+//
+// _Argument_ or _arg_: One of the parsed components of the command line.
+//
+// _Option_: A _named_ argument starting with `--` to configure some aspect of
+// the tool. These often have both long spellings that start with `--` and
+// short, single character spellings that start with `-` and can be bundled with
+// other single character options.
+//
+// _Flag_: A boolean or binary option. These can only be enabled or disabled.
+//
+// _Positional argument_: An argument that is not named but is identified based
+// on the order in which it is encountered in the command line, with options
+// removed. Only a leaf command can contain positional arguments.
+//
+// _Value_: The value parameter provided to an argument. For options, this is
+// provided after an `=` and the option name. For positional arguments, the
+// value is the only thing provided. The string of the value is parsed according
+// to the kind of argument with fairly simple rules. See the argument builders
+// for more details.
+//
+// _Command_: The container of options, subcommands, and positional arguments to
+// parse and an action to take when successful.
+//
+// _Leaf command_: A command that doesn't contain subcommands. This is the only
+// kind of command that can contain positional arguments.
+//
+// _Subcommand_: A command nested within another command and identified by a
+// specific name that ends the parsing of arguments based on the parent and
+// switches to parse based on the specific subcommand's options and positional
+// arguments. A command with subcommands cannot parse positional arguments.
+//
+// _Action_: An open-ended callback, typically reflecting a specific subcommand
+// being parsed. Can either directly perform the operation or simply mark which
+// operation was selected.
+//
+// _Meta action_: An action fully handled by argument parsing such as displaying
+// help, version information, or completion.
+//
+// Example command to illustrate the different components:
+//
+//     git --no-pager clone --shared --filter=blob:none my-repo my-directory
+//
+// This command breaks down as:
+// - `git`: The top-level command.
+// - `--no-pager`: A negated flag on the top-level command (`git`).
+// - `clone`: A subcommand.
+// - `--shared`: A positive flag for the subcommand (`clone`).
+// - `--filter=blob:none`: An option named `filter` with a value `blob:none`.
+// - `my-repo`: The first positional argument to the subcommand.
+// - `my-directory`: the second positional argument to the subcommand.
+//
+// **Note:** while the example uses a `git` command to be relatively familiar
+// and well documented, this library does not support the same flag syntaxes as
+// `git` or use anything similar for its subcommand dispatch. This example is
+// just to help clarify the terminology used, and carefully chosen to only use a
+// syntax that overlaps with this library's parsed syntax.
+//
+// ## Options and flags
+//
+// The option syntax and behavior supported by this library is designed to be
+// strict and relatively simple while still supporting a wide range of expected
+// use cases:
+//
+// - All options must have a unique long name that is accessed with a `--`
+//   prefix. The name must consist of characters in the set [-a-zA-Z0-9], and it
+//   must not start with a `-` or `no-`.
+//
+// - Values are always attached using an `=` after the name. Only a few simple
+//   value formats are supported:
+//   - Arbitrary strings
+//   - Integers as parsed by `llvm::StringRef` and whose value fits in an
+//     `int`.
+//   - One of a fixed set of strings
+//
+// - Options may be parsed multiple times, and the behavior can be configured:
+//   - Each time, they can set a new value, overwriting any previous.
+//   - They can append the value to a container.
+//   - TODO: They can increment a count.
+//
+// - Options may have a default value that will be synthesized even if they do
+//   not occur in the parsed command line.
+//
+// - Flags (boolean options) have some special rules.
+//   - They may be spelled normally, and default to setting that flag to `true`.
+//   - They may also accept a value of either exactly `true` or `false` and that
+//     is the value.
+//   - They may be spelled with a `no-` prefix, such as `--no-verbose`, and that
+//     is exactly equivalent to `--verbose=false`.
+//   - For flags with a default `true` value, they are rendered in help using
+//     the `no-` prefix.
+//
+// - Options may additionally have a short name of a single character [a-zA-Z].
+//   - There is no distinction between the behavior of long and short names.
+//   - The short name can only specify the positive or `true` value for flags.
+//     There is no negative form of short names.
+//   - Short names are parsed after a single `-`, such as `-v`.
+//   - Any number of short names for boolean flags or options with default
+//     values may be grouped after `-`, such as `-xyz`: this is three options,
+//     `x`, `y`, and `z`.
+//   - Short options may include a value after an `=`, but not when grouped with
+//     other short options.
+//
+// - Options are parsed from any argument until either a subcommand switches to
+//   that subcommand's options or a `--` argument ends all option parsing to
+//   allow arguments that would be ambiguous with the option syntax.
+//
+// ## Subcommands
+//
+// Subcommands can be nested to any depth, and each subcommand gets its own
+// option space.
+//
+// ## Positional arguments
+//
+// Leaf commands (and subcommands) can accept positional arguments. These work
+// similar to options but consist *only* of the value. They have names, but the
+// name is only used in documentation and error messages. Except for the special
+// case of the exact argument `-`, positional argument values cannot start with
+// a `-` character until after option parsing is ended with a `--` argument.
+//
+// Singular positional arguments store the single value in that position.
+// Appending positional arguments append all of the values in sequence.
+//
+// Multiple positional arguments to a single command are parsed in sequence. For
+// appending positional arguments, the sequence of values appended is ended with
+// a `--` argument. For example, a command that takes two sequences of
+// positional arguments might look like:
+//
+//     my-diff-tool a.txt b.txt c.txt -- new-a.txt new-b.txt new-c.txt
+//
+// The `--` used in this way *both* ends option parsing and the positional
+// argument sequence. Note that if there are no positional arguments prior to
+// the first `--`, then it will just end option parsing. To pass an empty
+// sequence of positional arguments two `--` arguments would be required. Once
+// option parsing is ended, even a single positional argument can be skipped
+// using a `--` argument without a positional argument.
+//
+// ## Help text blocks and rendering
+//
+// At many points in this library, a block of text is specified as a string.
+// These should be written using multi-line raw string literals that start on
+// their own line such as:
+//
+// ```cpp
+//     ...
+//     .help = R"""(
+// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+// tempor incididunt ut labore et dolore magna aliqua.
+// )""",
+// ```
+//
+// The code using these blocks will remove leading and trailing newlines. TODO:
+// It should also reflow all of the text in a way that tries to be roughly
+// similar to how Markdown would be reflowed when rendered:
+//
+// - Blank lines will be preserved.
+// - (TODO) A best effort to detect lists and other common Markdown constructs
+//   and preserve the newlines between those.
+// - (TODO) Fenced regions will be preserved exactly.
+//
+// The remaining blocks will have all newlines removed when there is no column
+// width information on the output stream, or will be re-flowed to the output
+// stream's column width when available.
+//
+// ## The `Args` class
+//
+// The `Args` class is not a meaningful type, it serves two purposes: to give a
+// structured name prefix to the inner types, and to allow marking many
+// components `private` and using `friend` to grant access to implementation
+// details.
+//
+// ## Roadmap / TODOs / planned future work
+//
+// - Detect column width when the stream is a terminal and re-flow text.
+// - Implement short help display mode (and enable that by default).
+// - Add help text to one-of values and render it when present.
+// - Add formatting when the stream supports it (colors, bold, etc).
+// - Improve error messages when parsing fails.
+//   - Reliably display the most appropriate usage string.
+//   - Suggest typo corrections?
+//   - Print snippet or otherwise render the failure better?
+// - Add support for responding to shell autocomplete queries.
+// - Finish adding support for setting and printing version information.
+// - Add short option counting support (`-vvv` -> `--verbose=3`).
+//
+namespace Carbon::CommandLine {
+
+// Forward declare some implementation detail classes and classes that are
+// friended.
+struct Arg;
+struct Command;
+class MetaPrinter;
+class Parser;
+class CommandBuilder;
+
+// The result of parsing arguments can be a parse error, a successfully parsed
+// command line, or a meta-success due to triggering a meta-action during the
+// parse such as rendering help text.
+enum class ParseResult {
+  // Signifies an error parsing arguments. It will have been diagnosed using
+  // the streams provided to the parser, and no useful parsed arguments are
+  // available.
+  Error,
+
+  // Signifies that program arguments were successfully parsed and can be
+  // used.
+  Success,
+
+  // Signifies a successful meta operation such as displaying help text
+  // was performed. No parsed arguments are available, and the side-effects
+  // have been directly provided via the streams provided to the parser.
+  MetaSuccess,
+};
+auto operator<<(llvm::raw_ostream& output, ParseResult result)
+    -> llvm::raw_ostream&;
+
+// Actions are stored in data structures so we use an owning closure to model
+// them.
+using ActionT = std::function<void()>;
+
+// The core argument info used to render help and other descriptive
+// information. This is used for both options and positional arguments.
+// Commands use an extended version below.
+struct ArgInfo {
+  // The name of the argument. For options, this is the long name parsed after
+  // `--`. Option names must also be unique. Conventionally, these are spelled
+  // in lower case with a single dash separating words as that tends to be
+  // especially easy to type and reasonably easy to read.
+  //
+  // For positional arguments this is only used for help text, and is often
+  // spelled with `ALL-CAPS` to make it clear that it is a placeholder name
+  // and does not appear in the actual command line.
+  llvm::StringRef name;
+
+  // Optional short name for options. This must consist of a single character
+  // in the set [a-zA-Z].
+  llvm::StringRef short_name = "";
+
+  // For options, it is sometimes useful to render a distinct value
+  // placeholder name to help clarify what the value should contain. These are
+  // often in `ALL-CAPS` to make it clear they are placeholders. For example,
+  // an `output` option might set this to `FILE` so it renders as
+  // `--output=FILE`.
+  llvm::StringRef value_name = "";
+
+  // A long-form help string that will be rendered for this argument in
+  // command help strings. It supports multiple lines and is rendered as a
+  // text block described in the `Args` top-level comment.
+  llvm::StringRef help = "";
+
+  // A short help string for the argument. This should be as short as
+  // possible, and always fit easily on a single line. Ideally, it can fit in
+  // a narrow column of a single line and is in the range of 40 columns wide.
+  //
+  // If not provided, the first paragraph (up to a blank line) from `help`
+  // will be used.
+  llvm::StringRef help_short = "";
+};
+
+// The kinds of arguments that can be parsed.
+enum class ArgKind {
+  Invalid,
+  Flag,
+  Integer,
+  String,
+  OneOf,
+  MetaActionOnly,
+};
+auto operator<<(llvm::raw_ostream& output, ArgKind kind) -> llvm::raw_ostream&;
+
+// A builder used to configure an argument for parsing.
+class ArgBuilder {
+ public:
+  // When marked as required, if an argument is not provided explicitly in the
+  // command line the parse will produce an error.
+  void Required(bool is_required);
+
+  // An argument can be hidden from the help output.
+  void HelpHidden(bool is_help_hidden);
+
+  // Sets a meta-action to run when this argument is parsed. This is used to
+  // set up arguments like `--help` or `--version` that can be entirely
+  // handled during parsing and rather than produce parsed information about
+  // the command line, override that for some custom behavior.
+  template <typename T>
+  void MetaAction(T action);
+
+ protected:
+  friend CommandBuilder;
+  explicit ArgBuilder(Arg& arg);
+
+  Arg& arg_;
+};
+
+// Customized argument builder when the value is a boolean flag.
+class FlagBuilder : public ArgBuilder {
+ public:
+  // Flags can be defaulted to true. However, flags always have *some*
+  // default, this merely customizes which value is default. If uncustomized,
+  // the default of a flag is false.
+  void Default(bool flag_value);
+
+  // Configures the argument to store a parsed value in the provided storage.
+  //
+  // This must be called on the builder.
+  void Set(bool* flag);
+
+ private:
+  using ArgBuilder::ArgBuilder;
+};
+
+// Customized argument builder when the value is an integer.
+class IntegerArgBuilder : public ArgBuilder {
+ public:
+  // Sets a default for the argument with the provided value. There is no
+  // default for integer arguments unless this is called.
+  //
+  // For arguments that are `Set` below, this value will be stored even if the
+  // argument is never explicitly provided. For arguments that are `Append`ed
+  // below, this value will be used whenever the argument occurs without an
+  // explicit value, but unless the argument is parsed nothing will be
+  // appended.
+  void Default(int integer_value);
+
+  // Configures the argument to store a parsed value in the provided storage.
+  // Each time the argument is parsed, it will write a new value to this
+  // storage.
+  //
+  // Exactly one of this method or `Append` below must be configured for the
+  // argument.
+  void Set(int* integer);
+
+  // Configures the argument to append a parsed value to the provided
+  // container. Each time the argument is parsed, a new value will be
+  // appended.
+  //
+  // Exactly one of this method or `Set` above must be configured for the
+  // argument.
+  void Append(llvm::SmallVectorImpl<int>* sequence);
+
+ private:
+  using ArgBuilder::ArgBuilder;
+};
+
+// Customized argument builder when the value is a string.
+class StringArgBuilder : public ArgBuilder {
+ public:
+  // Sets a default for the argument with the provided value. There is no
+  // default for string arguments unless this is called.
+  //
+  // For arguments that are `Set` below, this value will be stored even if the
+  // argument is never explicitly provided. For arguments that are `Append`ed
+  // below, this value will be used whenever the argument occurs without an
+  // explicit value, but unless the argument is parsed nothing will be
+  // appended.
+  void Default(llvm::StringRef string_value);
+
+  // Configures the argument to store a parsed value in the provided storage.
+  // Each time the argument is parsed, it will write a new value to this
+  // storage.
+  //
+  // Exactly one of this method or `Append` below must be configured for the
+  // argument.
+  void Set(llvm::StringRef* string);
+
+  // Configures the argument to append a parsed value to the provided
+  // container. Each time the argument is parsed, a new value will be
+  // appended.
+  //
+  // Exactly one of this method or `Set` above must be configured for the
+  // argument.
+  void Append(llvm::SmallVectorImpl<llvm::StringRef>* sequence);
+
+ private:
+  using ArgBuilder::ArgBuilder;
+};
+
+// Customized argument builder when the value is required to be one of a fixed
+// set of possible strings, and each one maps to a specific value of some
+// type, often an enumerator.
+class OneOfArgBuilder : public ArgBuilder {
+ public:
+  // A tiny helper / builder type for describing one of the possible values
+  // that a particular argument accepts.
+  //
+  // Beyond carrying the string, type, and value for this candidate, it also
+  // allows marking the candidate as the default.
+  template <typename T>
+  class OneOfValueT {
+   public:
+    // Configure whether a candidate is the default. If not called, it is not
+    // the default. This can only be used when setting, not when appending.
+    auto Default(bool is_default) && -> OneOfValueT;
+
+   private:
+    friend OneOfArgBuilder;
+
+    explicit OneOfValueT(llvm::StringRef str, T value);
+
+    llvm::StringRef str;
+    T value;
+    bool is_default = false;
+  };
+
+  // A helper function to create an object that models one of the candidate
+  // values for this argument. It takes the string used to select this value
+  // on the command line, deduces the type of the value, and accepts the value
+  // itself that should be used when this candidate is parsed.
+  template <typename T>
+  static auto OneOfValue(llvm::StringRef string, T value) -> OneOfValueT<T>;
+
+  // Configures the argument to store a parsed value in the provided storage.
+  // Each time the argument is parsed, it will write a new value to this
+  // storage.
+  //
+  // Exactly one of this method or `AppendOneOf` below must be configured for
+  // the argument.
+  //
+  // For one-of arguments, this method also provides an array of possible
+  // values that may be used:
+  //
+  // ```cpp
+  //   arg_b.SetOneOf(
+  //       {
+  //           arg_b.OneOfValue("x", 1),
+  //           arg_b.OneOfValue("y", 2),
+  //           arg_b.OneOfValue("z", 3).Default(true),
+  //       },
+  //       &value);
+  // ```
+  //
+  // The only value strings that will be parsed are those described in the
+  // array, and the value stored in the storage for a particular parsed value
+  // string is the one associated with it. There must be a single homogeneous
+  // type for the all of the candidate values and that type must be storable
+  // into the result storage pointee type. At most one of the array can also
+  // be marked as a default value, which will make specifying a value
+  // optional, and also will cause that value to be stored into the result
+  // even if the argument is not parsed explicitly.
+  template <typename T, typename U, size_t N>
+  void SetOneOf(const OneOfValueT<U> (&values)[N], T* result);
+
+  // Configures the argument to append a parsed value to the provided
+  // container. Each time the argument is parsed, a new value will be
+  // appended.
+  //
+  // Exactly one of this method or `SetOneOf` above must be configured for the
+  // argument.
+  //
+  // Similar to `SetOneOf`, this must also describe the candidate value
+  // strings that can be parsed and the consequent values that are used for
+  // those strings:
+  //
+  // ```cpp
+  //   arg_b.AppendOneOf(
+  //       {
+  //           arg_b.OneOfValue("x", 1),
+  //           arg_b.OneOfValue("y", 2),
+  //           arg_b.OneOfValue("z", 3),
+  //       },
+  //       &values);
+  // ```
+  //
+  // Instead of storing, these values are appended.
+  //
+  // However, appending one-of arguments cannot use a default. The values must
+  // always be explicitly parsed.
+  template <typename T, typename U, size_t N>
+  void AppendOneOf(const OneOfValueT<U> (&values)[N],
+                   llvm::SmallVectorImpl<T>* sequence);
+
+ private:
+  using ArgBuilder::ArgBuilder;
+
+  template <typename U, size_t N, typename MatchT, size_t... Indices>
+  void OneOfImpl(const OneOfValueT<U> (&input_values)[N], MatchT match,
+                 std::index_sequence<Indices...> /*indices*/);
+};
+
+// The extended info for a command, including for a subcommand.
+//
+// This provides the primary descriptive information for commands used by help
+// and other diagnostic messages. For subcommands, it also provides the name
+// used to access the subcommand.
+struct CommandInfo {
+  // The name of the command. For subcommands, this is also the argument
+  // spelling that accesses this subcommand. It must consist of characters in
+  // the set [-a-zA-Z0-9], and must not start with a `-`.
+  llvm::StringRef name;
+
+  // An optional version string that will be rendered as part of version meta
+  // action by the library. While this library does not impose a machine
+  // parsable structure, users will expect this to be extractable and parsable
+  // in practice.
+  //
+  // Subcommands with an empty version will inherit the first non-empty parent
+  // version.
+  //
+  // Whether a (possibly inherited) version string is non-empty determines
+  // whether this library provides the version-printing meta actions via a
+  // `--version` flag or, if there are subcommands, a `version` subcommand.
+  llvm::StringRef version = "";
+
+  // Optional build information to include when printing the version.
+  llvm::StringRef build_info = "";
+
+  // An optional long-form help string for the command.
+  //
+  // When accessing a command's dedicated help output, this will form the main
+  // prose output at the beginning of the help message. When listing
+  // subcommands, the subcommand's `help` string will be used to describe it
+  // in the list.
+  //
+  // The help meta actions are available regardless of whether this is
+  // provided in order to mechanically describe other aspects of the command.
+  //
+  // This field supports multiple lines and uses the text block handling
+  // described in the top-level `Args` comment.
+  llvm::StringRef help = "";
+
+  // An optional short help string for the command.
+  //
+  // This should be very short, at most one line and ideally fitting within a
+  // narrow column of a line. If left blank, the first paragraph of text (up
+  // to the first blank line) in the `help` string will be used.
+  llvm::StringRef help_short = "";
+
+  // An optional custom block of text to describe the usage of the command.
+  //
+  // The usage should just be a series of lines with possible invocations of
+  // the command summarized as briefly as possible. Ideally, each variation on
+  // a single line. The goal is to show the *structure* of different command
+  // invocations, not to be comprehensive.
+  //
+  // When omitted, the library will generate these based on the parsing logic.
+  // The generated usage is expected to be suitable for the vast majority of
+  // users, but can be customized in cases where necessary.
+  llvm::StringRef usage = "";
+
+  // An optional epilogue multi-line block of text appended to the help display
+  // for this command. It is only used for this command's dedicated help, but
+  // can contain extra, custom guidance that is especially useful to have at
+  // the very end of the output.
+  llvm::StringRef help_epilogue = "";
+};
+
+// Commands are classified based on the action they result in when run.
+//
+// Commands that require a subcommand have no action and are just a means to
+// reach the subcommand actions.
+//
+// Commands with _meta_ actions are also a separate kind from those with
+// normal actions.
+enum class CommandKind {
+  Invalid,
+  RequiresSubcommand,
+  Action,
+  MetaAction,
+};
+auto operator<<(llvm::raw_ostream& output, CommandKind kind)
+    -> llvm::raw_ostream&;
+
+// Builder used to configure a command to parse.
+//
+// An instance of this is used to configure the top-level command, and a fresh
+// instance is provided for configuring each subcommand as well.
+class CommandBuilder {
+ public:
+  using Kind = CommandKind;
+
+  void AddFlag(const ArgInfo& info,
+               llvm::function_ref<void(FlagBuilder&)> build);
+  void AddIntegerOption(const ArgInfo& info,
+                        llvm::function_ref<void(IntegerArgBuilder&)> build);
+  void AddStringOption(const ArgInfo& info,
+                       llvm::function_ref<void(StringArgBuilder&)> build);
+  void AddOneOfOption(const ArgInfo& info,
+                      llvm::function_ref<void(OneOfArgBuilder&)> build);
+  void AddMetaActionOption(const ArgInfo& info,
+                           llvm::function_ref<void(ArgBuilder&)> build);
+
+  void AddIntegerPositionalArg(
+      const ArgInfo& info, llvm::function_ref<void(IntegerArgBuilder&)> build);
+  void AddStringPositionalArg(
+      const ArgInfo& info, llvm::function_ref<void(StringArgBuilder&)> build);
+  void AddOneOfPositionalArg(const ArgInfo& info,
+                             llvm::function_ref<void(OneOfArgBuilder&)> build);
+
+  void AddSubcommand(const CommandInfo& info,
+                     llvm::function_ref<void(CommandBuilder&)> build);
+
+  // Subcommands can be hidden from the help listing of their parents with
+  // this setting. Hiding a subcommand doesn't disable its own help, it just
+  // removes it from the listing.
+  void HelpHidden(bool is_help_hidden);
+
+  // Exactly one of these three should be called to select and configure the
+  // kind of the built command.
+  void RequiresSubcommand();
+  void Do(ActionT action);
+  void Meta(ActionT meta_action);
+
+ private:
+  friend Parser;
+
+  explicit CommandBuilder(Command& command, MetaPrinter& meta_printer);
+
+  auto AddArgImpl(const ArgInfo& info, ArgKind kind) -> Arg&;
+  void AddPositionalArgImpl(const ArgInfo& info, ArgKind kind,
+                            llvm::function_ref<void(Arg&)> build);
+  void Finalize();
+
+  Command& command_;
+  MetaPrinter& meta_printer_;
+
+  llvm::SmallDenseSet<llvm::StringRef> arg_names_;
+  llvm::SmallDenseSet<llvm::StringRef> subcommand_names_;
+};
+
+// Builds a description of a command and then parses the provided arguments
+// for that command.
+//
+// This is the main entry point to both build up the description of the
+// command whose arguments are being parsed and to do the parsing. Everything
+// is done in a single invocation as the common case is to build a command
+// description, parse the arguments once, and then run with that
+// configuration.
+//
+// The `out` stream is treated like `stdout` would be for a Unix-style command
+// line tool, and `errors` like `stderr`: any errors or diagnostic information
+// are printed to `errors`, but meta-actions like printing a command's help go
+// to `out`.
+auto Parse(llvm::ArrayRef<llvm::StringRef> unparsed_args,
+           llvm::raw_ostream& out, llvm::raw_ostream& errors,
+           const CommandInfo& command_info,
+           llvm::function_ref<void(CommandBuilder&)> build) -> ParseResult;
+
+// Implementation details only below.
+
+// The internal representation of a parsable argument description.
+struct Arg {
+  using Kind = ArgKind;
+  using ValueActionT =
+      std::function<bool(const Arg& arg, llvm::StringRef value_string)>;
+  using DefaultActionT = std::function<void(const Arg& arg)>;
+
+  explicit Arg(const ArgInfo& info);
+  ~Arg();
+
+  ArgInfo info;
+  Kind kind = Kind::Invalid;
+  bool has_default = false;
+  bool is_required = false;
+  bool is_append = false;
+  bool is_help_hidden = false;
+
+  // Meta action storage, only populated if this argument causes a meta action.
+  ActionT meta_action;
+
+  // Storage options depending on the kind.
+  union {
+    // Singular argument storage pointers.
+    bool* flag_storage;
+    int* integer_storage;
+    llvm::StringRef* string_storage;
+
+    // Appending argument storage pointers.
+    llvm::SmallVectorImpl<int>* integer_sequence;
+    llvm::SmallVectorImpl<llvm::StringRef>* string_sequence;
+
+    // One-of information.
+    struct {
+      llvm::OwningArrayRef<llvm::StringRef> value_strings;
+      ValueActionT value_action;
+    };
+  };
+
+  // Default values depending on the kind.
+  union {
+    bool default_flag;
+    int default_integer;
+    llvm::StringRef default_string;
+    struct {
+      DefaultActionT default_action;
+      int default_value_index;
+    };
+  };
+};
+
+// The internal representation of a parsable command description, including its
+// options, positional arguments, and subcommands.
+struct Command {
+  using Kind = CommandBuilder::Kind;
+
+  explicit Command(const CommandInfo& info, Command* parent = nullptr);
+
+  CommandInfo info;
+  Command* parent;
+  ActionT action;
+  Kind kind = Kind::Invalid;
+
+  bool is_help_hidden = false;
+
+  llvm::SmallVector<std::unique_ptr<Arg>> options;
+  llvm::SmallVector<std::unique_ptr<Arg>> positional_args;
+  llvm::SmallVector<std::unique_ptr<Command>> subcommands;
+};
+
+template <typename T>
+void ArgBuilder::MetaAction(T action) {
+  CARBON_CHECK(!arg_.meta_action) << "Cannot set a meta action twice!";
+  arg_.meta_action = std::move(action);
+}
+
+template <typename T>
+auto OneOfArgBuilder::OneOfValueT<T>::Default(
+    bool is_default) && -> OneOfValueT {
+  OneOfValueT result = std::move(*this);
+  result.is_default = is_default;
+  return result;
+}
+
+template <typename T>
+OneOfArgBuilder::OneOfValueT<T>::OneOfValueT(llvm::StringRef str, T value)
+    : str(str), value(std::move(value)) {}
+
+template <typename T>
+auto OneOfArgBuilder::OneOfValue(llvm::StringRef str, T value)
+    -> OneOfValueT<T> {
+  return OneOfValueT<T>(str, value);
+}
+
+template <typename T, typename U, size_t N>
+void OneOfArgBuilder::SetOneOf(const OneOfValueT<U> (&values)[N], T* result) {
+  static_assert(N > 0, "Must include at least one value.");
+  arg_.is_append = false;
+  OneOfImpl(
+      values, [result](T value) { *result = value; },
+      std::make_index_sequence<N>{});
+}
+
+template <typename T, typename U, size_t N>
+void OneOfArgBuilder::AppendOneOf(const OneOfValueT<U> (&values)[N],
+                                  llvm::SmallVectorImpl<T>* sequence) {
+  static_assert(N > 0, "Must include at least one value.");
+  arg_.is_append = true;
+  OneOfImpl(
+      values, [sequence](T value) { sequence->push_back(value); },
+      std::make_index_sequence<N>{});
+}
+
+// An implementation tool for the one-of value candidate handling. Delegating to
+// this allows us to deduce a pack of indices from the array of candidates, and
+// then use that variadic pack to operate on the array in the variadic space.
+// This includes packaging the components up separately into our storage
+// representation, as well as processing the array to find and register any
+// default.
+//
+// The representation is especially tricky because we want all of the actual
+// values and even the *type* of values to be erased. We do that by building
+// lambdas that do the type-aware operations and storing those into type-erased
+// function objects.
+template <typename U, size_t N, typename MatchT, size_t... Indices>
+void OneOfArgBuilder::OneOfImpl(const OneOfValueT<U> (&input_values)[N],
+                                MatchT match,
+                                std::index_sequence<Indices...> /*indices*/) {
+  std::array<llvm::StringRef, N> value_strings = {input_values[Indices].str...};
+  std::array<U, N> values = {input_values[Indices].value...};
+
+  // Directly copy the value strings into a heap-allocated array in the
+  // argument.
+  new (&arg_.value_strings)
+      llvm::OwningArrayRef<llvm::StringRef>(value_strings);
+
+  // And build a type-erased action that maps a specific value string to a value
+  // by index.
+  new (&arg_.value_action) Arg::ValueActionT(
+      [values, match](const Arg& arg, llvm::StringRef value_string) -> bool {
+        for (int i : llvm::seq<int>(0, N)) {
+          if (value_string == arg.value_strings[i]) {
+            match(values[i]);
+            return true;
+          }
+        }
+        return false;
+      });
+
+  // Fold over all the input values to see if there is a default.
+  if ((input_values[Indices].is_default || ...)) {
+    CARBON_CHECK(!arg_.is_append) << "Can't append default.";
+    CARBON_CHECK((input_values[Indices].is_default + ... + 0) == 1)
+        << "Cannot default more than one value.";
+
+    arg_.has_default = true;
+
+    // First build a lambda that configures the default using an index. We'll
+    // call this below, this lambda isn't the one that is stored.
+    auto set_default = [&](size_t index, const auto& default_value) {
+      // Now that we have the desired default index, build a lambda and store it
+      // as the default action. This lambda is stored and so it captures the
+      // necessary information explicitly and by value.
+      new (&arg_.default_action)
+          Arg::DefaultActionT([value = default_value.value,
+                               match](const Arg& /*arg*/) { match(value); });
+
+      // Also store the index itself for use when printing help.
+      arg_.default_value_index = index;
+    };
+
+    // Now we fold across the inputs and in the one case that is the default, we
+    // call the lambda. This is just a somewhat awkward way to write a loop with
+    // a condition in it over a pack.
+    ((input_values[Indices].is_default
+          ? set_default(Indices, input_values[Indices])
+          : static_cast<void>(0)),
+     ...);
+  }
+}
+
+}  // namespace Carbon::CommandLine
+
+#endif  // CARBON_COMMON_COMMAND_LINE_H_

+ 938 - 0
common/command_line_test.cpp

@@ -0,0 +1,938 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "common/command_line.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "llvm/Support/FormatVariadic.h"
+#include "testing/base/test_raw_ostream.h"
+
+namespace Carbon::Testing {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::Not;
+using ::testing::StrEq;
+
+constexpr CommandLine::CommandInfo TestCommandInfo = {
+    .name = "test",
+    .help = "A test command.",
+    .help_epilogue = "TODO",
+};
+
+enum class TestEnum {
+  Val1,
+  Val2,
+};
+
+enum class TestSubcommand {
+  Sub1,
+  Sub2,
+};
+
+TEST(ArgParserTest, BasicCommand) {
+  bool flag = false;
+  int integer_option = -1;
+  llvm::StringRef string_option = "";
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args) {
+    return CommandLine::Parse(
+        args, llvm::errs(), llvm::errs(), TestCommandInfo, [&](auto& b) {
+          b.AddFlag({.name = "flag"}, [&](auto& arg_b) { arg_b.Set(&flag); });
+          b.AddIntegerOption({.name = "option1"},
+                             [&](auto& arg_b) { arg_b.Set(&integer_option); });
+          b.AddStringOption({.name = "option2"},
+                            [&](auto& arg_b) { arg_b.Set(&string_option); });
+          b.Do([] {});
+        });
+  };
+
+  EXPECT_THAT(parse({"--flag", "--option2=value", "--option1=42"}),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(integer_option, Eq(42));
+  EXPECT_THAT(string_option, StrEq("value"));
+}
+
+TEST(ArgParserTest, BooleanFlags) {
+  bool flag = false;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddFlag({.name = "flag"}, [&](auto& arg_b) { arg_b.Set(&flag); });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({"--no-flag"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_FALSE(flag);
+
+  EXPECT_THAT(parse({"--flag", "--no-flag"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_FALSE(flag);
+
+  EXPECT_THAT(parse({"--no-flag", "--flag"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+
+  EXPECT_THAT(parse({"--no-flag", "--flag", "--flag=false"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_FALSE(flag);
+
+  EXPECT_THAT(parse({"--no-flag", "--flag=true"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+
+  EXPECT_THAT(parse({"--no-flag", "--flag=true", "--no-flag"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_FALSE(flag);
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--no-flag=true"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(
+      os.TakeStr(),
+      StrEq("ERROR: Cannot specify a value when using a flag name prefixed "
+            "with 'no-' -- that prefix implies a value of 'false'.\n"));
+
+  EXPECT_THAT(parse({"--no-flag=false"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(
+      os.TakeStr(),
+      StrEq("ERROR: Cannot specify a value when using a flag name prefixed "
+            "with 'no-' -- that prefix implies a value of 'false'.\n"));
+}
+
+TEST(ArgParserTest, ArgDefaults) {
+  bool flag = false;
+  int integer_option = -1;
+  llvm::StringRef string_option = "";
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddFlag({.name = "flag"}, [&](auto& arg_b) {
+        arg_b.Default(true);
+        arg_b.Set(&flag);
+      });
+      b.AddIntegerOption({.name = "option1"}, [&](auto& arg_b) {
+        arg_b.Default(7);
+        arg_b.Set(&integer_option);
+      });
+      b.AddStringOption({.name = "option2"}, [&](auto& arg_b) {
+        arg_b.Default("default");
+        arg_b.Set(&string_option);
+      });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({}, llvm::errs()), Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(integer_option, Eq(7));
+  EXPECT_THAT(string_option, StrEq("default"));
+
+  EXPECT_THAT(parse({"--option1", "--option2"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(integer_option, Eq(7));
+  EXPECT_THAT(string_option, StrEq("default"));
+
+  EXPECT_THAT(parse({"--option1=42", "--option2=explicit"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(integer_option, Eq(42));
+  EXPECT_THAT(string_option, StrEq("explicit"));
+
+  EXPECT_THAT(
+      parse({"--option1=42", "--option2=explicit", "--option1", "--option2"},
+            llvm::errs()),
+      Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(integer_option, Eq(7));
+  EXPECT_THAT(string_option, StrEq("default"));
+}
+
+TEST(ArgParserTest, ShortArgs) {
+  bool flag = false;
+  bool example = false;
+  int integer_option = -1;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddFlag({.name = "flag", .short_name = "f"},
+                [&](auto& arg_b) { arg_b.Set(&flag); });
+      b.AddFlag({.name = "example", .short_name = "x"},
+                [&](auto& arg_b) { arg_b.Set(&example); });
+      b.AddIntegerOption({.name = "option1", .short_name = "o"},
+                         [&](auto& arg_b) {
+                           arg_b.Default(123);
+                           arg_b.Set(&integer_option);
+                         });
+      b.AddIntegerOption({.name = "option2", .short_name = "z"},
+                         [&](auto& arg_b) { arg_b.Set(&integer_option); });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({"-f", "-o=42", "-x"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_TRUE(example);
+  EXPECT_THAT(integer_option, Eq(42));
+
+  flag = false;
+  example = false;
+  EXPECT_THAT(parse({"--option1=13", "-xfo"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_TRUE(example);
+  EXPECT_THAT(integer_option, Eq(123));
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"-xzf"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(),
+              StrEq("ERROR: Option '-z' (short for '--option2') requires a "
+                    "value to be provided and none was.\n"));
+
+  EXPECT_THAT(parse({"-xz=123"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(
+      os.TakeStr(),
+      StrEq("ERROR: Cannot provide a value to the group of multiple short "
+            "options '-xz=...'; values must be provided to a single option, "
+            "using either the short or long spelling.\n"));
+}
+
+TEST(ArgParserTest, PositionalArgs) {
+  bool flag = false;
+  llvm::StringRef string_option = "";
+  llvm::StringRef source_string;
+  llvm::StringRef dest_string;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddFlag({.name = "flag"}, [&](auto& arg_b) { arg_b.Set(&flag); });
+      b.AddStringOption({.name = "option"},
+                        [&](auto& arg_b) { arg_b.Set(&string_option); });
+      b.AddStringPositionalArg({.name = "source"}, [&](auto& arg_b) {
+        arg_b.Set(&source_string);
+        arg_b.Required(true);
+      });
+      b.AddStringPositionalArg({.name = "dest"}, [&](auto& arg_b) {
+        arg_b.Set(&dest_string);
+        arg_b.Required(true);
+      });
+      b.Do([] {});
+    });
+  };
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--flag", "--option=x"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(
+      os.TakeStr(),
+      StrEq("ERROR: Not all required positional arguments were provided. First "
+            "missing and required positional argument: 'source'\n"));
+
+  EXPECT_THAT(parse({"src", "--flag", "--option=value", "dst"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(string_option, StrEq("value"));
+  EXPECT_THAT(source_string, StrEq("src"));
+  EXPECT_THAT(dest_string, StrEq("dst"));
+
+  EXPECT_THAT(parse({"src2", "--", "dst2"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(source_string, StrEq("src2"));
+  EXPECT_THAT(dest_string, StrEq("dst2"));
+
+  EXPECT_THAT(parse({"-", "--", "-"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(source_string, StrEq("-"));
+  EXPECT_THAT(dest_string, StrEq("-"));
+}
+
+TEST(ArgParserTest, PositionalAppendArgs) {
+  bool flag = false;
+  llvm::StringRef string_option = "";
+  llvm::SmallVector<llvm::StringRef> source_strings;
+  llvm::SmallVector<llvm::StringRef> dest_strings;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddFlag({.name = "flag"}, [&](auto& arg_b) { arg_b.Set(&flag); });
+      b.AddStringOption({.name = "option"},
+                        [&](auto& arg_b) { arg_b.Set(&string_option); });
+      b.AddStringPositionalArg({.name = "sources"}, [&](auto& arg_b) {
+        arg_b.Append(&source_strings);
+        arg_b.Required(true);
+      });
+      b.AddStringPositionalArg({.name = "dest"}, [&](auto& arg_b) {
+        arg_b.Append(&dest_strings);
+        arg_b.Required(true);
+      });
+      b.Do([] {});
+    });
+  };
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--flag", "--option=x"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(
+      os.TakeStr(),
+      StrEq("ERROR: Not all required positional arguments were provided. First "
+            "missing and required positional argument: 'sources'\n"));
+
+  EXPECT_THAT(
+      parse({"src1", "--flag", "src2", "--option=value", "--", "--dst--"},
+            llvm::errs()),
+      Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(flag);
+  EXPECT_THAT(string_option, StrEq("value"));
+  EXPECT_THAT(source_strings, ElementsAre(StrEq("src1"), StrEq("src2")));
+  EXPECT_THAT(dest_strings, ElementsAre(StrEq("--dst--")));
+
+  source_strings.clear();
+  dest_strings.clear();
+  EXPECT_THAT(
+      parse({"--", "--src1--", "--src2--", "--", "dst1", "dst2"}, llvm::errs()),
+      Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(source_strings,
+              ElementsAre(StrEq("--src1--"), StrEq("--src2--")));
+  EXPECT_THAT(dest_strings, ElementsAre(StrEq("dst1"), StrEq("dst2")));
+
+  source_strings.clear();
+  dest_strings.clear();
+  EXPECT_THAT(parse({"--", "--", "dst1", "dst2"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(source_strings, ElementsAre());
+  EXPECT_THAT(dest_strings, ElementsAre(StrEq("dst1"), StrEq("dst2")));
+}
+
+TEST(ArgParserTest, BasicSubcommands) {
+  bool flag = false;
+  llvm::StringRef option1 = "";
+  llvm::StringRef option2 = "";
+
+  TestSubcommand subcommand;
+  bool subsub_command = false;
+
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddSubcommand({.name = "sub1"}, [&](auto& sub_b) {
+        sub_b.AddFlag({.name = "flag"}, [&](auto& arg_b) { arg_b.Set(&flag); });
+        sub_b.AddStringOption({.name = "option"},
+                              [&](auto& arg_b) { arg_b.Set(&option1); });
+        sub_b.Do([&] { subcommand = TestSubcommand::Sub1; });
+      });
+      b.AddSubcommand({.name = "sub2"}, [&](auto& sub_b) {
+        sub_b.AddStringOption({.name = "option"},
+                              [&](auto& arg_b) { arg_b.Set(&option1); });
+        sub_b.AddSubcommand({.name = "subsub"}, [&](auto& subsub_b) {
+          subsub_b.AddStringOption({.name = "option"},
+                                   [&](auto& arg_b) { arg_b.Set(&option2); });
+          subsub_b.Do([&] { subsub_command = true; });
+        });
+        sub_b.Do([&] { subcommand = TestSubcommand::Sub2; });
+      });
+      b.RequiresSubcommand();
+    });
+  };
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq("ERROR: No subcommand specified. Available "
+                                  "subcommands: 'sub1', 'sub2', or 'help'\n"));
+
+  EXPECT_THAT(parse({"--flag"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq("ERROR: Unknown option '--flag'\n"));
+
+  EXPECT_THAT(parse({"sub3"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq("ERROR: Invalid subcommand 'sub3'. Available "
+                                  "subcommands: 'sub1', 'sub2', or 'help'\n"));
+
+  EXPECT_THAT(parse({"--flag", "sub1"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq("ERROR: Unknown option '--flag'\n"));
+
+  EXPECT_THAT(parse({"sub1", "--flag"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(subcommand, Eq(TestSubcommand::Sub1));
+  EXPECT_TRUE(flag);
+
+  EXPECT_THAT(parse({"sub2", "--option=value"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(subcommand, Eq(TestSubcommand::Sub2));
+  EXPECT_THAT(option1, StrEq("value"));
+
+  EXPECT_THAT(parse({"sub2", "--option=abc", "subsub42", "--option=xyz"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(),
+              StrEq("ERROR: Invalid subcommand 'subsub42'. Available "
+                    "subcommands: 'subsub', or 'help'\n"));
+
+  EXPECT_THAT(
+      parse({"sub2", "--option=abc", "subsub", "--option=xyz"}, llvm::errs()),
+      Eq(CommandLine::ParseResult::Success));
+  EXPECT_TRUE(subsub_command);
+  EXPECT_THAT(option1, StrEq("abc"));
+  EXPECT_THAT(option2, StrEq("xyz"));
+}
+
+TEST(ArgParserTest, Appending) {
+  llvm::SmallVector<int> integers;
+  llvm::SmallVector<llvm::StringRef> strings;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddIntegerOption({.name = "int"},
+                         [&](auto& arg_b) { arg_b.Append(&integers); });
+      b.AddStringOption({.name = "str"},
+                        [&](auto& arg_b) { arg_b.Append(&strings); });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({}, llvm::errs()), Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integers, ElementsAre());
+  EXPECT_THAT(strings, ElementsAre());
+
+  EXPECT_THAT(parse({"--int=1", "--int=2", "--int=3"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integers, ElementsAre(Eq(1), Eq(2), Eq(3)));
+  EXPECT_THAT(strings, ElementsAre());
+
+  EXPECT_THAT(parse({"--str=a", "--str=b", "--str=c"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integers, ElementsAre(Eq(1), Eq(2), Eq(3)));
+  EXPECT_THAT(strings, ElementsAre(StrEq("a"), StrEq("b"), StrEq("c")));
+
+  EXPECT_THAT(parse({"--str=d", "--int=4", "--str=e"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integers, ElementsAre(Eq(1), Eq(2), Eq(3), Eq(4)));
+  EXPECT_THAT(strings, ElementsAre(StrEq("a"), StrEq("b"), StrEq("c"),
+                                   StrEq("d"), StrEq("e")));
+}
+
+TEST(ArgParserTest, PositionalAppending) {
+  llvm::SmallVector<llvm::StringRef> option_strings;
+  llvm::SmallVector<llvm::StringRef> strings1;
+  llvm::SmallVector<llvm::StringRef> strings2;
+  llvm::SmallVector<llvm::StringRef> strings3;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddStringOption({.name = "opt"},
+                        [&](auto& arg_b) { arg_b.Append(&option_strings); });
+      b.AddStringPositionalArg({.name = "s1"},
+                               [&](auto& arg_b) { arg_b.Append(&strings1); });
+      b.AddStringPositionalArg({.name = "s2"},
+                               [&](auto& arg_b) { arg_b.Append(&strings2); });
+      b.AddStringPositionalArg({.name = "s3"},
+                               [&](auto& arg_b) { arg_b.Append(&strings3); });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({"a", "--opt=x", "b", "--opt=y", "--", "c", "--opt=z", "d",
+                     "--", "e", "f"},
+                    llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(option_strings, ElementsAre(StrEq("x"), StrEq("y")));
+  EXPECT_THAT(strings1, ElementsAre(StrEq("a"), StrEq("b")));
+  EXPECT_THAT(strings2, ElementsAre(StrEq("c"), StrEq("--opt=z"), StrEq("d")));
+  EXPECT_THAT(strings3, ElementsAre(StrEq("e"), StrEq("f")));
+}
+
+TEST(ArgParserTest, OneOfOption) {
+  int value = 0;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddOneOfOption({.name = "option"}, [&](auto& arg_b) {
+        arg_b.SetOneOf(
+            {
+                arg_b.OneOfValue("x", 1),
+                arg_b.OneOfValue("y", 2),
+                arg_b.OneOfValue("z", 3),
+            },
+            &value);
+      });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({"--option=x"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(value, Eq(1));
+
+  EXPECT_THAT(parse({"--option=y"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(value, Eq(2));
+
+  EXPECT_THAT(parse({"--option=z"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(value, Eq(3));
+
+  constexpr const char* ErrorStr =
+      "ERROR: Option '--option={0}' has an invalid value '{0}'; valid values "
+      "are: 'x', 'y', or 'z'\n";
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--option=a"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "a")));
+
+  EXPECT_THAT(parse({"--option="}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "")));
+
+  EXPECT_THAT(parse({"--option=xx"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "xx")));
+
+  EXPECT_THAT(parse({"--option=\xFF"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "\\FF")));
+
+  EXPECT_THAT(parse({llvm::StringRef("--option=\0", 10)}, os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "\\00")));
+}
+
+TEST(ArgParserTest, OneOfOptionAppending) {
+  llvm::SmallVector<int> values;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddOneOfOption({.name = "option"}, [&](auto& arg_b) {
+        arg_b.AppendOneOf(
+            {
+                arg_b.OneOfValue("x", 1),
+                arg_b.OneOfValue("y", 2),
+                arg_b.OneOfValue("z", 3),
+            },
+            &values);
+      });
+      b.Do([] {});
+    });
+  };
+
+  EXPECT_THAT(parse({}, llvm::errs()), Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(values, ElementsAre());
+
+  EXPECT_THAT(parse({"--option=x", "--option=y", "--option=z"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(values, ElementsAre(Eq(1), Eq(2), Eq(3)));
+
+  EXPECT_THAT(parse({"--option=y", "--option=y", "--option=x"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(values, ElementsAre(Eq(1), Eq(2), Eq(3), Eq(2), Eq(2), Eq(1)));
+}
+
+TEST(ArgParserTest, RequiredArgs) {
+  int integer_option;
+  llvm::StringRef string_option;
+
+  TestSubcommand subcommand;
+  int integer_sub_option;
+  llvm::StringRef string_sub_option;
+  TestEnum enum_sub_option;
+
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(args, s, s, TestCommandInfo, [&](auto& b) {
+      b.AddIntegerOption({.name = "opt1"}, [&](auto& arg_b) {
+        arg_b.Required(true);
+        arg_b.Set(&integer_option);
+      });
+      b.AddStringOption({.name = "opt2"}, [&](auto& arg_b) {
+        arg_b.Required(true);
+        arg_b.Set(&string_option);
+      });
+      b.AddSubcommand({.name = "sub1"}, [&](auto& sub_b) {
+        sub_b.AddIntegerOption({.name = "sub-opt1"}, [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.Set(&integer_sub_option);
+        });
+        sub_b.AddStringOption({.name = "sub-opt2"}, [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.Set(&string_sub_option);
+        });
+        sub_b.Do([&] { subcommand = TestSubcommand::Sub1; });
+      });
+      b.AddSubcommand({.name = "sub2"}, [&](auto& sub_b) {
+        sub_b.AddIntegerOption({.name = "sub-opt1"}, [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.Set(&integer_sub_option);
+        });
+        sub_b.AddStringOption({.name = "sub-opt2"}, [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.Set(&string_sub_option);
+        });
+        sub_b.AddOneOfOption({.name = "sub-opt3"}, [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.SetOneOf(
+              {
+                  arg_b.OneOfValue("a", TestEnum::Val1),
+                  arg_b.OneOfValue("b", TestEnum::Val2),
+              },
+              &enum_sub_option);
+        });
+        sub_b.Do([&] { subcommand = TestSubcommand::Sub2; });
+      });
+      b.Do([] {});
+    });
+  };
+
+  constexpr const char* ErrorStr =
+      "ERROR: Required option '{0}' not provided.\n";
+  TestRawOstream os;
+  EXPECT_THAT(parse({}, os), Eq(CommandLine::ParseResult::Error));
+  auto errors = os.TakeStr();
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--opt1")));
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--opt2")));
+
+  EXPECT_THAT(parse({"--opt2=xyz"}, os), Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "--opt1")));
+
+  EXPECT_THAT(parse({"--opt2=xyz", "--opt1=42"}, llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integer_option, Eq(42));
+  EXPECT_THAT(string_option, StrEq("xyz"));
+
+  EXPECT_THAT(parse({"--opt2=xyz", "--opt1=42", "sub2"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  errors = os.TakeStr();
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--sub-opt1")));
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--sub-opt2")));
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--sub-opt3")));
+
+  EXPECT_THAT(parse({"--opt2=xyz", "--opt1=42", "sub2", "--sub-opt3=b"}, os),
+              Eq(CommandLine::ParseResult::Error));
+  errors = os.TakeStr();
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--sub-opt1")));
+  EXPECT_THAT(errors, HasSubstr(llvm::formatv(ErrorStr, "--sub-opt2")));
+  EXPECT_THAT(errors, Not(HasSubstr(llvm::formatv(ErrorStr, "--sub-opt3"))));
+
+  EXPECT_THAT(parse({"--opt2=xyz", "--opt1=42", "sub2", "--sub-opt3=b",
+                     "--sub-opt1=13"},
+                    os),
+              Eq(CommandLine::ParseResult::Error));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::formatv(ErrorStr, "--sub-opt2")));
+
+  EXPECT_THAT(parse({"--opt2=xyz", "--opt1=42", "sub2", "--sub-opt3=b",
+                     "--sub-opt1=13", "--sub-opt2=abc"},
+                    llvm::errs()),
+              Eq(CommandLine::ParseResult::Success));
+  EXPECT_THAT(integer_option, Eq(42));
+  EXPECT_THAT(string_option, StrEq("xyz"));
+  EXPECT_THAT(subcommand, Eq(TestSubcommand::Sub2));
+  EXPECT_THAT(integer_sub_option, Eq(13));
+  EXPECT_THAT(string_sub_option, StrEq("abc"));
+  EXPECT_THAT(enum_sub_option, Eq(TestEnum::Val2));
+}
+
+TEST(ArgParserTest, HelpAndVersion) {
+  bool flag = false;
+  int storage = -1;
+  llvm::StringRef string = "";
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(  // Force line break.
+        args, s, s,
+        {
+            .name = "test",
+            .version = "Test Command -- version 1.2.3",
+            .build_info = R"""(
+Build timestamp: )""" __TIMESTAMP__ R"""(
+Build config: test-config-info
+)""",
+            .help = R"""(
+A test command.
+
+Lots more information about the test command.
+)""",
+            .help_epilogue = R"""(
+Closing remarks.
+)""",
+        },
+        [&](auto& b) {
+          b.AddFlag(
+              {
+                  .name = "flag",
+                  .short_name = "f",
+                  .help = R"""(
+A boolean flag.
+)""",
+              },
+              [&](auto& arg_b) { arg_b.Set(&flag); });
+          b.AddFlag(
+              {
+                  .name = "hidden_flag",
+                  .help = R"""(
+A *hidden* boolean flag.
+)""",
+              },
+              [&](auto& arg_b) {
+                arg_b.HelpHidden(true);
+                arg_b.Set(&flag);
+              });
+          b.AddSubcommand(
+              {
+                  .name = "edit",
+                  .help = R"""(
+Edit the widget.
+
+This will take the provided widgets and edit them.
+)""",
+                  .help_epilogue = R"""(
+That's all.
+)""",
+              },
+              [&](auto& sub_b) {
+                sub_b.AddIntegerOption(
+                    {
+                        .name = "option",
+                        .short_name = "o",
+                        .help = R"""(
+An integer option.
+)""",
+                    },
+                    [&](auto& arg_b) { arg_b.Set(&storage); });
+                sub_b.Do([] {});
+              });
+          b.AddSubcommand(
+              {
+                  .name = "run",
+                  .help = R"""(
+Run wombats across the screen.
+
+This will cause several wombats to run across your screen.
+)""",
+                  .help_epilogue = R"""(
+Or it won't, who knows.
+)""",
+              },
+              [&](auto& sub_b) {
+                sub_b.AddStringOption(
+                    {
+                        .name = "option",
+                        .short_name = "o",
+                        .help = R"""(
+A string option.
+)""",
+                    },
+                    [&](auto& arg_b) { arg_b.Set(&string); });
+                sub_b.AddOneOfOption(
+                    {
+                        .name = "one-of-option",
+                        .help = R"""(
+A one-of option.
+)""",
+                    },
+                    [&](auto& arg_b) {
+                      arg_b.SetOneOf(
+                          {
+                              arg_b.OneOfValue("x", 1),
+                              arg_b.OneOfValue("y", 2),
+                              arg_b.OneOfValue("z", 3),
+                          },
+                          &storage);
+                    });
+                sub_b.Do([] {});
+              });
+          b.AddSubcommand(
+              {
+                  .name = "hidden",
+                  .help = R"""(
+A hidden subcommand.
+)""",
+              },
+              [&](auto& sub_b) {
+                sub_b.HelpHidden(true);
+                sub_b.Do([] {});
+              });
+          b.RequiresSubcommand();
+        });
+  };
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--flag", "--help"}, os),
+              Eq(CommandLine::ParseResult::MetaSuccess));
+  std::string help_flag_output = os.TakeStr();
+  EXPECT_THAT(help_flag_output, StrEq(llvm::StringRef(R"""(
+Test Command -- version 1.2.3
+
+A test command.
+
+Lots more information about the test command.
+
+Build info:
+  Build timestamp: )""" __TIMESTAMP__ R"""(
+  Build config: test-config-info
+
+Usage:
+  test [-f] edit [--option=...]
+  test [-f] run [OPTIONS]
+
+Subcommands:
+  edit
+          Edit the widget.
+
+          This will take the provided widgets and edit them.
+
+  run
+          Run wombats across the screen.
+
+          This will cause several wombats to run across your screen.
+
+  help
+          Prints help information for the command, including a description, command line usage, and details of each subcommand and option that can be provided.
+
+  version
+          Prints the version of this command.
+
+Command options:
+  -f, --flag
+          A boolean flag.
+
+Closing remarks.
+
+)""")
+                                          .ltrim('\n')));
+  EXPECT_THAT(parse({"help"}, os), Eq(CommandLine::ParseResult::MetaSuccess));
+  EXPECT_THAT(os.TakeStr(), StrEq(help_flag_output));
+
+  EXPECT_THAT(parse({"--version"}, os),
+              Eq(CommandLine::ParseResult::MetaSuccess));
+  std::string version_flag_output = os.TakeStr();
+  EXPECT_THAT(version_flag_output, StrEq(llvm::StringRef(R"""(
+Test Command -- version 1.2.3
+
+Build timestamp: )""" __TIMESTAMP__ R"""(
+Build config: test-config-info
+)""")
+                                             .ltrim('\n')));
+  EXPECT_THAT(parse({"version"}, os),
+              Eq(CommandLine::ParseResult::MetaSuccess));
+  EXPECT_THAT(os.TakeStr(), StrEq(version_flag_output));
+
+  EXPECT_THAT(parse({"--flag", "edit", "--option=42", "--help"}, os),
+              Eq(CommandLine::ParseResult::MetaSuccess));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::StringRef(R"""(
+Edit the widget.
+
+This will take the provided widgets and edit them.
+
+Subcommand 'edit' usage:
+  test [-f] edit [--option=...]
+
+Subcommand 'edit' options:
+  -o, --option=...
+          An integer option.
+
+      --help[=(full|short)]
+          Prints help information for the subcommand, including a description, command line usage, and details of each option that can be provided.
+
+          Possible values:
+          - full (default)
+          - short
+
+That's all.
+
+)""")
+                                      .ltrim('\n')));
+
+  EXPECT_THAT(parse({"--flag", "run", "--option=abc", "--help"}, os),
+              Eq(CommandLine::ParseResult::MetaSuccess));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::StringRef(R"""(
+Run wombats across the screen.
+
+This will cause several wombats to run across your screen.
+
+Subcommand 'run' usage:
+  test [-f] run [OPTIONS]
+
+Subcommand 'run' options:
+  -o, --option=...
+          A string option.
+
+      --one-of-option=(x|y|z)
+          A one-of option.
+
+          Possible values:
+          - x
+          - y
+          - z
+
+      --help[=(full|short)]
+          Prints help information for the subcommand, including a description, command line usage, and details of each option that can be provided.
+
+          Possible values:
+          - full (default)
+          - short
+
+Or it won't, who knows.
+
+)""")
+                                      .ltrim('\n')));
+}
+
+TEST(ArgParserTest, HelpMarkdownLike) {
+  bool flag = false;
+  auto parse = [&](llvm::ArrayRef<llvm::StringRef> args, llvm::raw_ostream& s) {
+    return CommandLine::Parse(  // Force line break.
+        args, s, s, {.name = "test"}, [&](auto& b) {
+          b.AddFlag(
+              {
+                  .name = "flag",
+                  .help = R"""(
+A boolean flag.
+
+    Preformatted
+        code....
+        ........
+
+But
+  here
+    lines
+      are
+        collapsed.
+
+```
+x
+ y
+  z
+```
+)""",
+              },
+              [&](auto& arg_b) { arg_b.Set(&flag); });
+          b.Do([] {});
+        });
+  };
+
+  TestRawOstream os;
+  EXPECT_THAT(parse({"--help"}, os), Eq(CommandLine::ParseResult::MetaSuccess));
+  EXPECT_THAT(os.TakeStr(), StrEq(llvm::StringRef(R"""(
+Usage:
+  test [--flag]
+
+Options:
+      --flag
+          A boolean flag.
+
+              Preformatted
+                  code....
+                  ........
+
+          But here lines are collapsed.
+
+          ```
+          x
+           y
+            z
+          ```
+
+      --help[=(full|short)]
+          Prints help information for the command, including a description, command line usage, and details of each option that can be provided.
+
+          Possible values:
+          - full (default)
+          - short
+
+)""")
+                                      .ltrim('\n')));
+}
+
+}  // namespace
+}  // namespace Carbon::Testing