diff --git a/impl/command_line/arguments.cxx b/impl/command_line/arguments.cxx index bacd58e5..276f766c 100644 --- a/impl/command_line/arguments.cxx +++ b/impl/command_line/arguments.cxx @@ -8,6 +8,7 @@ using namespace std::literals::string_literals; using namespace std::literals::string_view_literals; +using optionsVisited_t = substrate::commandLine::arguments_t::optionsVisited_t; namespace substrate::commandLine { @@ -88,7 +89,7 @@ namespace substrate::commandLine tokeniser_t lexer{argCount - 1U, argList + 1}; arguments_t result{}; // Try to parse all available arguments against the options tree for the program - if (!result.parseFrom(lexer, options)) + if (!result.parseFrom(lexer, options, {})) return std::nullopt; return result; } @@ -129,7 +130,7 @@ namespace substrate::commandLine [[nodiscard]] static auto collectRequiredOptions(const options_t &options) noexcept { // Build a set of all the required options defined in this options_t - std::set requiredOptions{}; + optionsVisited_t requiredOptions{}; for (const auto &option : options) { // If the optionItem_t is a bad variant, ignore it @@ -150,7 +151,7 @@ namespace substrate::commandLine return requiredOptions; } - static auto displayName(const optionsItem_t &item) + [[nodiscard]] static auto displayName(const optionsItem_t &item) { if (item.valueless_by_exception()) return ""s; @@ -161,14 +162,44 @@ namespace substrate::commandLine }, item); } + [[nodiscard]] static size_t checkExclusivity(const optionsVisited_t &options) noexcept + { + std::set exclusiveOptions{}; + // Loop through all the visited options + for (const auto &option : options) + { + // Dispatch on the option type + std::visit(match_t + { + [&](const option_t &value) + { + if (value.isExclusive()) + exclusiveOptions.insert(value); + }, + [](const optionSet_t &) { }, + }, option); + } + // Now we've collected all the options that are exclusive, display diagnostics + // if there is more than one in the set and then return how many there are + if (exclusiveOptions.size() > 1) + { + console.error("Multiple mutually exclusive options given together on command line, only one allowed."sv); + console.error("Mutually exclusive options given are:"sv); + for (const auto &option : exclusiveOptions) + console.error(" "sv, option.displayName()); + } + return exclusiveOptions.size(); + } + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) - bool arguments_t::parseFrom(tokeniser_t &lexer, const options_t &options) + // NOLINTNEXTLINE(performance-unnecessary-value-param) + bool arguments_t::parseFrom(tokeniser_t &lexer, const options_t &options, const optionsVisited_t globalOptions) { const auto &token{lexer.token()}; optionsVisited_t optionsVisited{}; while (token.valid()) { - const auto result{parseArgument(lexer, options, optionsVisited)}; + const auto result{parseArgument(lexer, options, globalOptions, optionsVisited)}; // If the result is a nullopt, we're unwinding an inner failure if (!result) return false; @@ -181,6 +212,14 @@ namespace substrate::commandLine return false; } } + // Having parsed as many options as we can, collect all exclusive options seen and + // perform the exclusivity check (there may not be more than 1 exclusive option given) + const auto exclusiveOptions{checkExclusivity(optionsVisited)}; + if (exclusiveOptions > 1U) + return false; + // If there was just one exclusive option, return true to short-circuit the required options check + if (exclusiveOptions == 1U) + return true; // Having parsed as many options as we can, collect all the required options into a set const auto requiredOptions{collectRequiredOptions(options)}; optionsVisited_t missingOptions{}; @@ -200,14 +239,15 @@ namespace substrate::commandLine static std::optional matchOption(tokeniser_t &lexer, const option_t &option, const std::string_view &argument) noexcept; static std::optional matchOptionSet(tokeniser_t &lexer, const optionSet_t &option, - const std::string_view &argument) noexcept; + const std::string_view &argument, const options_t &options, + const optionsVisited_t &globalOptions) noexcept; template static bool checkMatchValid(const optionsItem_t &option, set_t &optionsVisited) noexcept; template static std::optional handleResult(arguments_t &arguments, const optionsItem_t &option, set_t &optionsVisited, const std::string_view &argument, const optionMatch_t &match) noexcept; static void handleUnrecognised(tokeniser_t &lexer, const std::string_view &argument) noexcept; std::optional arguments_t::parseArgument(tokeniser_t &lexer, const options_t &options, - optionsVisited_t &optionsVisited) noexcept + const optionsVisited_t &globalOptions, optionsVisited_t &optionsVisited) noexcept { // Start by checking we're in a suitable state const auto &token{lexer.token()}; @@ -220,34 +260,50 @@ namespace substrate::commandLine const auto argument{token.value()}; // Initialise look-aside for optionValue_t{} options std::optional valueOption{}; - for (const auto &option : options) + const auto matchOptions { - // Check if this option is an option_t that is valueOnly() (optionValue_t{}) - if (std::holds_alternative(option)) + [&](const auto &optionsSet) -> std::variant> { - const auto &value{std::get(option)}; - if (value.valueOnly()) + for (const auto &option : optionsSet) { - valueOption = value; - continue; + // Check if this option is an option_t that is valueOnly() (optionValue_t{}) + if (std::holds_alternative(option)) + { + const auto &value{std::get(option)}; + if (value.valueOnly()) + { + valueOption = value; + continue; + } + } + + // Otherwise, process the option normally + const auto match + { + // Dispatch based on the option type + std::visit(match_t + { + [&](const option_t &value) { return matchOption(lexer, value, argument); }, + [&](const optionSet_t &value) + { return matchOptionSet(lexer, value, argument, options, globalOptions); }, + }, option) + }; + + // If we got a valid match, use the result + if (match) + return handleResult(*this, option, optionsVisited, argument, *match); } + return std::monostate{}; } - - // Otherwise, process the option normally - const auto match - { - // Dispatch based on the option type - std::visit(match_t - { - [&](const option_t &value) { return matchOption(lexer, value, argument); }, - [&](const optionSet_t &value) { return matchOptionSet(lexer, value, argument); }, - }, option) - }; - - // If we got a valid match, use the result - if (match) - return handleResult(*this, option, optionsVisited, argument, *match); - } + }; + // Try matching on the arguments from this level of the recursion + const auto localsMatch{matchOptions(options)}; + if (std::holds_alternative>(localsMatch)) + return std::get>(localsMatch); + // If that fails, now try matching on the global options + const auto globalsMatch{matchOptions(globalOptions)}; + if (std::holds_alternative>(globalsMatch)) + return std::get>(globalsMatch); // If there's an optionValue_t{} and we got no match so far, try matching on it if (valueOption) { @@ -307,8 +363,36 @@ namespace substrate::commandLine return flag_t{option.metaName(), std::move(*value)}; } + static auto gatherGlobals(const options_t &options, + const optionsVisited_t &globalOptions) noexcept + { + // Clone the existing set of global options + auto result{globalOptions}; + // Loop through the current level's options and pull out any that are global + std::for_each + ( + options.begin(), + options.end(), + [&](const internal::optionsItem_t &option) + { + std::visit(match_t + { + [&](const option_t &value) + { + if (value.isGlobal()) + result.insert(value); + }, + [&](const optionSet_t &) { }, + }, option); + } + ); + // Having gathered all of them up, return the new set + return result; + } + static std::optional matchOptionSet(tokeniser_t &lexer, const optionSet_t &option, - const std::string_view &argument) noexcept + const std::string_view &argument, const options_t &options, + const optionsVisited_t &globalOptions) noexcept { // Check if we're parsing an alternation from a set const auto match{option.matches(argument)}; @@ -321,7 +405,7 @@ namespace substrate::commandLine lexer.next(); arguments_t subarguments{}; const auto &suboptions{alternation.suboptions()}; - if (!suboptions.empty() && !subarguments.parseFrom(lexer, suboptions)) + if (!suboptions.empty() && !subarguments.parseFrom(lexer, suboptions, gatherGlobals(options, globalOptions))) // If the operation fails, use monostate to signal match-but-fail. return std::monostate{}; return choice_t{option.metaName(), argument, std::move(subarguments)}; @@ -388,12 +472,12 @@ namespace substrate::commandLine } // Implementation of the innards of arguments_t as otherwise we get compile errors - // NOLINTNEXTLINE(modernize-use-equals-default) - arguments_t::arguments_t() noexcept : _arguments{} { } - arguments_t::arguments_t(const arguments_t &arguments) noexcept : _arguments{arguments._arguments} { } - arguments_t::arguments_t(arguments_t &&arguments) noexcept : _arguments{std::move(arguments._arguments)} { } - // NOLINTNEXTLINE(modernize-use-equals-default) - arguments_t::~arguments_t() noexcept { } + arguments_t::arguments_t() noexcept = default; + arguments_t::arguments_t(const arguments_t &arguments) = default; + arguments_t::arguments_t(arguments_t &&arguments) noexcept = default; + arguments_t::~arguments_t() noexcept = default; + arguments_t &arguments_t::operator =(const arguments_t &arguments) = default; + arguments_t &arguments_t::operator =(arguments_t &&arguments) noexcept = default; size_t arguments_t::count() const noexcept { return _arguments.size(); } size_t arguments_t::countMatching(const std::string_view &option) const noexcept @@ -405,18 +489,6 @@ namespace substrate::commandLine arguments_t::iterator_t arguments_t::find(const std::string_view &option) const noexcept { return _arguments.find(option); } - arguments_t &arguments_t::operator =(const arguments_t &arguments) noexcept - { - _arguments = arguments._arguments; - return *this; - } - - arguments_t &arguments_t::operator =(arguments_t &&arguments) noexcept - { - _arguments = std::move(arguments._arguments); - return *this; - } - // NOLINTNEXTLINE(readability-convert-member-functions-to-static) std::vector arguments_t::findAll(const std::string_view &option) const noexcept { diff --git a/impl/command_line/options.cxx b/impl/command_line/options.cxx index fb2c7ddc..18d60344 100644 --- a/impl/command_line/options.cxx +++ b/impl/command_line/options.cxx @@ -6,6 +6,7 @@ #include #include #include +#include using namespace std::literals::string_literals; using namespace std::literals::string_view_literals; @@ -201,6 +202,7 @@ namespace substrate::commandLine case optionValueType_t::userDefined: return "VAL"sv; } + substrate::unreachable(); } [[nodiscard]] std::string option_t::displayName() const noexcept @@ -220,9 +222,7 @@ namespace substrate::commandLine { [&](const std::string_view &option) { return std::string{option} + typeValue; }, [&](const optionFlagPair_t &option) - { - return std::string{option._shortFlag} + ", "s + std::string{option._longFlag} + typeValue; - }, + { return std::string{option._shortFlag} + ", "s + std::string{option._longFlag} + typeValue; }, [&](const optionValue_t &option) { return std::string{option.metaName()} + (isRepeatable() ? "..."s : ""s); } }, _option); } diff --git a/substrate/command_line/arguments b/substrate/command_line/arguments index 12b24ed8..f0a5bc15 100644 --- a/substrate/command_line/arguments +++ b/substrate/command_line/arguments @@ -40,25 +40,28 @@ namespace substrate::commandLine struct SUBSTRATE_CLS_API arguments_t { + public: + using optionsVisited_t = std::set; + private: using storage_t = std::multiset>; using iterator_t = typename storage_t::const_iterator; - using optionsVisited_t = std::set; - storage_t _arguments; + storage_t _arguments{}; [[nodiscard]] std::optional parseArgument(internal::tokeniser_t &lexer, const options_t &options, - optionsVisited_t &optionsVisited) noexcept; + const optionsVisited_t &globalOptions, optionsVisited_t &optionsVisited) noexcept; public: arguments_t() noexcept; - arguments_t(const arguments_t &arguments) noexcept; + arguments_t(const arguments_t &arguments); arguments_t(arguments_t &&arguments) noexcept; ~arguments_t() noexcept; - arguments_t &operator =(const arguments_t &arguments) noexcept; + arguments_t &operator =(const arguments_t &arguments); arguments_t &operator =(arguments_t &&arguments) noexcept; - [[nodiscard]] bool parseFrom(internal::tokeniser_t &lexer, const options_t &options); + [[nodiscard]] bool parseFrom(internal::tokeniser_t &lexer, const options_t &options, + optionsVisited_t globalOptions); [[nodiscard]] bool add(item_t argument) noexcept; [[nodiscard]] size_t count() const noexcept; diff --git a/substrate/command_line/options b/substrate/command_line/options index 6e92f659..214ce79a 100644 --- a/substrate/command_line/options +++ b/substrate/command_line/options @@ -25,6 +25,8 @@ namespace substrate::commandLine repeatable, takesParameter, required, + global, + exclusive, }; enum class optionValueType_t @@ -125,6 +127,18 @@ namespace substrate::commandLine return *this; } + [[nodiscard]] constexpr option_t &global() noexcept + { + _flags.set(optionFlags_t::global); + return *this; + } + + [[nodiscard]] constexpr option_t &exclusive() noexcept + { + _flags.set(optionFlags_t::exclusive); + return *this; + } + template [[nodiscard]] constexpr option_t &valueRange(T min, T max) noexcept { // These perform casts only to silence conversion warnings @@ -144,6 +158,10 @@ namespace substrate::commandLine { return _flags.includes(optionFlags_t::repeatable); } [[nodiscard]] constexpr bool isRequired() const noexcept { return _flags.includes(optionFlags_t::required); } + [[nodiscard]] constexpr bool isGlobal() const noexcept + { return _flags.includes(optionFlags_t::global); } + [[nodiscard]] constexpr bool isExclusive() const noexcept + { return _flags.includes(optionFlags_t::exclusive); } [[nodiscard]] constexpr bool valueOnly() const noexcept { return std::holds_alternative(_option); }