LCOV - code coverage report
Current view: top level - src/parser - CommandLine.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: fa5e60 Lines: 230 236 97.5 %
Date: 2026-06-24 08:03:36 Functions: 25 25 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : #include "CommandLine.h"
      11             : 
      12             : // C++ includes
      13             : #include <iomanip>
      14             : #include <optional>
      15             : 
      16             : #include "pcrecpp.h"
      17             : 
      18             : #include "hit.h"
      19             : 
      20             : #include "libmesh/libmesh.h"
      21             : #include "libmesh/simple_range.h"
      22             : #include "libmesh/parallel_algebra.h"
      23             : #include "libmesh/parallel_sync.h"
      24             : 
      25             : #include "PetscSupport.h"
      26             : 
      27         242 : CommandLine::CommandLine() {}
      28      110088 : CommandLine::CommandLine(int argc, char * argv[]) { addArguments(argc, argv); }
      29       12300 : CommandLine::CommandLine(const std::vector<std::string> & args) { addArguments(args); }
      30             : 
      31             : void
      32      110088 : CommandLine::addArguments(int argc, char * argv[])
      33             : {
      34     1294044 :   for (int i = 0; i < argc; i++)
      35     2367912 :     addArgument(argv[i]);
      36      110088 : }
      37             : 
      38             : void
      39     1244872 : CommandLine::addArgument(const std::string & arg)
      40             : {
      41             :   mooseAssert(!hasParsed(), "Has already parsed");
      42     1244872 :   _argv.push_back(arg);
      43     1244872 : }
      44             : 
      45             : void
      46       12354 : CommandLine::addArguments(const std::vector<std::string> & args)
      47             : {
      48       72534 :   for (const auto & arg : args)
      49       60180 :     addArgument(arg);
      50       12354 : }
      51             : 
      52             : bool
      53          32 : CommandLine::hasArgument(const std::string & arg) const
      54             : {
      55          32 :   return std::find(_argv.begin(), _argv.end(), arg) != _argv.end();
      56             : }
      57             : 
      58             : void
      59           4 : CommandLine::removeArgument(const std::string & arg)
      60             : {
      61             :   mooseAssert(!hasParsed(), "Has already parsed");
      62           4 :   auto it = std::find(_argv.begin(), _argv.end(), arg);
      63           4 :   if (it == _argv.end())
      64           2 :     mooseError("CommandLine::removeArgument(): The argument '", arg, "' does not exist");
      65           2 :   _argv.erase(it);
      66           2 : }
      67             : 
      68             : void
      69      122626 : CommandLine::parse()
      70             : {
      71             :   mooseAssert(!hasParsed(), "Has already parsed");
      72             :   mooseAssert(_entries.empty(), "Should be empty");
      73             : 
      74             :   // Whether or not we have a entry that accepts values
      75      122626 :   bool has_value_accepting_entry = false;
      76             : 
      77             :   // Helper for adding an entry
      78     1118572 :   auto add_entry = [this](const auto & name) -> Entry &
      79             :   {
      80     1118572 :     auto & entry = _entries.emplace_back();
      81     1118572 :     entry.name = name;
      82     1118572 :     return entry;
      83      122626 :   };
      84             : 
      85             :   // Work through each argument
      86     1367476 :   for (const auto i : index_range(_argv))
      87             :   {
      88     1244866 :     const auto & arg = _argv[i];
      89     1244866 :     const auto begins_with_dash = MooseUtils::beginsWith(arg, "-");
      90     1244866 :     std::string subapp_prefix, subapp_name, hit_path, hit_value;
      91             : 
      92             :     // MultiApp syntax with a non-hit option
      93     1244866 :     if (!begins_with_dash && std::regex_search(arg, std::regex("^(([^\\s\n\t[\\]\\/=#&:]+):)[-]")))
      94             :     {
      95          12 :       mooseError("The MultiApp command line argument '",
      96             :                  arg,
      97             :                  "' sets a command line option.\nMultiApp command line arguments can only be "
      98             :                  "used for setting HIT parameters.");
      99             :     }
     100             :     // Match HIT CLI syntax (including for multiapps with the prefix)
     101             :     // For subapp hit cli syntax (i.e, <subname>:<value)), we will store the
     102             :     // subapp names to be stripped away in initSubAppCommandLine() as they
     103             :     // are passed down
     104     1707108 :     else if (!begins_with_dash &&
     105     1707108 :              pcrecpp::RE("(([^\\s\n\t[\\]\\/=#&:]+)?:)?((?:[^\\s\n\t[\\]=#&]+\\/"
     106             :                          ")?(?:[^\\s\n\t=#$'\"]+))=([^#]+)?")
     107     1707108 :                  .FullMatch(arg, &subapp_prefix, &subapp_name, &hit_path, &hit_value))
     108             :     {
     109      226522 :       auto & entry = add_entry(hit_path);
     110      226522 :       if (subapp_prefix.size())
     111             :       {
     112        2295 :         if (subapp_name.empty()) // :param=value; means apply to all
     113         315 :           entry.global = true;
     114             :         else
     115        1980 :           entry.subapp_name = subapp_name;
     116             :       }
     117      226522 :       entry.value = MooseUtils::removeExtraWhitespace(hit_value);
     118      226522 :       entry.value_separator = "=";
     119      226522 :       entry.raw_args.push_back(arg);
     120      226522 :       entry.hit_param = true;
     121      226522 :       has_value_accepting_entry = false;
     122             :     }
     123             :     // Has an = sign in it, so we have a name=value (non-HIT)
     124     1018332 :     else if (const auto find_equals = arg.find("="); find_equals != std::string::npos)
     125             :     {
     126      134206 :       const auto begin = arg.substr(0, find_equals);
     127      134206 :       const auto end = arg.substr(find_equals + 1);
     128      134206 :       auto & entry = add_entry(begin);
     129      134206 :       entry.value = MooseUtils::removeExtraWhitespace(end);
     130      134206 :       entry.value_separator = "=";
     131      134206 :       entry.raw_args.push_back(arg);
     132      134206 :       has_value_accepting_entry = false;
     133      134206 :     }
     134             :     // Begins with dash(es) and a character, so a new argument. We pass on
     135             :     // everything else that starts with a dash as a value, so the error
     136             :     // will be associated with the value before it
     137      884126 :     else if (std::regex_search(arg, std::regex("^\\-+[a-zA-Z]")))
     138             :     {
     139      648414 :       auto & entry = add_entry(arg);
     140      648414 :       entry.raw_args.push_back(arg);
     141      648414 :       has_value_accepting_entry = true;
     142             :     }
     143             :     // Should be tagging on a value to the previous argument
     144             :     else
     145             :     {
     146             :       // First one is the executable
     147      235712 :       if (i == 0)
     148             :       {
     149      109430 :         auto & entry = add_entry(arg);
     150      109430 :         entry.raw_args.push_back(arg);
     151      109430 :         continue;
     152      109430 :       }
     153             : 
     154             :       // Throw an error if this a value and we don't have anything to apply it to
     155      126282 :       if (!has_value_accepting_entry)
     156             :       {
     157           4 :         std::stringstream err;
     158             :         err << "The command line argument '" << arg
     159           4 :             << "' is not applied to an option and is not a HIT parameter.";
     160             :         // Maybe they meant to apply it to the previous thing
     161             :         // Example: "-i foo.i bar.i" would suggest "-i 'foo.i bar.i'"
     162           4 :         if (i > 0 && _entries.back().value)
     163             :         {
     164             :           err << "\n\nDid you mean to combine this argument with the previous argument, such "
     165             :                  "as:\n\n  "
     166           2 :               << _entries.back().name << *_entries.back().value_separator << "'"
     167           4 :               << *_entries.back().value << " " << arg << "'\n";
     168             :         }
     169           8 :         mooseError(err.str());
     170           4 :       }
     171             : 
     172      126278 :       auto & entry = _entries.back();
     173      126278 :       if (entry.value)
     174         414 :         *entry.value += " " + MooseUtils::removeExtraWhitespace(arg);
     175             :       else
     176      125864 :         entry.value = MooseUtils::removeExtraWhitespace(arg);
     177      126278 :       entry.value_separator = " ";
     178      126278 :       entry.raw_args.push_back(arg);
     179             :     }
     180     1573204 :   }
     181             : 
     182      122610 :   _has_parsed = true;
     183      122610 : }
     184             : 
     185             : const std::list<CommandLine::Entry> &
     186     4921966 : CommandLine::getEntries() const
     187             : {
     188             :   mooseAssert(hasParsed(), "Has not parsed");
     189     4921966 :   return _entries;
     190             : }
     191             : 
     192             : std::list<CommandLine::Entry> &
     193      100185 : CommandLine::getEntries()
     194             : {
     195             :   mooseAssert(hasParsed(), "Has not parsed");
     196      100185 :   return _entries;
     197             : }
     198             : 
     199      181651 : CommandLine::~CommandLine() {}
     200             : 
     201             : std::unique_ptr<CommandLine>
     202       12234 : CommandLine::initSubAppCommandLine(const std::string & multiapp_name,
     203             :                                    const std::string & subapp_name,
     204             :                                    const std::vector<std::string> & input_cli_args,
     205             :                                    const std::set<std::string> & exclude_params)
     206             : {
     207             :   mooseAssert(MooseUtils::beginsWith(subapp_name, multiapp_name),
     208             :               "Name for the subapp should begin with the multiapp");
     209             : 
     210       12234 :   std::vector<std::string> subapp_args;
     211             : 
     212             :   // Start with the arguments from the input file; we want these to take the
     213             :   // lowest priority so we put them first. Also trim extra whitespace
     214       19692 :   for (const auto & arg : input_cli_args)
     215        7458 :     subapp_args.push_back(MooseUtils::removeExtraWhitespace(arg));
     216             : 
     217             :   // Resolve the excluded parameter names to their Entry pointers so we can
     218             :   // efficiently skip them in the propagation loop below. We skip entries for
     219             :   // which findCommandLineParam returns end() (param registered but not present).
     220       12234 :   std::set<const Entry *> skip_entries;
     221       20656 :   for (const auto & param_name : exclude_params)
     222             :   {
     223        8422 :     auto it = findCommandLineParam(param_name);
     224        8422 :     if (it != getEntries().end())
     225         546 :       skip_entries.insert(&*it);
     226             :   }
     227             : 
     228             :   // Pull out all of the arguments that are relevant to this multiapp from the parent
     229             :   // Note that the 0th argument of the main app, i.e., the name used to invoke the program,
     230             :   // is neither a global entry nor a subapp entry and, as such, won't be passed on
     231      139218 :   for (auto & entry : as_range(getEntries().begin(), getEntries().end()))
     232      202885 :     if (entry.global || (entry.subapp_name && (*entry.subapp_name == multiapp_name ||
     233       75901 :                                                *entry.subapp_name == subapp_name)))
     234             :     {
     235             :       // Skip entries that the caller has asked to suppress from propagation
     236       53062 :       if (skip_entries.count(&entry))
     237         546 :         continue;
     238             : 
     239       52516 :       if (entry.hit_param)
     240             :       {
     241             :         // Append : to the beginning if this is global and should be passed to all
     242        1158 :         const std::string prefix = entry.global ? ":" : "";
     243             :         // Apply the param, but without the subapp name
     244        1158 :         subapp_args.push_back(prefix + entry.name + *entry.value_separator + *entry.value);
     245             :         // Mark this entry as used as a child has consumed it
     246        1158 :         entry.used = true;
     247        1158 :       }
     248             :       else
     249       51358 :         subapp_args.insert(subapp_args.end(), entry.raw_args.begin(), entry.raw_args.end());
     250             :     }
     251             : 
     252       24468 :   return std::make_unique<CommandLine>(subapp_args);
     253       12234 : }
     254             : 
     255             : std::vector<std::string>
     256       67295 : CommandLine::buildHitParams()
     257             : {
     258             :   mooseAssert(!_hit_params_built, "Already built");
     259       67295 :   _hit_params_built = true;
     260             : 
     261       67295 :   std::vector<std::string> params;
     262             : 
     263             :   // Collect all hit parameters that aren't for subapps
     264      656306 :   for (auto & entry : getEntries())
     265      589011 :     if (entry.hit_param && !entry.subapp_name)
     266             :     {
     267             :       mooseAssert(entry.value, "Should have a value");
     268             :       mooseAssert(entry.value_separator, "Should have value separator");
     269             :       mooseAssert(*entry.value_separator == "=", "Should be an equals");
     270             : 
     271      116524 :       const std::string name_and_equals = entry.name + *entry.value_separator;
     272      116524 :       std::string arg = name_and_equals;
     273             :       // In the case of empty values, we want them to be empty to hit
     274      116524 :       if (entry.value->empty())
     275        1182 :         arg += "''";
     276             :       else
     277      115342 :         arg += *entry.value;
     278             : 
     279             :       // We could have issues with bash eating strings, so the first try
     280             :       // gives us a chance to wrap the value in quotes
     281             :       try
     282             :       {
     283      233048 :         hit::check("CLI_ARG", arg);
     284             :       }
     285        6085 :       catch (hit::Error & err)
     286             :       {
     287             :         // bash might have eaten quotes around a hit string value or vector
     288             :         // so try quoting after the "=" and reparse
     289        6085 :         arg = name_and_equals + "'" + *entry.value + "'";
     290             :         try
     291             :         {
     292        6085 :           hit::check("CLI_ARG", arg);
     293             :         }
     294             :         // At this point, we've failed to fix it
     295           0 :         catch (hit::Error & err)
     296             :         {
     297           0 :           mooseError("Failed to parse HIT in command line argument '", arg, "'\n\n", err.what());
     298           0 :         }
     299        6085 :       }
     300             : 
     301             :       // Append to the total output
     302      116524 :       params.push_back(arg);
     303             :       // Consider this parameter used
     304      116524 :       entry.used = true;
     305      116524 :     }
     306             : 
     307       67295 :   return params;
     308           0 : }
     309             : 
     310             : void
     311      122501 : CommandLine::populateCommandLineParams(InputParameters & params)
     312             : {
     313             :   mooseAssert(!_command_line_params_populated, "Already populated");
     314             : 
     315             :   // Set the metadata for each command line parameter
     316             :   // We set this separately so that it can be used later to print usage
     317      122501 :   std::map<std::string, std::string> switch_param_map;
     318     6466322 :   for (const auto & name_value_pair : params)
     319             :   {
     320     6343823 :     const auto & name = name_value_pair.first;
     321     6343823 :     if (const auto metadata = params.queryCommandLineMetadata(name))
     322             :     {
     323     5019370 :       auto it_inserted_pair = _command_line_params.emplace(name, CommandLineParam());
     324     5019370 :       auto & option = it_inserted_pair.first->second;
     325             : 
     326     5019370 :       option.description = params.getDocString(name);
     327     5019370 :       option.metadata = *metadata;
     328             : 
     329             :       // Make sure that one switch isn't specified for multiple parameters
     330    10442658 :       for (const auto & cli_switch : option.metadata.switches)
     331     5423290 :         if (const auto it_inserted_pair = switch_param_map.emplace(cli_switch, name);
     332     5423290 :             !it_inserted_pair.second)
     333           2 :           mooseError("The command line options '",
     334           2 :                      it_inserted_pair.first->second,
     335             :                      "' and '",
     336             :                      name,
     337             :                      "' both declare the command line switch '",
     338             :                      cli_switch,
     339             :                      "'");
     340     6343823 :     }
     341             :   }
     342             : 
     343             :   // Set each paramter that we have a value for
     344     5141847 :   for (const auto & [name, param] : _command_line_params)
     345             :   {
     346     5019366 :     auto entry_it = findCommandLineParam(name);
     347     5019366 :     if (entry_it != _entries.end())
     348             :     {
     349      456534 :       auto & entry = *entry_it;
     350             :       mooseAssert(!entry.subapp_name, "Should not be for a subapp");
     351             : 
     352      456534 :       bool found = false;
     353             : #define trySetParameter(type)                                                                      \
     354             :   if (!found && params.have_parameter<type>(name))                                                 \
     355             :   {                                                                                                \
     356             :     static_assert(InputParameters::isValidCommandLineType<type>::value, "Not a supported value");  \
     357             :     auto & value = params.set<type>(name);                                                         \
     358             :     setCommandLineParam(entry_it, param, entry.name, value);                                       \
     359             :     found = true;                                                                                  \
     360             :   }
     361             : 
     362      456534 :       trySetParameter(std::string);
     363      456530 :       trySetParameter(std::vector<std::string>);
     364      456530 :       trySetParameter(Real);
     365      456528 :       trySetParameter(unsigned int);
     366      456526 :       trySetParameter(int);
     367      456524 :       trySetParameter(bool);
     368      456522 :       trySetParameter(MooseEnum);
     369             : #undef trySetParameter
     370             : 
     371             :       mooseAssert(found, "Should have been found");
     372             : 
     373             :       // If we found this parameter, that means we set it and we should mark in the
     374             :       // InputParameters that it is set so that isParamSetByUser() returns true for this param
     375      456518 :       params.commandLineParamSet(name, {});
     376             : 
     377             :       // If this parameter is global, mark its entry as global
     378      456518 :       if (param.metadata.global)
     379      277433 :         entry.global = true;
     380             : 
     381             :       // If the arg is of the form "--key=value", PETSc will recognize it as an unused
     382             :       // argument. That is, setting "--key" as a known command line argument is not
     383             :       // sufficient for PETSc to consider "--key=value" as known. Thus, we explicitly
     384             :       // add "--key=value" args as known when we come across them.
     385      456518 :       if (entry.value_separator && *entry.value_separator == "=")
     386             :       {
     387             :         mooseAssert(entry.raw_args.size() == 1, "Should have one value");
     388       61129 :         libMesh::add_command_line_name(entry.raw_args[0]);
     389             :       }
     390             :     }
     391             :     // If we didn't find it and it is required, we need to error
     392     4562832 :     else if (param.metadata.required)
     393           2 :       mooseError(
     394           2 :           "Missing required command-line parameter: ", name, "\nDoc string: ", param.description);
     395             :   }
     396             : 
     397      122481 :   _command_line_params_populated = true;
     398      122501 : }
     399             : 
     400             : std::string
     401          24 : CommandLine::getExecutableName() const
     402             : {
     403             :   mooseAssert(_entries.size() > 0, "Does not have any entries");
     404             : 
     405             :   // Grab the first item out of argv
     406          24 :   const auto & command = _entries.begin()->name;
     407          24 :   return command.substr(command.find_last_of("/\\") + 1);
     408             : }
     409             : 
     410             : std::string
     411           6 : CommandLine::getExecutableNameBase() const
     412             : {
     413           6 :   auto name = getExecutableName();
     414           6 :   name = name.substr(0, name.find_last_of("-"));
     415           6 :   if (name.find_first_of("/") != std::string::npos)
     416           0 :     name = name.substr(name.find_first_of("/") + 1, std::string::npos);
     417           6 :   return name;
     418           0 : }
     419             : 
     420             : void
     421           9 : CommandLine::printUsage() const
     422             : {
     423           9 :   Moose::out << "Usage: " << getExecutableName() << " [<options>]\n\n";
     424             : 
     425           9 :   std::size_t max_len = 0;
     426         666 :   for (const auto & name_option_pair : _command_line_params)
     427         657 :     max_len = std::max(max_len, name_option_pair.second.metadata.syntax.size());
     428             : 
     429          18 :   const auto output_options = [this, &max_len](const bool global)
     430             :   {
     431          18 :     Moose::out << (global ? "Global " : "") << "Options:\n" << std::left;
     432        1332 :     for (const auto & name_option_pair : _command_line_params)
     433             :     {
     434        1314 :       const auto & option = name_option_pair.second;
     435        1314 :       if (option.metadata.syntax.empty() || option.metadata.global != global)
     436         657 :         continue;
     437             : 
     438        1314 :       Moose::out << "  " << std::setw(max_len + 2) << option.metadata.syntax << option.description
     439         657 :                  << "\n";
     440             :     }
     441          18 :     Moose::out << "\n";
     442          18 :   };
     443             : 
     444           9 :   output_options(false);
     445           9 :   output_options(true);
     446             : 
     447           9 :   Moose::out << "Solver Options:\n"
     448           9 :              << "  See PETSc manual for details" << std::endl;
     449             : 
     450             :   // If we get here, we are not running a simulation and should silence petsc's unused options
     451             :   // warning
     452          27 :   Moose::PetscSupport::setSinglePetscOption("-options_left", "0");
     453           9 : }
     454             : 
     455             : std::vector<std::string>
     456       62781 : CommandLine::unusedHitParams(const Parallel::Communicator & comm) const
     457             : {
     458             :   libmesh_parallel_only(comm);
     459             : 
     460       62781 :   std::vector<const Entry *> hit_params;
     461       62781 :   std::vector<std::size_t> use_count;
     462      616761 :   for (const auto & entry : getEntries())
     463      553980 :     if (entry.hit_param)
     464             :     {
     465      110513 :       hit_params.push_back(&entry);
     466      110513 :       use_count.push_back(entry.used ? 1 : 0);
     467             :     }
     468             : 
     469             :   mooseAssert(comm.verify(use_count.size()), "Inconsistent HIT params across procs");
     470       62781 :   comm.sum(use_count);
     471             : 
     472       62781 :   std::vector<std::string> unused;
     473      173294 :   for (const auto i : index_range(use_count))
     474      110513 :     if (use_count[i] == 0)
     475           3 :       unused.push_back(hit_params[i]->raw_args[0]);
     476      125562 :   return unused;
     477       62781 : }
     478             : 
     479             : std::list<CommandLine::Entry>::const_iterator
     480     5027797 : CommandLine::findCommandLineParam(const std::string & name) const
     481             : {
     482     5027797 :   const auto find_param = _command_line_params.find(name);
     483     5027797 :   if (find_param == _command_line_params.end())
     484           2 :     mooseError("CommandLine::findCommandLineParam(): The parameter '",
     485             :                name,
     486             :                "' is not a command line parameter");
     487             : 
     488             :   // Search for the last thing that matches a switch from this param
     489     5027795 :   const auto & param = find_param->second;
     490    46796182 :   for (auto rit = _entries.rbegin(); rit != _entries.rend(); ++rit)
     491    86785483 :     for (const auto & search_switch : param.metadata.switches)
     492    45017096 :       if (rit->name == search_switch)
     493      457085 :         return --(rit.base());
     494             : 
     495     4570710 :   return getEntries().end();
     496             : }
     497             : 
     498             : std::list<CommandLine::Entry>::iterator
     499     5027788 : CommandLine::findCommandLineParam(const std::string & name)
     500             : {
     501     5027788 :   const auto it = std::as_const(*this).findCommandLineParam(name);
     502             :   // Easy way to go from a const iterator -> non-const iterator
     503    10055576 :   return _entries.erase(it, it);
     504             : }
     505             : 
     506             : std::string
     507      627868 : CommandLine::formatEntry(const CommandLine::Entry & entry) const
     508             : {
     509      627868 :   std::stringstream oss;
     510      627868 :   oss << entry.name;
     511      627868 :   if (entry.value)
     512             :   {
     513      250225 :     const auto q = (*entry.value).find(" ") != std::string::npos ? "'" : "";
     514      250225 :     oss << *entry.value_separator << q << *entry.value << q;
     515             :   }
     516     1255736 :   return oss.str();
     517      627868 : }

Generated by: LCOV version 1.14