diff --git a/framework/include/parser/CommandLine.h b/framework/include/parser/CommandLine.h index ad1f1de96fdf..ef8610ed9c8c 100644 --- a/framework/include/parser/CommandLine.h +++ b/framework/include/parser/CommandLine.h @@ -12,6 +12,8 @@ // Moose Includes #include "MooseError.h" #include "Conversion.h" +#include "MooseEnum.h" +#include "InputParameters.h" #include "libmesh/parallel.h" @@ -31,24 +33,19 @@ class InputParameters; class CommandLine { public: - /// Type of argument for a given option - enum ARGUMENT - { - NONE, - OPTIONAL, - REQUIRED - }; + using ArgumentType = InputParameters::CommandLineMetadata::ArgumentType; struct Option { std::string description; std::vector cli_syntax; bool required; - ARGUMENT argument_type; + ArgumentType argument_type; /// This gets filled in automagicaly when calling addOption() std::vector cli_switch; }; + CommandLine(); CommandLine(int argc, char * argv[]); CommandLine(const CommandLine & other); virtual ~CommandLine(); @@ -73,7 +70,7 @@ class CommandLine */ const std::vector & getArguments() { return _argv; } - void addCommandLineOptionsFromParams(InputParameters & params); + void addCommandLineOptionsFromParams(const InputParameters & params); void populateInputParams(InputParameters & params); @@ -144,11 +141,17 @@ class CommandLine protected: /** - * Used to set the argument value, allows specialization + * Helper for setting the argument value, allows specialization */ template void setArgument(std::stringstream & stream, T & argument); + /** + * Helper for setting the argument value; catches errors so we can provide more context + */ + template + void setArgument(std::stringstream & stream, T & argument, const std::string & cli_switch); + /// Command line options std::map _cli_options; @@ -172,40 +175,75 @@ CommandLine::setArgument(std::stringstream & stream, T & argument) stream >> argument; } +template +void +CommandLine::setArgument(std::stringstream & stream, T & argument, const std::string & cli_switch) +{ + // Keep track of and change the throw on error characteristics so that + // we can catch parsing errors for the argument + const auto throw_on_error_orig = Moose::_throw_on_error; + Moose::_throw_on_error = true; + + const auto raw_value = stream.str(); + try + { + setArgument(stream, argument); + } + catch (std::exception & e) + { + Moose::_throw_on_error = throw_on_error_orig; + mooseError("While parsing command line argument '", + cli_switch, + "' with value '", + raw_value, + "':\n\n", + e.what()); + } + + Moose::_throw_on_error = throw_on_error_orig; +} + // Specialization for std::string template <> void CommandLine::setArgument(std::stringstream & stream, std::string & argument); +// Specialization for MooseEnum +template <> +void CommandLine::setArgument(std::stringstream & stream, MooseEnum & argument); template bool CommandLine::search(const std::string & option_name, T & argument) { - std::map::iterator pos = _cli_options.find(option_name); - if (pos != _cli_options.end()) + if (auto pos = _cli_options.find(option_name); pos != _cli_options.end()) { - for (unsigned int i = 0; i < pos->second.cli_switch.size(); ++i) - { - for (size_t j = 0; j < _args.size(); j++) + const auto & option = pos->second; + for (const auto & cli_switch : option.cli_switch) + for (const auto arg_i : index_range(_args)) { - auto arg = _args[j]; + const auto & arg = _args[arg_i]; - if (arg == pos->second.cli_switch[i]) + if (arg == cli_switch) { // "Flag" CLI options are added as Boolean types, when we see them // we set the Boolean argument to true - if (pos->second.argument_type == NONE) + if (option.argument_type == ArgumentType::NONE) argument = true; - else if (j + 1 < _args.size()) + else if (arg_i + 1 < _args.size()) { std::stringstream ss; - ss << _args[j + 1]; + ss << _args[arg_i + 1]; - setArgument(ss, argument); + setArgument(ss, argument, cli_switch); + } + else if (option.argument_type == ArgumentType::REQUIRED) + { + mooseError("The command line argument '", + cli_switch, + "' requires a value and one was not provided."); } return true; } } - } if (pos->second.required) { @@ -235,8 +273,11 @@ CommandLine::search(const std::string & option_name, std::vector & argument) // "Flag" CLI options added vector of Boolean types may apprear multiple times on the // command line (like a repeated verbosity flag to increase verbosity), when we see them // we append a true value to the vector. - if (pos->second.argument_type == NONE) + if (pos->second.argument_type == ArgumentType::NONE) argument.push_back(T()); + else if (pos->second.argument_type == ArgumentType::REQUIRED) + mooseError("Adding vector command line parameters with required arguments is not " + "currently supported"); else while (j + 1 < _argv.size() && _argv[j + 1][0] != '-' && _argv[j + 1].find("=") == std::string::npos) @@ -245,7 +286,7 @@ CommandLine::search(const std::string & option_name, std::vector & argument) ss << _argv[j + 1]; T item; - setArgument(ss, item); + setArgument(ss, item, pos->second.cli_switch[i]); argument.push_back(item); ++j; } diff --git a/framework/include/utils/InputParameters.h b/framework/include/utils/InputParameters.h index ee3229f25b21..f00a21f0a78b 100644 --- a/framework/include/utils/InputParameters.h +++ b/framework/include/utils/InputParameters.h @@ -31,6 +31,7 @@ class FunctionParserBase #include #include #include +#include // Forward declarations class Action; @@ -54,6 +55,24 @@ class InputParameters : public Parameters virtual void clear() override; + /** + * Structure for storing information about a command line parameter + */ + struct CommandLineMetadata + { + enum ArgumentType + { + NONE, + OPTIONAL, + REQUIRED + }; + + /// The syntax for the parameter (i.e., ["-t", "--timing"]) + std::vector syntax; + /// The type of argument + ArgumentType argument_type; + }; + /** * This method adds a description of the class that will be displayed * in the input file syntax dump @@ -281,9 +300,19 @@ class InputParameters : public Parameters void checkConsistentType(const std::string & name) const; /** - * Get the syntax for a command-line parameter + * @return Whether or not the parameter \p name is a command line parameter */ - std::vector getSyntax(const std::string & name) const; + bool isCommandLineParameter(const std::string & name) const; + + /** + * @return The command line syntax for the parameter \p name + */ + const std::vector & getCommandLineSyntax(const std::string & name) const; + + /** + * @return The command line argument type for the parameter \p name + */ + CommandLineMetadata::ArgumentType getCommandLineArgumentType(const std::string & name) const; /** * Get the documentation string for a parameter @@ -992,22 +1021,6 @@ class InputParameters : public Parameters */ std::string appendFunctorDescription(const std::string & doc_string) const; - /** - * Helper that uses overloading to distinguish adding command-line parameters of - * a scalar and a vector kind. Vector parameters are options that may appear multiple - * times on the command line (like -i). - */ - template - void addCommandLineParamHelper(const std::string & name, - const std::string & syntax, - const std::string & doc_string, - T *); - template - void addCommandLineParamHelper(const std::string & name, - const std::string & syntax, - const std::string & doc_string, - std::vector *); - /** * Private method for setting deprecated coupled variable documentation strings */ @@ -1030,7 +1043,8 @@ class InputParameters : public Parameters std::string _doc_string; /// The custom type that will be printed in the YAML dump for a parameter if supplied std::string _custom_type; - std::vector _cli_flag_names; + /// The data pertaining to a command line parameter (empty if not a command line param) + std::optional _cl_data; /// The names of the parameters organized into groups std::string _group; /// The map of functions used for range checked parameters @@ -1108,6 +1122,17 @@ class InputParameters : public Parameters template void setParamHelper(const std::string & name, T & l_value, const S & r_value); + /** + * @return The command line metadata for the parameter \p name. + */ + const CommandLineMetadata & getCommandLineMetadata(const std::string & name) const; + + /** + * Helper for all of the addCommandLineParam() calls, which sets up _cl_data in the metadata + */ + template + void addCommandLineParamHelper(const std::string & name, const std::string & syntax); + /// original location of input block (i.e. filename,linenum) - used for nice error messages. std::string _block_location; @@ -1450,6 +1475,21 @@ InputParameters::setParamHelper(const std::string & /*name*/, T & l_value, const l_value = r_value; } +template +void +InputParameters::addCommandLineParamHelper(const std::string & name, const std::string & syntax) +{ + auto & cl_data = at(name)._cl_data; + cl_data = CommandLineMetadata(); + MooseUtils::tokenize(syntax, cl_data->syntax, 1, " \t\n\v\f\r"); + if constexpr (std::is_same_v) + cl_data->argument_type = CommandLineMetadata::ArgumentType::NONE; + else if constexpr (std::is_same_v) + cl_data->argument_type = CommandLineMetadata::ArgumentType::REQUIRED; + else + cl_data->argument_type = CommandLineMetadata::ArgumentType::OPTIONAL; +} + template void InputParameters::addRequiredRangeCheckedParam(const std::string & name, @@ -1559,7 +1599,7 @@ InputParameters::addRequiredCommandLineParam(const std::string & name, const std::string & doc_string) { addRequiredParam(name, doc_string); - MooseUtils::tokenize(syntax, _params[name]._cli_flag_names, 1, " \t\n\v\f\r"); + addCommandLineParamHelper(name, syntax); } template @@ -1569,7 +1609,7 @@ InputParameters::addCommandLineParam(const std::string & name, const std::string & doc_string) { addParam(name, doc_string); - MooseUtils::tokenize(syntax, _params[name]._cli_flag_names, 1, " \t\n\v\f\r"); + addCommandLineParamHelper(name, syntax); } template @@ -1580,7 +1620,7 @@ InputParameters::addCommandLineParam(const std::string & name, const std::string & doc_string) { addParam(name, value, doc_string); - MooseUtils::tokenize(syntax, _params[name]._cli_flag_names, 1, " \t\n\v\f\r"); + addCommandLineParamHelper(name, syntax); } template diff --git a/framework/src/base/MooseApp.C b/framework/src/base/MooseApp.C index 7296edc62486..3c8e9b3a595d 100644 --- a/framework/src/base/MooseApp.C +++ b/framework/src/base/MooseApp.C @@ -1539,7 +1539,7 @@ MooseApp::showInputs() const { if (isParamValid("show_inputs")) { - auto copy_syntax = _pars.getSyntax("copy_inputs"); + auto copy_syntax = _pars.getCommandLineSyntax("copy_inputs"); std::vector dirs; const auto installable_inputs = getInstallableInputs(); @@ -1590,11 +1590,11 @@ MooseApp::copyInputs() const if (binname == "") mooseError("could not locate installed tests to run (unresolved binary/app name)"); - auto src_dir = - MooseUtils::installedInputsDir(binname, - dir_to_copy, - "Rerun binary with " + _pars.getSyntax("show_inputs")[0] + - " to get a list of installable directories."); + auto src_dir = MooseUtils::installedInputsDir(binname, + dir_to_copy, + "Rerun binary with " + + _pars.getCommandLineSyntax("show_inputs")[0] + + " to get a list of installable directories."); // Use the command line here because if we have a symlink to another binary, // we want to dump into a directory that is named after the symlink not the true binary diff --git a/framework/src/parser/CommandLine.C b/framework/src/parser/CommandLine.C index ef07d5004ecf..33a6fb2d9a85 100644 --- a/framework/src/parser/CommandLine.C +++ b/framework/src/parser/CommandLine.C @@ -12,6 +12,7 @@ #include "MooseInit.h" #include "MooseUtils.h" #include "InputParameters.h" +#include "MooseEnum.h" // Contrib RE #include "pcrecpp.h" @@ -19,6 +20,8 @@ // C++ includes #include +CommandLine::CommandLine() {} + CommandLine::CommandLine(int argc, char * argv[]) { addArguments(argc, argv); } CommandLine::CommandLine(const CommandLine & other) @@ -110,80 +113,58 @@ CommandLine::initForMultiApp(const std::string & subapp_full_name) } void -CommandLine::addCommandLineOptionsFromParams(InputParameters & params) +CommandLine::addCommandLineOptionsFromParams(const InputParameters & params) { - for (const auto & it : params) + for (const auto & name_value_pair : params) { - Option cli_opt; - std::vector syntax; - std::string orig_name = it.first; - - cli_opt.description = params.getDocString(orig_name); - if (!params.isPrivate(orig_name)) - // If a param is private then it shouldn't have any command line syntax. - syntax = params.getSyntax(orig_name); - cli_opt.cli_syntax = syntax; - cli_opt.required = false; - - if (params.have_parameter(orig_name)) - cli_opt.argument_type = CommandLine::NONE; - else - cli_opt.argument_type = CommandLine::REQUIRED; - - addOption(orig_name, cli_opt); + const auto & name = name_value_pair.first; + if (params.isCommandLineParameter(name)) + { + Option cli_opt; + cli_opt.description = params.getDocString(name); + cli_opt.cli_syntax = params.getCommandLineSyntax(name); + cli_opt.argument_type = params.getCommandLineArgumentType(name); + cli_opt.required = false; + addOption(name, cli_opt); + } } } void CommandLine::populateInputParams(InputParameters & params) { - for (const auto & it : params) - { - std::string orig_name = it.first; +#define trySetParameter(type) \ + if (dynamic_cast *>(value.get())) \ + { \ + search(name, params.set(name)); \ + continue; \ + } - if (search(orig_name)) + for (const auto & [name, value] : params) + { + if (params.isCommandLineParameter(name) && search(name)) { - if (params.have_parameter(orig_name)) - { - search(orig_name, params.set(orig_name)); - continue; - } - - if (params.have_parameter>(orig_name)) - { - search(orig_name, params.set>(orig_name)); - continue; - } - - if (params.have_parameter(orig_name)) - { - search(orig_name, params.set(orig_name)); - continue; - } - - if (params.have_parameter(orig_name)) - { - search(orig_name, params.set(orig_name)); - continue; - } - - if (params.have_parameter(orig_name)) - { - search(orig_name, params.set(orig_name)); - continue; - } - - if (params.have_parameter(orig_name)) - { - search(orig_name, params.set(orig_name)); - continue; - } + trySetParameter(std::string); + trySetParameter(std::vector); + trySetParameter(Real); + trySetParameter(unsigned int); + trySetParameter(int); + trySetParameter(bool); + trySetParameter(MooseEnum); +#undef trySetParameter + + mooseError("Command-line parameter '", + name, + "' of type '", + value->type(), + "' is not of a consumable type.\n\nAdd an entry with this type to " + "CommandLine::populateInputParams if it is needed."); } - else if (params.isParamRequired(orig_name)) + else if (params.isParamRequired(name)) mooseError("Missing required command-line parameter: ", - orig_name, + name, "\nDoc String: ", - params.getDocString(orig_name)); + params.getDocString(name)); } } @@ -298,3 +279,10 @@ CommandLine::setArgument(std::stringstream & stream, std::string & { argument = stream.str(); } + +template <> +void +CommandLine::setArgument(std::stringstream & stream, MooseEnum & argument) +{ + argument = stream.str(); +} diff --git a/framework/src/utils/InputParameters.C b/framework/src/utils/InputParameters.C index 669aeebd0885..c8b042cb2ef7 100644 --- a/framework/src/utils/InputParameters.C +++ b/framework/src/utils/InputParameters.C @@ -795,14 +795,22 @@ InputParameters::addParamNamesToGroup(const std::string & space_delim_names, '.'); } -std::vector -InputParameters::getSyntax(const std::string & name_in) const +bool +InputParameters::isCommandLineParameter(const std::string & name) const { - const auto name = checkForRename(name_in); - auto it = _params.find(name); - if (it == _params.end()) - mooseError("No parameter exists with the name ", name); - return it->second._cli_flag_names; + return at(checkForRename(name))._cl_data.has_value(); +} + +const std::vector & +InputParameters::getCommandLineSyntax(const std::string & name) const +{ + return getCommandLineMetadata(name).syntax; +} + +InputParameters::CommandLineMetadata::ArgumentType +InputParameters::getCommandLineArgumentType(const std::string & name) const +{ + return getCommandLineMetadata(name).argument_type; } std::string @@ -1248,6 +1256,15 @@ InputParameters::checkParamName(const std::string & name) const mooseError("Invalid parameter name: '", name, "'"); } +const InputParameters::CommandLineMetadata & +InputParameters::getCommandLineMetadata(const std::string & name) const +{ + const auto & cl_data = at(checkForRename(name))._cl_data; + if (!cl_data) + mooseError("The parameter '", name, "' is not a command line parameter."); + return *cl_data; +} + bool InputParameters::shouldIgnore(const std::string & name_in) { diff --git a/unit/src/CommandLine.C b/unit/src/CommandLine.C index 617c6b0a037d..2c16f0ea66a9 100644 --- a/unit/src/CommandLine.C +++ b/unit/src/CommandLine.C @@ -11,6 +11,7 @@ #include "CommandLine.h" #include "InputParameters.h" +#include "MooseEnum.h" TEST(CommandLine, parse) { @@ -103,3 +104,68 @@ TEST(CommandLine, initForMultiApp) EXPECT_EQ(cl.getArguments(), gold); } } + +TEST(CommandLine, requiredArgument) +{ + InputParameters params = emptyInputParameters(); + const auto default_value = "foo"; + MooseEnum enum_values("foo", default_value); + params.addCommandLineParam("value", "--value", enum_values, "Doc"); + + CommandLine cl; + cl.addArgument("--value"); + cl.addCommandLineOptionsFromParams(params); + + try + { + cl.populateInputParams(params); + FAIL(); + } + catch (const std::exception & err) + { + const auto pos = + std::string(err.what()) + .find("The command line argument '--value' requires a value and one was not provided"); + ASSERT_TRUE(pos != std::string::npos); + } +} + +TEST(CommandLine, setMooseEnum) +{ + InputParameters params = emptyInputParameters(); + const auto default_value = "foo"; + MooseEnum enum_values("foo bar", default_value); + params.addCommandLineParam("value", "--value", enum_values, "Doc"); + + const auto check = [params, &default_value](const std::string & value) + { + auto params_copy = params; + CommandLine cl; + if (value.size()) + { + cl.addArgument("--value"); + cl.addArgument(value); + } + cl.addCommandLineOptionsFromParams(params_copy); + cl.populateInputParams(params_copy); + + EXPECT_EQ((std::string)params_copy.get("value"), + value.size() ? value : default_value); + }; + + check(""); + check("foo"); + check("bar"); + try + { + check("baz"); + FAIL(); + } + catch (const std::exception & err) + { + const std::string expected_err = "While parsing command line argument '--value' with value " + "'baz':\n\nInvalid option \"baz\" in MooseEnum."; + const auto pos = std::string(err.what()).find(expected_err); + EXPECT_TRUE(pos != std::string::npos); + } +}