Branch data Line data Source code
1 : : #pragma once
2 : :
3 : : /*
4 : : MIT License
5 : :
6 : : Copyright (c) 2014-2024 Stephane Cuillerdier (aka aiekick)
7 : :
8 : : Permission is hereby granted, free of charge, to any person obtaining a copy
9 : : of this software and associated documentation files (the "Software"), to deal
10 : : in the Software without restriction, including without limitation the rights
11 : : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 : : copies of the Software, and to permit persons to whom the Software is
13 : : furnished to do so, subject to the following conditions:
14 : :
15 : : The above copyright notice and this permission notice shall be included in all
16 : : copies or substantial portions of the Software.
17 : :
18 : : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 : : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 : : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 : : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 : : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 : : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 : : SOFTWARE.
25 : : */
26 : :
27 : : // ezArgs is part of the ezLibs project : https://github.com/aiekick/ezLibs.git
28 : :
29 : : #include "ezOS.hpp"
30 : :
31 : : #include <vector>
32 : : #include <string>
33 : : #include <cstdio> // FILENAME_MAX
34 : : #include <cstdint> // int32_t
35 : : #include <iostream>
36 : : #include <stdexcept>
37 : : #include <algorithm>
38 : :
39 : : #include "ezStr.hpp"
40 : :
41 : : #include "ezLog.hpp"
42 : :
43 : : namespace ez {
44 : :
45 : : class Args {
46 : : private:
47 : : class Argument {
48 : : friend class Args;
49 : : protected:
50 : : std::vector<std::string> m_base_args; // base args
51 : : std::set<std::string> m_full_args; // full args
52 : : char one_char_arg = 0;
53 : : std::string m_help_text;
54 : : std::string m_help_var_name;
55 : : std::string m_type;
56 : : char m_delimiter = 0; // delimiter used for arguments : toto a, toto=a, toto:a, etc...
57 : : bool m_is_present = false; // found during parsing
58 : : bool m_has_value = false; // found during parsing
59 : : std::string m_value; // value
60 : :
61 : : public:
62 : 29 : Argument() = default;
63 : :
64 : : template <typename TRETURN_TYPE>
65 : 26 : TRETURN_TYPE &help(const std::string &vHelp, const std::string &vVarName) {
66 : 26 : m_help_text = vHelp;
67 : 26 : m_help_var_name = vVarName;
68 : 26 : return *static_cast<TRETURN_TYPE *>(this);
69 : 26 : }
70 : :
71 : : template <typename TRETURN_TYPE>
72 : 0 : TRETURN_TYPE &def(const std::string &vDefValue) {
73 : 0 : m_value = vDefValue;
74 : 0 : return *static_cast<TRETURN_TYPE *>(this);
75 : 0 : }
76 : :
77 : : template <typename TRETURN_TYPE>
78 : 0 : TRETURN_TYPE &type(const std::string &vType) {
79 : 0 : m_type = vType;
80 : 0 : return *static_cast<TRETURN_TYPE *>(this);
81 : 0 : }
82 : :
83 : : template <typename TRETURN_TYPE>
84 : 9 : TRETURN_TYPE &delimiter(char vDelimiter) {
85 : 9 : m_delimiter = vDelimiter;
86 : 9 : return *static_cast<TRETURN_TYPE *>(this);
87 : 9 : }
88 : :
89 : : private:
90 : : typedef std::pair<std::string, std::string> HelpCnt;
91 : :
92 : 12 : HelpCnt m_getHelp(bool vPositional, size_t &vInOutFirstColSize) const {
93 : 12 : HelpCnt res;
94 : 12 : std::stringstream ss;
95 [ - + ]: 12 : if (vPositional) {
96 : 0 : std::string token = m_help_var_name;
97 [ # # ]: 0 : if (token.empty()) {
98 : 0 : token = *(m_base_args.begin());
99 : 0 : }
100 : 0 : ss << " " << token;
101 : 12 : } else {
102 : 12 : size_t idx = 0;
103 : 12 : ss << " ";
104 [ + + ]: 18 : for (const auto &arg : m_base_args) {
105 [ + + ]: 18 : if (idx++ > 0) {
106 : 6 : ss << ", ";
107 : 6 : }
108 : 18 : ss << arg;
109 : 18 : }
110 [ + + ]: 12 : if (!m_help_var_name.empty()) {
111 : 6 : ss << " " << m_help_var_name;
112 : 6 : }
113 : 12 : }
114 : 12 : auto ret = ss.str();
115 [ + + ]: 12 : if (vInOutFirstColSize < ret.size()) {
116 : 5 : vInOutFirstColSize = ret.size();
117 : 5 : }
118 : 12 : return std::make_pair(ret, m_help_text);
119 : 12 : }
120 : : };
121 : :
122 : : class PositionalArgument final : public Argument {
123 : : friend class Args;
124 : : public:
125 : 4 : PositionalArgument &help(const std::string &vHelp, const std::string &vVarName = {}) { return Argument::help<PositionalArgument>(vHelp, vVarName); }
126 : 0 : PositionalArgument &def(const std::string &vDefValue) { return Argument::def<PositionalArgument>(vDefValue); }
127 : 0 : PositionalArgument &type(const std::string &vType) { return Argument::type<PositionalArgument>(vType); }
128 : 0 : PositionalArgument &delimiter(char vDelimiter) { return Argument::delimiter<PositionalArgument>(vDelimiter); }
129 : : };
130 : :
131 : : class OptionalArgument final : public Argument {
132 : : friend class Args;
133 : : private:
134 : : bool m_required = false;
135 : : public:
136 : 22 : OptionalArgument &help(const std::string &vHelp, const std::string &vVarName = {}) { return Argument::help<OptionalArgument>(vHelp, vVarName); }
137 : 0 : OptionalArgument &def(const std::string &vDefValue) { return Argument::def<OptionalArgument>(vDefValue); }
138 : 0 : OptionalArgument &type(const std::string &vType) { return Argument::type<OptionalArgument>(vType); }
139 : 9 : OptionalArgument &delimiter(char vDelimiter) { return Argument::delimiter<OptionalArgument>(vDelimiter); }
140 : 1 : OptionalArgument &required(bool vValue) {
141 : 1 : m_required = vValue;
142 : 1 : return *this;
143 : 1 : }
144 : : };
145 : :
146 : : private:
147 : : std::string m_AppName;
148 : : std::string m_HelpHeader;
149 : : std::string m_HelpFooter;
150 : : std::string m_HelpDescription;
151 : : OptionalArgument m_HelpArgument;
152 : : std::vector<PositionalArgument> m_Positionals;
153 : : std::vector<OptionalArgument> m_Optionals;
154 : : std::vector<std::string> m_errors;
155 : :
156 : : public:
157 : : Args() = default;
158 : 7 : virtual ~Args() = default;
159 : 7 : Args(const std::string &vName) : m_AppName(vName) {
160 [ - + ]: 7 : if (vName.empty()) {
161 : 0 : throw std::runtime_error("Name cant be empty");
162 : 0 : }
163 : 7 : m_addOptional(m_HelpArgument, "-h/--help").help("Show the usage");
164 : 7 : }
165 : :
166 : 2 : Args &addHeader(const std::string &vHeader) {
167 : 2 : m_HelpHeader = vHeader;
168 : 2 : return *this;
169 : 2 : }
170 : :
171 : 2 : Args &addFooter(const std::string &vFooter) {
172 : 2 : m_HelpFooter = vFooter;
173 : 2 : return *this;
174 : 2 : }
175 : :
176 : 1 : Args &addDescription(const std::string &vDescription) {
177 : 1 : m_HelpDescription = vDescription;
178 : 1 : return *this;
179 : 1 : }
180 : :
181 : 4 : PositionalArgument &addPositional(const std::string &vKey) {
182 [ - + ]: 4 : if (vKey.empty()) {
183 : 0 : throw std::runtime_error("argument cant be empty");
184 : 0 : }
185 : 4 : PositionalArgument res;
186 : 4 : res.m_base_args = ez::str::splitStringToVector(vKey, '/');
187 [ + + ]: 4 : for (const auto &a : res.m_base_args) {
188 : 4 : res.m_full_args.emplace(a);
189 : 4 : }
190 : 4 : m_Positionals.push_back(res);
191 : 4 : return m_Positionals.back();
192 : 4 : }
193 : :
194 : 18 : OptionalArgument &addOptional(const std::string &vKey) {
195 [ - + ]: 18 : if (vKey.empty()) {
196 : 0 : throw std::runtime_error("optional cant be empty");
197 : 0 : }
198 : 18 : OptionalArgument res;
199 : 18 : m_addOptional(res, vKey);
200 : 18 : m_Optionals.push_back(res);
201 : 18 : return m_Optionals.back();
202 : 18 : }
203 : :
204 : : // is token present
205 : 23 : bool isPresent(const std::string &vKey) {
206 : 23 : auto *ptr = m_getArgumentPtr(vKey, true);
207 [ + + ]: 23 : if (ptr != nullptr) {
208 : 21 : return ptr->m_is_present;
209 : 21 : }
210 : 2 : return false;
211 : 23 : }
212 : :
213 : : // is token have value
214 : 10 : bool hasValue(const std::string &vKey) {
215 : 10 : auto *ptr = m_getArgumentPtr(vKey, true);
216 [ + + ]: 10 : if (ptr != nullptr) {
217 : 9 : return ptr->m_has_value;
218 : 9 : }
219 : 1 : return false;
220 : 10 : }
221 : :
222 : : template <typename T>
223 : 21 : T getValue(const std::string &vKey, bool vNoExcept = false) const {
224 : 21 : auto *ptr = m_getArgumentPtr(vKey, vNoExcept);
225 [ + + ][ + - ]: 21 : if (ptr != nullptr && !ptr->m_value.empty()) {
[ + + ][ + + ]
226 : 11 : return m_convertString<T>(ptr->m_value);
227 : 11 : }
228 : 10 : return {};
229 : 21 : }
230 : :
231 : : std::string getHelp( //
232 : : const std::string &vPositionalHeader = "Positionnal arguments",
233 : 4 : const std::string &vOptionalHeader = "Optional arguments") const {
234 : 4 : std::string token;
235 : 4 : std::stringstream ss;
236 [ + + ]: 4 : if (!m_HelpHeader.empty()) {
237 : 2 : ss << m_HelpHeader << std::endl << std::endl;
238 : 2 : }
239 : 4 : ss << m_getCmdLineHelp();
240 : 4 : ss << std::endl;
241 [ + + ]: 4 : if (!m_HelpDescription.empty()) {
242 : 1 : ss << std::endl << " " << m_HelpDescription << std::endl;
243 : 1 : }
244 : 4 : ss << m_getHelpDetails(vPositionalHeader, vOptionalHeader);
245 [ + + ]: 4 : if (!m_HelpFooter.empty()) {
246 : 2 : ss << std::endl << m_HelpFooter << std::endl;
247 : 2 : }
248 : 4 : return ss.str();
249 : 4 : }
250 : :
251 : 4 : void printHelp() const { std::cout << getHelp() << std::endl; }
252 : :
253 : 7 : bool parse(const int32_t vArgc, char **vArgv, const int32_t vStartIdx = 1U) {
254 : 7 : size_t positional_idx = 0;
255 [ + + ]: 24 : for (int32_t idx = vStartIdx; idx < vArgc; ++idx) {
256 : 18 : std::string arg = m_trim(vArgv[idx]);
257 : :
258 : : // print help
259 [ + + ]: 18 : if (m_HelpArgument.m_full_args.find(arg) != m_HelpArgument.m_full_args.end()) {
260 : 1 : printHelp();
261 : 1 : return m_errors.empty();
262 : 1 : }
263 : :
264 : : // check optionals
265 : 17 : std::string token = arg;
266 : 17 : std::string value;
267 : 17 : bool is_optional = false;
268 : 17 : bool check_for_value = false;
269 [ + + ]: 42 : for (auto &arg_ref : m_Optionals) {
270 : 42 : check_for_value = false;
271 [ + + ]: 42 : if (arg_ref.m_delimiter != 0) {
272 [ + + ]: 24 : if (arg_ref.m_delimiter != ' ') {
273 [ + + ]: 14 : if (token.find(arg_ref.m_delimiter) != std::string::npos) {
274 : 3 : auto arr = ez::str::splitStringToVector(token, arg_ref.m_delimiter);
275 [ + - ]: 3 : if (arr.size() == 2) {
276 : 3 : token = arr.at(0);
277 : 3 : value = arr.at(1);
278 : 3 : } else {
279 [ # # ]: 0 : if (arr.size() < 2) {
280 : 0 : m_addError("bad parsing of key \"" + token + "\". no value");
281 [ # # ]: 0 : } else if (arr.size() > 2) {
282 : 0 : m_addError("bad parsing of key \"" + token + "\". more than one value");
283 : 0 : }
284 : 0 : }
285 : 3 : }
286 : 14 : }
287 : 24 : }
288 [ + + ]: 42 : if (arg_ref.one_char_arg != 0) {
289 : 21 : auto p = token.find(arg_ref.one_char_arg);
290 [ + + ]: 21 : if (p != std::string::npos) {
291 : 9 : arg_ref.m_is_present = true;
292 : 9 : is_optional = true;
293 [ + + ]: 9 : if (p == (token.size() - 1)) {
294 : 2 : check_for_value = true;
295 : 2 : }
296 : 9 : }
297 [ + + ]: 21 : } else if (arg_ref.m_full_args.find(token) != arg_ref.m_full_args.end()) {
298 : 8 : arg_ref.m_is_present = true;
299 : 8 : is_optional = true;
300 : 8 : check_for_value = true;
301 : 8 : }
302 [ + + ]: 42 : if (check_for_value) {
303 [ + + ]: 10 : if (arg_ref.m_delimiter == ' ') {
304 [ + - ]: 4 : if ((idx + 1) < vArgc) {
305 : 4 : auto *existingArg = m_getArgumentPtr(vArgv[idx + 1], true);
306 [ + + ]: 4 : if (!existingArg) {
307 : 3 : arg_ref.m_value = vArgv[++idx];
308 : 3 : arg_ref.m_has_value = true;
309 : 3 : }
310 : 4 : }
311 [ + + ]: 6 : } else if (arg_ref.m_delimiter != 0) {
312 : 5 : auto *existingArg = m_getArgumentPtr(value, true);
313 [ + - ]: 5 : if (!existingArg) {
314 : 5 : arg_ref.m_value = value;
315 : 5 : arg_ref.m_has_value = true;
316 : 5 : }
317 : 5 : }
318 : 10 : }
319 : 42 : }
320 : :
321 : : // check positionals
322 [ + + ]: 17 : if (!is_optional) {
323 [ + + ]: 7 : if (positional_idx < m_Positionals.size()) {
324 : 4 : auto &positional = m_Positionals.at(positional_idx);
325 : 4 : positional.m_is_present = true;
326 : 4 : positional.m_value = arg;
327 : 4 : positional.m_has_value = true;
328 : 4 : ++positional_idx;
329 : 4 : }
330 : 7 : }
331 : 17 : }
332 : :
333 : : // check if required fields are not not seen during parsing
334 [ + + ]: 6 : for (const auto &pos : m_Positionals) {
335 : : // not seen during parsing
336 [ - + ]: 4 : if (!pos.m_is_present) {
337 : : // its normally impossible than m_base_args can be empty
338 : 0 : m_addError("Positional <" + pos.m_base_args.at(0) + "> not present");
339 : 0 : }
340 : 4 : }
341 [ + + ]: 17 : for (const auto &opt : m_Optionals) {
342 : : // required but not seen during parsing
343 [ + + ][ + - ]: 17 : if (opt.m_required && !opt.m_is_present) {
344 : : // its normally impossible than m_base_args can be empty
345 : 1 : m_addError("Optional <" + opt.m_base_args.at(0) + "> not present");
346 : 1 : }
347 : 17 : }
348 : :
349 : 6 : return m_errors.empty();
350 : 7 : }
351 : :
352 : 2 : const std::vector<std::string> &getErrors() { return m_errors; }
353 : :
354 : : private:
355 : 63 : const Argument *m_getArgumentPtr(const std::string &vKey, bool vNoExcept = false) const {
356 : 63 : const Argument *ret = nullptr;
357 [ + + ]: 63 : for (const auto &arg : m_Positionals) {
358 [ + + ]: 31 : if (arg.m_full_args.find(vKey) != arg.m_full_args.end()) {
359 : 8 : ret = &arg;
360 : 8 : }
361 : 31 : }
362 [ + + ]: 63 : if (ret == nullptr) {
363 [ + + ]: 211 : for (const auto &arg : m_Optionals) {
364 [ + + ]: 211 : if (arg.m_full_args.find(vKey) != arg.m_full_args.end()) {
365 : 43 : ret = &arg;
366 : 43 : }
367 : 211 : }
368 : 55 : }
369 [ + + ][ - + ]: 63 : if (ret == nullptr && vNoExcept == false) {
370 : 0 : throw std::runtime_error("Argument not found");
371 : 0 : }
372 : 63 : return ret;
373 : 63 : }
374 : :
375 : 25 : OptionalArgument &m_addOptional(OptionalArgument &vInOutArgument, const std::string &vKey) {
376 [ - + ]: 25 : if (vKey.empty()) {
377 : 0 : throw std::runtime_error("optional cant be empty");
378 : 0 : }
379 : 25 : vInOutArgument.m_base_args = ez::str::splitStringToVector(vKey, '/');
380 [ + + ]: 38 : for (const auto &a : vInOutArgument.m_base_args) {
381 : 38 : vInOutArgument.m_full_args.emplace(a);
382 : 38 : }
383 [ + + ]: 38 : for (const auto &arg : vInOutArgument.m_base_args) {
384 : : // tofix : may fail if arg is --toto-titi.
385 : : // we will get titi but we want toto-titi
386 : 38 : vInOutArgument.m_full_args.emplace(m_trim(arg));
387 : 38 : }
388 [ + + ]: 25 : vInOutArgument.one_char_arg = (vKey.size() == 1U) ? vKey[0] : 0;
389 : 25 : return vInOutArgument;
390 : 25 : }
391 : :
392 : 16 : void m_getCmdLineOptional(const OptionalArgument &vOptionalArgument, std::stringstream &vStream) const {
393 : 16 : vStream << " [";
394 : 16 : size_t idx = 0;
395 [ + + ]: 26 : for (const auto &o : vOptionalArgument.m_base_args) {
396 [ + + ]: 26 : if (idx++ > 0) {
397 : 10 : vStream << ':';
398 : 10 : }
399 : 26 : vStream << o;
400 : 26 : }
401 [ + + ]: 16 : if (!vOptionalArgument.m_help_var_name.empty()) {
402 : 6 : vStream << " " << vOptionalArgument.m_help_var_name;
403 : 6 : }
404 : 16 : vStream << "]";
405 : 16 : }
406 : 0 : void m_getCmdLinePositional(const PositionalArgument &vPositionalArgument, std::stringstream &vStream) const {
407 : 0 : std::string token = vPositionalArgument.m_help_var_name;
408 [ # # ]: 0 : if (token.empty()) {
409 : 0 : token = *(vPositionalArgument.m_base_args.begin());
410 : 0 : }
411 : 0 : vStream << " " << token;
412 : 0 : }
413 : 4 : std::string m_getCmdLineHelp() const {
414 : 4 : std::stringstream ss;
415 : 4 : ss << " Usage : " << m_AppName;
416 : 4 : m_getCmdLineOptional(m_HelpArgument, ss);
417 [ + + ]: 12 : for (const auto &arg : m_Optionals) {
418 : 12 : m_getCmdLineOptional(arg, ss);
419 : 12 : }
420 [ - + ]: 4 : for (const auto &arg : m_Positionals) {
421 : 0 : m_getCmdLinePositional(arg, ss);
422 : 0 : }
423 : 4 : return ss.str();
424 : 4 : }
425 : :
426 : 0 : void m_clearErrors() { m_errors.clear(); }
427 : 1 : void m_addError(const std::string &vError) { m_errors.push_back("Error : " + vError); }
428 : :
429 : : std::string m_getHelpDetails( //
430 : : const std::string &vPositionalHeader,
431 : 4 : const std::string &vOptionalHeader) const {
432 : : // collect infos with padding
433 : 4 : size_t first_col_size = 0U;
434 : 4 : std::vector<Argument::HelpCnt> cnt_pos;
435 [ - + ]: 4 : for (const auto &arg : m_Positionals) {
436 : 0 : cnt_pos.push_back(arg.m_getHelp(true, first_col_size));
437 : 0 : }
438 : 4 : std::vector<Argument::HelpCnt> cnt_opt;
439 [ + + ]: 12 : for (const auto &opt : m_Optionals) {
440 : 12 : cnt_opt.push_back(opt.m_getHelp(false, first_col_size));
441 : 12 : }
442 : : // display
443 : 4 : first_col_size += 4U;
444 : 4 : std::stringstream ss;
445 [ - + ]: 4 : if (!cnt_pos.empty()) {
446 : 0 : ss << std::endl << " " << vPositionalHeader << " : " << std::endl;
447 [ # # ]: 0 : for (const auto &it : cnt_pos) {
448 : 0 : ss << it.first << std::string(first_col_size - it.first.size(), ' ') << it.second << std::endl;
449 : 0 : }
450 : 0 : }
451 [ + - ]: 4 : if (!cnt_opt.empty()) {
452 : 4 : ss << std::endl << " " << vOptionalHeader << " : " << std::endl;
453 [ + + ]: 12 : for (const auto &it : cnt_opt) {
454 : 12 : ss << it.first << std::string(first_col_size - it.first.size(), ' ') << it.second << std::endl;
455 : 12 : }
456 : 4 : }
457 : 4 : return ss.str();
458 : 4 : }
459 : :
460 : : // remove the first '-' of a token
461 : 56 : std::string m_trim(const std::string &vToken) {
462 : 56 : auto short_last_minus = vToken.find_first_not_of("-");
463 [ + - ]: 56 : if (short_last_minus != std::string::npos) {
464 : 56 : return vToken.substr(short_last_minus);
465 : 56 : }
466 : 0 : return {};
467 : 56 : }
468 : :
469 : : template <typename T>
470 : 11 : T m_convertString(const std::string &str) const {
471 : 11 : std::istringstream iss(str);
472 : 11 : T value;
473 [ - + ][ - + ]: 11 : if (!(iss >> value)) {
474 : 0 : throw std::runtime_error("Conversion failed");
475 : 0 : }
476 : 11 : return value;
477 : 11 : }
478 : : };
479 : :
480 : : template <>
481 : 0 : inline bool Args::m_convertString<bool>(const std::string &str) const {
482 : 0 : if (str == "true" || str == "1") {
483 : 0 : return true;
484 : 0 : } else if (str == "false" || str == "0") {
485 : 0 : return false;
486 : 0 : }
487 : 0 : throw std::runtime_error("Invalid boolean string");
488 : 0 : }
489 : : } // namespace ez
|