Visual Servoing Platform  version 3.6.1 under development (2024-11-15)
vpJsonArgumentParser.cpp
1 /*
2  * ViSP, open source Visual Servoing Platform software.
3  * Copyright (C) 2005 - 2024 by Inria. All rights reserved.
4  *
5  * This software is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  * See the file LICENSE.txt at the root directory of this source
10  * distribution for additional information about the GNU GPL.
11  *
12  * For using ViSP with software that can not be combined with the GNU
13  * GPL, please contact Inria about acquiring a ViSP Professional
14  * Edition License.
15  *
16  * See https://visp.inria.fr for more information.
17  *
18  * This software was developed at:
19  * Inria Rennes - Bretagne Atlantique
20  * Campus Universitaire de Beaulieu
21  * 35042 Rennes Cedex
22  * France
23  *
24  * If you have questions regarding the use of this file, please contact
25  * Inria at visp@inria.fr
26  *
27  * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
28  * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
29  */
30 
31 #include <visp3/core/vpConfig.h>
32 #include <visp3/io/vpJsonArgumentParser.h>
33 #include <visp3/core/vpException.h>
34 #include <fstream>
35 
36 #if defined(VISP_HAVE_NLOHMANN_JSON)
37 BEGIN_VISP_NAMESPACE
38 
39 using json = nlohmann::json;
40 
41 vpJsonArgumentParser::vpJsonArgumentParser(const std::string &description, const std::string &jsonFileArgumentName,
42  const std::string &nestSeparator) :
43  description(description),
44  jsonFileArgumentName(jsonFileArgumentName),
45  nestSeparator(nestSeparator)
46 {
47  if (jsonFileArgumentName.empty()) {
48  throw vpException(vpException::badValue, "The JSON file argument must not be empty");
49  }
50 
51  if (nestSeparator.empty()) {
52  throw vpException(vpException::badValue, "You must provide a JSON nesting delimiter to be able to parse JSON");
53  }
54 
55  helpers[jsonFileArgumentName] = []() -> std::string {
56  return "Path to the JSON configuration file. Values in this files are loaded, and can be overridden by command line arguments.\nOptional";
57  };
58 }
59 
60 std::string vpJsonArgumentParser::help() const
61 {
62  std::stringstream ss;
63 
64  ss << "Program description: " << description << std::endl;
65  ss << "Arguments: " << std::endl;
66  unsigned spacesBetweenArgAndDescription = 0;
67  for (const auto &helper : helpers) {
68  if (helper.first.size() > spacesBetweenArgAndDescription) {
69  spacesBetweenArgAndDescription = static_cast<unsigned int>(helper.first.size());
70  }
71  }
72  spacesBetweenArgAndDescription += 4;
73 
74  for (const auto &helper : helpers) {
75  std::stringstream argss(helper.second());
76  std::string line;
77  bool first = true;
78  while (getline(argss, line, '\n')) {
79  const unsigned lineSpace = first ? spacesBetweenArgAndDescription - static_cast<unsigned>(helper.first.size()) : spacesBetweenArgAndDescription;
80  const std::string spaceBetweenArgAndDescription(lineSpace, ' ');
81  if (first) {
82  ss << "\t" << helper.first << spaceBetweenArgAndDescription << line << std::endl;
83  }
84  else {
85  ss << "\t" << spaceBetweenArgAndDescription << line << std::endl;
86  }
87  first = false;
88 
89  }
90  ss << std::endl;
91  }
92  ss << "Example JSON configuration file: " << std::endl << std::endl;
93  ss << exampleJson.dump(2) << std::endl;
94  return ss.str();
95 }
96 
97 vpJsonArgumentParser &vpJsonArgumentParser::addFlag(const std::string &name, bool &parameter, const std::string &help)
98 {
99  argumentType[name] = FLAG;
100  const auto getter = [name, this](nlohmann::json &j, bool create) -> nlohmann::json * {
101  size_t pos = 0;
102  nlohmann::json *f = &j;
103  std::string token;
104  std::string name_copy = name;
105 
106  while ((pos = name_copy.find(nestSeparator)) != std::string::npos) {
107  token = name_copy.substr(0, pos);
108 
109  name_copy.erase(0, pos + nestSeparator.length());
110  if (create && !f->contains(token)) {
111  (*f)[token] = {};
112  }
113  else if (!f->contains(token)) {
114  return nullptr;
115  }
116  f = &(f->at(token));
117  }
118  if (create && !f->contains(name_copy)) {
119  (*f)[name_copy] = {};
120  }
121  else if (!f->contains(name_copy)) {
122  return nullptr;
123  }
124  f = &(f->at(name_copy));
125  return f;
126  };
127 
128  parsers[name] = [&parameter, getter, name](nlohmann::json &j) {
129  const nlohmann::json *field = getter(j, false);
130  const bool fieldHasNoValue = ((field == nullptr) || (field != nullptr && field->is_null()));
131  if (!fieldHasNoValue && (field->type() == json::value_t::boolean && (*field) == true)) {
132  parameter = !parameter;
133  }
134  };
135 
136  updaters[name] = [getter](nlohmann::json &j, const std::string &) {
137  nlohmann::json *field = getter(j, true);
138  *field = true;
139  };
140 
141  helpers[name] = [help, parameter]() -> std::string {
142  std::stringstream ss;
143  nlohmann::json repr = parameter;
144  ss << help << std::endl << "Default: " << repr;
145  return ss.str();
146  };
147 
148  nlohmann::json *exampleField = getter(exampleJson, true);
149  *exampleField = parameter;
150 
151  return *this;
152 }
153 
154 void vpJsonArgumentParser::parse(int argc, const char *argv[])
155 {
156  json j;
157  const std::vector<std::string> arguments(argv + 1, argv + argc);
158  std::vector<unsigned> ignoredArguments;
159  const auto jsonFileArgumentPos = std::find(arguments.begin(), arguments.end(), jsonFileArgumentName);
160  // Load JSON file if present
161  if (jsonFileArgumentPos != arguments.end()) {
162  ignoredArguments.push_back(static_cast<unsigned>(jsonFileArgumentPos - arguments.begin() + 1));
163  ignoredArguments.push_back(static_cast<unsigned>(jsonFileArgumentPos - arguments.begin() + 2));
164 
165  if (jsonFileArgumentPos == arguments.end() - 1) {
166  throw vpException(vpException::ioError, "No JSON file was provided");
167  }
168  const std::string jsonFileName = *(jsonFileArgumentPos + 1);
169  std::ifstream jsonFile(jsonFileName);
170  if (!jsonFile.good()) {
171  std::stringstream ss;
172  ss << "Could not open JSON file " << jsonFileName << "! Make sure it exists and is readable" << std::endl;
173  throw vpException(vpException::ioError, ss.str());
174  }
175  j = json::parse(jsonFile);
176  jsonFile.close();
177  }
178  // Parse command line arguments
179  for (int i = 1; i < argc; ++i) {
180  const std::string arg = argv[i];
181  bool stop_for_loop = false;
182  if (std::find(ignoredArguments.begin(), ignoredArguments.end(), i) != ignoredArguments.end()) {
183  stop_for_loop = true;
184  }
185  if (!stop_for_loop) {
186  if (arg == "-h" || arg == "--help") {
187  std::cout << help() << std::endl;
188  exit(1);
189  }
190 
191  if (parsers.find(arg) != parsers.end()) {
192  if (argumentType[arg] == WITH_FIELD) {
193  if (i < argc - 1) {
194  updaters[arg](j, std::string(argv[i + 1]));
195  ++i;
196  }
197  else {
198  std::stringstream ss;
199  ss << "Argument " << arg << " was passed but no value was provided" << std::endl;
200  throw vpException(vpException::ioError, ss.str());
201  }
202  }
203  else if (argumentType[arg] == FLAG) {
204  updaters[arg](j, std::string());
205  }
206  }
207  else {
208  std::cerr << "Unknown parameter when parsing: " << arg << std::endl;
209  }
210  }
211  }
212 
213  // Get the values from json document and store them in the arguments passed by ref in addArgument
214  for (const auto &parser : parsers) {
215  parser.second(j);
216  }
217 }
218 
219 END_VISP_NAMESPACE
220 
221 #endif
error that can be emitted by ViSP classes.
Definition: vpException.h:60
@ ioError
I/O error.
Definition: vpException.h:67
@ badValue
Used to indicate that a value is not in the allowed range.
Definition: vpException.h:73
Command line argument parsing with support for JSON files. If a JSON file is supplied,...
std::string help() const
Generate a help message, containing the description of the arguments, their default value and whether...
void parse(int argc, const char *argv[])
Parse the arguments.
vpJsonArgumentParser(const std::string &description, const std::string &jsonFileArgumentName, const std::string &nestSeparator)
Create a new argument parser, that can take into account both a JSON configuration file and command l...
vpJsonArgumentParser & addFlag(const std::string &name, bool &parameter, const std::string &help="No description")
Add an argument that acts as a flag when specified on the command line. When this flag is specified,...