diff --git a/common/BUILD b/common/BUILD index 342441bf7fee..8187cc7e74f4 100644 --- a/common/BUILD +++ b/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 = [ diff --git a/common/command_line.cpp b/common/command_line.cpp new file mode 100644 index 000000000000..aede68360033 --- /dev/null +++ b/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 +#include + +#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 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 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(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(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(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(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(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 build); + + auto Parse(llvm::ArrayRef 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, + 16>; + using SubcommandMapT = llvm::SmallDenseMap; + + // 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; + + void PopulateMaps(const Command& command); + + void SetOptionDefault(const Arg& option); + + auto ParseNegatedFlag(const Arg& flag, std::optional value) + -> bool; + auto ParseFlag(const Arg& flag, std::optional 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 value, bool negated_name = false) + -> bool; + + auto SplitValue(llvm::StringRef& unparsed_arg) + -> std::optional; + 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 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 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 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 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 { + // Split out a value if present. + std::optional 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 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 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 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(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(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 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(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 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 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(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* 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* 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 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 build) { + IntegerArgBuilder builder(AddArgImpl(info, Arg::Kind::Integer)); + build(builder); +} + +void CommandBuilder::AddStringOption( + const ArgInfo& info, llvm::function_ref build) { + StringArgBuilder builder(AddArgImpl(info, Arg::Kind::String)); + build(builder); +} + +void CommandBuilder::AddOneOfOption( + const ArgInfo& info, llvm::function_ref build) { + OneOfArgBuilder builder(AddArgImpl(info, Arg::Kind::OneOf)); + build(builder); +} + +void CommandBuilder::AddMetaActionOption( + const ArgInfo& info, llvm::function_ref build) { + ArgBuilder builder(AddArgImpl(info, Arg::Kind::MetaActionOnly)); + build(builder); +} + +void CommandBuilder::AddIntegerPositionalArg( + const ArgInfo& info, llvm::function_ref build) { + AddPositionalArgImpl(info, Arg::Kind::Integer, [build](Arg& arg) { + IntegerArgBuilder builder(arg); + build(builder); + }); +} + +void CommandBuilder::AddStringPositionalArg( + const ArgInfo& info, llvm::function_ref build) { + AddPositionalArgImpl(info, Arg::Kind::String, [build](Arg& arg) { + StringArgBuilder builder(arg); + build(builder); + }); +} + +void CommandBuilder::AddOneOfPositionalArg( + const ArgInfo& info, llvm::function_ref build) { + AddPositionalArgImpl(info, Arg::Kind::OneOf, [build](Arg& arg) { + OneOfArgBuilder builder(arg); + build(builder); + }); +} + +void CommandBuilder::AddSubcommand( + const CommandInfo& info, llvm::function_ref 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 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 unparsed_args, + llvm::raw_ostream& out, llvm::raw_ostream& errors, + const CommandInfo& command_info, + llvm::function_ref 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 diff --git a/common/command_line.h b/common/command_line.h new file mode 100644 index 000000000000..781911501cab --- /dev/null +++ b/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 +#include +#include +#include +#include + +#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; + +// 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 + 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* 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* 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 + 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 + static auto OneOfValue(llvm::StringRef string, T value) -> OneOfValueT; + + // 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 + void SetOneOf(const OneOfValueT (&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 + void AppendOneOf(const OneOfValueT (&values)[N], + llvm::SmallVectorImpl* sequence); + + private: + using ArgBuilder::ArgBuilder; + + template + void OneOfImpl(const OneOfValueT (&input_values)[N], MatchT match, + std::index_sequence /*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 build); + void AddIntegerOption(const ArgInfo& info, + llvm::function_ref build); + void AddStringOption(const ArgInfo& info, + llvm::function_ref build); + void AddOneOfOption(const ArgInfo& info, + llvm::function_ref build); + void AddMetaActionOption(const ArgInfo& info, + llvm::function_ref build); + + void AddIntegerPositionalArg( + const ArgInfo& info, llvm::function_ref build); + void AddStringPositionalArg( + const ArgInfo& info, llvm::function_ref build); + void AddOneOfPositionalArg(const ArgInfo& info, + llvm::function_ref build); + + void AddSubcommand(const CommandInfo& info, + llvm::function_ref 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 build); + void Finalize(); + + Command& command_; + MetaPrinter& meta_printer_; + + llvm::SmallDenseSet arg_names_; + llvm::SmallDenseSet 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 unparsed_args, + llvm::raw_ostream& out, llvm::raw_ostream& errors, + const CommandInfo& command_info, + llvm::function_ref build) -> ParseResult; + +// Implementation details only below. + +// The internal representation of a parsable argument description. +struct Arg { + using Kind = ArgKind; + using ValueActionT = + std::function; + using DefaultActionT = std::function; + + 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* integer_sequence; + llvm::SmallVectorImpl* string_sequence; + + // One-of information. + struct { + llvm::OwningArrayRef 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> options; + llvm::SmallVector> positional_args; + llvm::SmallVector> subcommands; +}; + +template +void ArgBuilder::MetaAction(T action) { + CARBON_CHECK(!arg_.meta_action) << "Cannot set a meta action twice!"; + arg_.meta_action = std::move(action); +} + +template +auto OneOfArgBuilder::OneOfValueT::Default( + bool is_default) && -> OneOfValueT { + OneOfValueT result = std::move(*this); + result.is_default = is_default; + return result; +} + +template +OneOfArgBuilder::OneOfValueT::OneOfValueT(llvm::StringRef str, T value) + : str(str), value(std::move(value)) {} + +template +auto OneOfArgBuilder::OneOfValue(llvm::StringRef str, T value) + -> OneOfValueT { + return OneOfValueT(str, value); +} + +template +void OneOfArgBuilder::SetOneOf(const OneOfValueT (&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{}); +} + +template +void OneOfArgBuilder::AppendOneOf(const OneOfValueT (&values)[N], + llvm::SmallVectorImpl* 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{}); +} + +// 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 +void OneOfArgBuilder::OneOfImpl(const OneOfValueT (&input_values)[N], + MatchT match, + std::index_sequence /*indices*/) { + std::array value_strings = {input_values[Indices].str...}; + std::array values = {input_values[Indices].value...}; + + // Directly copy the value strings into a heap-allocated array in the + // argument. + new (&arg_.value_strings) + llvm::OwningArrayRef(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(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(0)), + ...); + } +} + +} // namespace Carbon::CommandLine + +#endif // CARBON_COMMON_COMMAND_LINE_H_ diff --git a/common/command_line_test.cpp b/common/command_line_test.cpp new file mode 100644 index 000000000000..9f12882971e8 --- /dev/null +++ b/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 +#include + +#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 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 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 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 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 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 source_strings; + llvm::SmallVector dest_strings; + auto parse = [&](llvm::ArrayRef 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 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 integers; + llvm::SmallVector strings; + auto parse = [&](llvm::ArrayRef 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 option_strings; + llvm::SmallVector strings1; + llvm::SmallVector strings2; + llvm::SmallVector strings3; + auto parse = [&](llvm::ArrayRef 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 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 values; + auto parse = [&](llvm::ArrayRef 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 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 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 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