1//! @file argument.cpp
2//! @author ryftchen
3//! @brief The definitions (argument) in the utility module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2025 ryftchen. All rights reserved.
6
7#include "argument.hpp"
8
9#include <iomanip>
10#include <limits>
11#include <numeric>
12#include <ranges>
13#include <utility>
14
15namespace utility::argument
16{
17//! @brief Function version number.
18//! @return version number (major.minor.patch)
19const char* version() noexcept
20{
21 static const char* const ver = "0.1.0";
22 return ver;
23}
24
25ArgsNumRange::ArgsNumRange(const std::size_t minimum, const std::size_t maximum) : min{minimum}, max{maximum}
26{
27 if (minimum > maximum)
28 {
29 throw std::runtime_error{"The range of number of arguments is invalid."};
30 }
31}
32
33bool ArgsNumRange::operator==(const ArgsNumRange& rhs) const
34{
35 return std::tie(args: rhs.min, args: rhs.max) == std::tie(args: min, args: max);
36}
37
38bool ArgsNumRange::operator!=(const ArgsNumRange& rhs) const
39{
40 return !(rhs == *this);
41}
42
43bool ArgsNumRange::within(const std::size_t value) const
44{
45 return (value >= min) && (value <= max);
46}
47
48bool ArgsNumRange::isExact() const
49{
50 return min == max;
51}
52
53bool ArgsNumRange::existRightBound() const
54{
55 return max < std::numeric_limits<std::size_t>::max();
56}
57
58//! @brief The operator (<<) overloading of the ArgsNumRange class.
59//! @param os - output stream object
60//! @param range - specific ArgsNumRange object
61//! @return reference of the output stream object
62std::ostream& operator<<(std::ostream& os, const ArgsNumRange& range)
63{
64 const auto numMin = range.min;
65 const auto numMax = range.max;
66 if (numMin == numMax)
67 {
68 if ((numMin != 0) && (numMin != 1))
69 {
70 os << "[args: " << numMin << "] ";
71 }
72 }
73 else if (numMax == std::numeric_limits<std::size_t>::max())
74 {
75 os << "[args: " << numMin << " or more] ";
76 }
77 else
78 {
79 os << "[args=" << numMin << ".." << numMax << "] ";
80 }
81 return os;
82}
83
84ArgTrait& ArgTrait::help(const std::string_view message)
85{
86 helpMsg = message;
87 return *this;
88}
89
90ArgTrait& ArgTrait::metaVariable(const std::string_view variable)
91{
92 metaVar = variable;
93 return *this;
94}
95
96ArgTrait& ArgTrait::defaultValue(const std::string_view value)
97{
98 return defaultValue(value: std::string{value});
99}
100
101ArgTrait& ArgTrait::implicitValue(std::any value)
102{
103 implicitVal = std::move(value);
104 argsNumRange = ArgsNumRange{0, 0};
105 return *this;
106}
107
108ArgTrait& ArgTrait::required()
109{
110 isRequired = true;
111 return *this;
112}
113
114ArgTrait& ArgTrait::appending()
115{
116 isRepeatable = true;
117 return *this;
118}
119
120ArgTrait& ArgTrait::remaining()
121{
122 optionalAsValue = true;
123 return argsNum(pattern: ArgsNumPattern::any);
124}
125
126ArgTrait& ArgTrait::argsNum(const std::size_t num)
127{
128 argsNumRange = ArgsNumRange{num, num};
129 return *this;
130}
131
132ArgTrait& ArgTrait::argsNum(const std::size_t numMin, const std::size_t numMax)
133{
134 argsNumRange = ArgsNumRange{numMin, numMax};
135 return *this;
136}
137
138ArgTrait& ArgTrait::argsNum(const ArgsNumPattern pattern)
139{
140 switch (pattern)
141 {
142 case ArgsNumPattern::optional:
143 argsNumRange = ArgsNumRange{0, 1};
144 break;
145 case ArgsNumPattern::any:
146 argsNumRange = ArgsNumRange{0, std::numeric_limits<std::size_t>::max()};
147 break;
148 case ArgsNumPattern::atLeastOne:
149 argsNumRange = ArgsNumRange{1, std::numeric_limits<std::size_t>::max()};
150 break;
151 default:
152 break;
153 }
154 return *this;
155}
156
157void ArgTrait::validate() const
158{
159 if (isOptional)
160 {
161 if (!isUsed && !defaultVal.has_value() && isRequired)
162 {
163 throw std::runtime_error{names.at(n: 0) + ": required."};
164 }
165 if (isUsed && isRequired && values.empty())
166 {
167 throw std::runtime_error{usedName + ": no value provided."};
168 }
169 }
170 else if (!argsNumRange.within(value: values.size()) && !defaultVal.has_value())
171 {
172 throwInvalidArgsNumRange();
173 }
174}
175
176std::string ArgTrait::getInlineUsage() const
177{
178 std::string longestName(names.at(n: 0));
179 for (const auto& str : names)
180 {
181 if (str.length() > longestName.length())
182 {
183 longestName = str;
184 }
185 }
186 std::ostringstream out{};
187 if (!isRequired)
188 {
189 out << '[';
190 }
191 out << longestName;
192 const auto var = metaVar.empty() ? "VAR" : metaVar;
193 if (const auto numMax = argsNumRange.max; numMax > 0)
194 {
195 out << ' ' << var;
196 if (numMax > 1)
197 {
198 out << "...";
199 }
200 }
201 if (!isRequired)
202 {
203 out << ']';
204 }
205 return std::move(out).str();
206}
207
208std::size_t ArgTrait::getArgumentsLength() const
209{
210 const std::size_t namesSize = std::accumulate(
211 first: names.cbegin(), last: names.cend(), init: 0, binary_op: [](const auto sum, const auto& str) { return sum + str.length(); });
212 if (checkIfPositional(name: names.at(n: 0), prefix: prefixChars))
213 {
214 return metaVar.empty() ? (namesSize + (names.size() - 1)) : metaVar.length();
215 }
216
217 std::size_t size = namesSize + (2 * (names.size() - 1));
218 if (!metaVar.empty() && (argsNumRange == ArgsNumRange{1, 1}))
219 {
220 size += metaVar.length() + 1;
221 }
222 return size;
223}
224
225void ArgTrait::throwInvalidArgsNumRange() const
226{
227 const auto numMin = argsNumRange.min;
228 const auto numMax = argsNumRange.max;
229 std::ostringstream out{};
230 out << (usedName.empty() ? names.at(n: 0) : usedName) << ": " << numMin;
231 if (!argsNumRange.isExact())
232 {
233 if (argsNumRange.existRightBound())
234 {
235 out << " to " << numMax;
236 }
237 else
238 {
239 out << " or more";
240 }
241 }
242 out << " argument(s) expected. " << values.size() << " provided.";
243 throw std::runtime_error{out.str()};
244}
245
246int ArgTrait::lookAhead(const std::string_view name)
247{
248 return name.empty() ? eof : static_cast<int>(static_cast<unsigned char>(name.front()));
249}
250
251bool ArgTrait::checkIfOptional(const std::string_view name, const std::string_view prefix)
252{
253 return !checkIfPositional(name, prefix);
254}
255
256bool ArgTrait::checkIfPositional(const std::string_view name, const std::string_view prefix)
257{
258 const int first = lookAhead(name);
259 if (first == eof)
260 {
261 return true;
262 }
263 if (prefix.find(c: static_cast<char>(first)) != std::string_view::npos)
264 {
265 std::string_view remain(name);
266 remain.remove_prefix(n: 1);
267 return remain.empty();
268 }
269 return true;
270}
271
272//! @brief Join a series of strings into a single string using a separator.
273//! @tparam StrIter - type of iterator
274//! @param first - iterator pointing to the beginning of the range
275//! @param last - iterator pointing to the end of the range
276//! @param separator - separator to be used between strings
277//! @return joined string
278template <typename StrIter>
279static std::string join(StrIter first, StrIter last, const std::string_view separator)
280{
281 if (first == last)
282 {
283 return {};
284 }
285
286 std::ostringstream out{};
287 out << *first;
288 ++first;
289 while (first != last)
290 {
291 out << separator << *first;
292 ++first;
293 }
294 return std::move(out).str();
295}
296
297//! @brief The operator (<<) overloading of the ArgTrait class.
298//! @param os - output stream object
299//! @param tra - specific ArgTrait object
300//! @return reference of the output stream object
301std::ostream& operator<<(std::ostream& os, const ArgTrait& tra)
302{
303 std::ostringstream out{};
304 if (tra.checkIfPositional(name: tra.names.at(n: 0), prefix: tra.prefixChars))
305 {
306 out << (tra.metaVar.empty() ? join(first: tra.names.cbegin(), last: tra.names.cend(), separator: " ") : tra.metaVar);
307 }
308 else
309 {
310 out << join(first: tra.names.cbegin(), last: tra.names.cend(), separator: ", ");
311 if (!tra.metaVar.empty() && (tra.argsNumRange == ArgsNumRange{1, 1}))
312 {
313 out << ' ' << tra.metaVar;
314 }
315 }
316
317 const auto streamWidth = os.width();
318 const auto namePadding = std::string(out.str().length(), ' ');
319 os << out.str();
320
321 std::size_t pos = 0;
322 std::size_t prev = 0;
323 bool firstLine = true;
324 const std::string_view helpView = tra.helpMsg;
325 const char* tabSpace = " ";
326 while ((pos = tra.helpMsg.find(c: '\n', pos: prev)) != std::string::npos)
327 {
328 const auto line = helpView.substr(pos: prev, n: pos - prev + 1);
329 if (firstLine)
330 {
331 os << tabSpace << line;
332 firstLine = false;
333 }
334 else
335 {
336 os.width(wide: streamWidth);
337 os << namePadding << tabSpace << line;
338 }
339 prev += pos - prev + 1;
340 }
341
342 if (firstLine)
343 {
344 os << tabSpace << tra.helpMsg;
345 }
346 else
347 {
348 const auto remain = helpView.substr(pos: prev, n: tra.helpMsg.length() - prev);
349 if (!remain.empty())
350 {
351 os.width(wide: streamWidth);
352 os << namePadding << tabSpace << remain;
353 }
354 }
355
356 if (!tra.helpMsg.empty())
357 {
358 os << ' ';
359 }
360 os << tra.argsNumRange;
361
362 if (tra.defaultVal.has_value() && (tra.argsNumRange != ArgsNumRange{0, 0}))
363 {
364 os << "[default: " << tra.representedDefVal << ']';
365 }
366 else if (tra.isRequired)
367 {
368 os << "[required]";
369 }
370 os << '\n';
371 return os;
372}
373
374Argument::Argument(const Argument& arg) :
375 titleName{arg.titleName},
376 versionNumber{arg.versionNumber},
377 descrText{arg.descrText},
378 prefixChars{arg.prefixChars},
379 assignChars{arg.assignChars},
380 isParsed{arg.isParsed},
381 optionalArgs{arg.optionalArgs},
382 positionalArgs{arg.positionalArgs},
383 parserPath{arg.parserPath},
384 subParsers{arg.subParsers}
385{
386 for (auto iterator = optionalArgs.begin(); iterator != optionalArgs.end(); ++iterator)
387 {
388 indexArgument(iterator);
389 }
390 for (auto iterator = positionalArgs.begin(); iterator != positionalArgs.end(); ++iterator)
391 {
392 indexArgument(iterator);
393 }
394 for (auto iterator = subParsers.begin(); iterator != subParsers.end(); ++iterator)
395 {
396 subParserMap.insert_or_assign(k: iterator->get().titleName, obj&: iterator);
397 subParserUsed.insert_or_assign(k: iterator->get().titleName, obj: false);
398 }
399}
400
401Argument& Argument::operator=(const Argument& arg)
402{
403 if (&arg != this)
404 {
405 auto temp = arg;
406 std::swap(a&: *this, b&: temp);
407 }
408 return *this;
409}
410
411Argument::operator bool() const
412{
413 const bool isArgUsed =
414 std::any_of(first: argumentMap.cbegin(), last: argumentMap.cend(), pred: [](const auto& iter) { return iter.second->isUsed; });
415 const bool isSubParserUsed =
416 std::any_of(first: subParserUsed.cbegin(), last: subParserUsed.cend(), pred: [](const auto& iter) { return iter.second; });
417 return isParsed && (isArgUsed || isSubParserUsed);
418}
419
420ArgTrait& Argument::operator[](const std::string_view argName) const
421{
422 auto iterator = argumentMap.find(x: argName);
423 if (iterator != argumentMap.cend())
424 {
425 return *(iterator->second);
426 }
427
428 if (!isValidPrefixChar(c: argName.at(pos: 0)))
429 {
430 const char legalPrefixChar = getAnyValidPrefixChar();
431 const auto prefix = std::string(1, legalPrefixChar);
432
433 const auto name = prefix + argName.data();
434 iterator = argumentMap.find(x: name);
435 if (iterator != argumentMap.cend())
436 {
437 return *(iterator->second);
438 }
439
440 iterator = argumentMap.find(x: prefix + name);
441 if (iterator != argumentMap.cend())
442 {
443 return *(iterator->second);
444 }
445 }
446 throw std::runtime_error{"No such argument: " + std::string{argName} + '.'};
447}
448
449Argument& Argument::addDescription(const std::string_view text)
450{
451 descrText = text;
452 return *this;
453}
454
455void Argument::parseArgs(const std::vector<std::string>& arguments)
456{
457 parseArgsInternal(rawArguments: arguments);
458 for (const auto& argument : std::views::values(argumentMap))
459 {
460 argument->validate();
461 }
462}
463
464void Argument::parseArgs(const int argc, const char* const argv[])
465{
466 if (argc < 0)
467 {
468 throw std::runtime_error{"The argc must be non-negative."};
469 }
470 if (!argv)
471 {
472 throw std::runtime_error{"The argv is null."};
473 }
474 for (int i = 0; i < argc; ++i)
475 {
476 if (!argv[i])
477 {
478 throw std::runtime_error{"The argv contains a null pointer at index " + std::to_string(val: i)};
479 }
480 }
481
482 parseArgs(arguments: std::vector<std::string>{argv, argv + argc});
483}
484
485bool Argument::isUsed(const std::string_view argName) const
486{
487 return (*this)[argName].isUsed;
488}
489
490bool Argument::isSubCommandUsed(const std::string_view subCommandName) const
491{
492 return subParserUsed.at(k: subCommandName);
493}
494
495bool Argument::isSubCommandUsed(const Argument& subParser) const
496{
497 return isSubCommandUsed(subCommandName: subParser.titleName);
498}
499
500void Argument::clearUsed()
501{
502 isParsed = false;
503 constexpr auto resetting = [](ArgTrait& tra) constexpr
504 {
505 tra.isUsed = false;
506 tra.usedName.clear();
507 tra.values.clear();
508 };
509
510 for (auto& argument : optionalArgs)
511 {
512 resetting(argument);
513 }
514 for (auto& argument : positionalArgs)
515 {
516 resetting(argument);
517 }
518
519 for ([[maybe_unused]] auto& [name, isUsed] : subParserUsed)
520 {
521 isUsed = false;
522 }
523 for (auto& subParser : subParsers)
524 {
525 subParser.get().clearUsed();
526 }
527}
528
529std::string Argument::title() const
530{
531 return titleName;
532}
533
534std::string Argument::version() const
535{
536 return versionNumber;
537}
538
539std::ostringstream Argument::help() const
540{
541 std::ostringstream out{};
542 out << *this;
543 return out;
544}
545
546std::string Argument::usage() const
547{
548 std::ostringstream out{};
549 out << "usage: " << ((parserPath.find(str: ' ' + titleName) == std::string::npos) ? titleName : parserPath);
550
551 for (const auto& argument : optionalArgs)
552 {
553 out << ' ' << argument.getInlineUsage();
554 }
555 for (const auto& argument : positionalArgs)
556 {
557 out << ' ' << (argument.metaVar.empty() ? argument.names.at(n: 0) : argument.metaVar);
558 }
559
560 if (!subParserMap.empty())
561 {
562 out << " {";
563 for (std::size_t i = 0; const auto& command : std::views::keys(subParserMap))
564 {
565 if (i != 0)
566 {
567 out << ',';
568 }
569 out << command;
570 ++i;
571 }
572 out << '}';
573 }
574 return std::move(out).str();
575}
576
577void Argument::addSubParser(Argument& parser)
578{
579 parser.parserPath = titleName + ' ' + parser.titleName;
580 const auto iterator = subParsers.emplace(position: subParsers.cend(), args&: parser);
581 subParserMap.insert_or_assign(k: parser.titleName, obj: iterator);
582 subParserUsed.insert_or_assign(k: parser.titleName, obj: false);
583}
584
585bool Argument::isValidPrefixChar(const char c) const
586{
587 return prefixChars.find(c: c) != std::string::npos;
588}
589
590char Argument::getAnyValidPrefixChar() const
591{
592 return prefixChars.at(n: 0);
593}
594
595std::vector<std::string> Argument::preprocessArguments(const std::vector<std::string>& rawArguments) const
596{
597 std::vector<std::string> arguments{};
598 const auto startWithPrefixChars = [this](const std::string_view str)
599 {
600 const auto legalPrefix = [this](const char c) { return prefixChars.find(c: c) != std::string::npos; };
601 return (str.length() > 1) && (legalPrefix(str.at(pos: 0)) && legalPrefix(str.at(pos: 1)));
602 };
603
604 for (const auto& arg : rawArguments)
605 {
606 if (const auto assignCharPos = arg.find_first_of(str: assignChars); (argumentMap.find(x: arg) == argumentMap.cend())
607 && startWithPrefixChars(arg) && (assignCharPos != std::string::npos))
608 {
609 if (const auto optName = arg.substr(pos: 0, n: assignCharPos); argumentMap.find(x: optName) != argumentMap.cend())
610 {
611 arguments.emplace_back(args: optName);
612 arguments.emplace_back(args: arg.substr(pos: assignCharPos + 1));
613 continue;
614 }
615 }
616 arguments.emplace_back(args: arg);
617 }
618 return arguments;
619}
620
621void Argument::parseArgsInternal(const std::vector<std::string>& rawArguments)
622{
623 const auto arguments = preprocessArguments(rawArguments);
624 if (titleName.empty() && !arguments.empty())
625 {
626 titleName = arguments.front();
627 }
628
629 const auto ending = arguments.cend();
630 auto positionalArgIter = positionalArgs.begin();
631 for (auto iterator = std::next(x: arguments.cbegin()); iterator != ending;)
632 {
633 const auto& currentArg = *iterator;
634 if (ArgTrait::checkIfPositional(name: currentArg, prefix: prefixChars))
635 {
636 if (positionalArgIter != positionalArgs.cend())
637 {
638 const auto argument = positionalArgIter++;
639 iterator = argument->consume(start: iterator, end: ending);
640 continue;
641 }
642
643 const std::string_view maybeCommand = currentArg;
644 if (const auto subParserIter = subParserMap.find(x: maybeCommand); subParserIter != subParserMap.cend())
645 {
646 const auto unprocessedArgs = std::vector<std::string>(iterator, ending);
647 isParsed = true;
648 subParserUsed[maybeCommand] = true;
649 subParserIter->second->get().parseArgs(arguments: unprocessedArgs);
650 return;
651 }
652 throw std::runtime_error{"Maximum number of positional arguments exceeded."};
653 }
654
655 iterator = processRegArgument(current: iterator, end: ending, argName: currentArg);
656 }
657 isParsed = true;
658}
659
660std::size_t Argument::getLengthOfLongestArgument() const
661{
662 if (argumentMap.empty())
663 {
664 return 0;
665 }
666
667 std::size_t maxSize = 0;
668 for (const auto& argument : std::views::values(argumentMap))
669 {
670 maxSize = std::max<std::size_t>(a: maxSize, b: argument->getArgumentsLength());
671 }
672 for (const auto& command : std::views::keys(subParserMap))
673 {
674 maxSize = std::max<std::size_t>(a: maxSize, b: command.length());
675 }
676 return maxSize;
677}
678
679void Argument::indexArgument(const TraitIter& iterator)
680{
681 for (const auto& name : std::as_const(t&: iterator->names))
682 {
683 argumentMap.insert_or_assign(k: name, obj: iterator);
684 }
685}
686
687//! @brief The operator (<<) overloading of the Argument class.
688//! @param os - output stream object
689//! @param arg - specific Argument object
690//! @return reference of the output stream object
691std::ostream& operator<<(std::ostream& os, const Argument& arg)
692{
693 os.setf(std::ios_base::left);
694 os << arg.usage() << "\n\n";
695
696 if (!arg.descrText.empty())
697 {
698 os << arg.descrText << "\n\n";
699 }
700
701 if (!arg.optionalArgs.empty())
702 {
703 os << "optional:\n";
704 }
705 const auto longestArgLen = arg.getLengthOfLongestArgument();
706 for (const auto& argument : arg.optionalArgs)
707 {
708 os.width(wide: static_cast<std::streamsize>(longestArgLen));
709 os << argument;
710 }
711
712 if (!arg.positionalArgs.empty())
713 {
714 os << (arg.optionalArgs.empty() ? "" : "\n") << "positional:\n";
715 }
716 for (const auto& argument : arg.positionalArgs)
717 {
718 os.width(wide: static_cast<std::streamsize>(longestArgLen));
719 os << argument;
720 }
721
722 if (!arg.subParserMap.empty())
723 {
724 os << (arg.optionalArgs.empty() ? (arg.positionalArgs.empty() ? "" : "\n") : "\n") << "sub-command:\n";
725 for (const auto& [command, subParser] : arg.subParserMap)
726 {
727 os << std::setw(static_cast<int>(longestArgLen)) << command << " " << subParser->get().descrText << '\n';
728 }
729 }
730 os << std::flush;
731 return os;
732}
733} // namespace utility::argument
734