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 : }
|