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-2026 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 if (const auto numMax = argsNumRange.max; numMax > 0)
193 {
194 out << ' ' << (metaVar.empty() ? "VAR" : metaVar);
195 if (numMax > 1)
196 {
197 out << "...";
198 }
199 }
200 if (!isRequired)
201 {
202 out << ']';
203 }
204 return std::move(out).str();
205}
206
207std::size_t ArgTrait::getArgumentsLength() const
208{
209 const std::size_t namesSize = std::accumulate(
210 first: names.cbegin(), last: names.cend(), init: 0, binary_op: [](const auto sum, const auto& str) { return sum + str.length(); });
211 if (checkIfPositional(name: names.at(n: 0), prefix: prefixChars))
212 {
213 return metaVar.empty() ? (namesSize + (names.size() - 1)) : metaVar.length();
214 }
215
216 std::size_t size = namesSize + (2 * (names.size() - 1));
217 if (!metaVar.empty() && (argsNumRange == ArgsNumRange{1, 1}))
218 {
219 size += metaVar.length() + 1;
220 }
221 return size;
222}
223
224void ArgTrait::throwInvalidArgsNumRange() const
225{
226 const auto numMin = argsNumRange.min;
227 const auto numMax = argsNumRange.max;
228 std::ostringstream out{};
229 out << (usedName.empty() ? names.at(n: 0) : usedName) << ": " << numMin;
230 if (!argsNumRange.isExact())
231 {
232 if (argsNumRange.existRightBound())
233 {
234 out << " to " << numMax;
235 }
236 else
237 {
238 out << " or more";
239 }
240 }
241 out << " argument(s) expected. " << values.size() << " provided.";
242 throw std::runtime_error{out.str()};
243}
244
245int ArgTrait::lookAhead(const std::string_view name)
246{
247 return name.empty() ? eof : static_cast<int>(static_cast<unsigned char>(name.front()));
248}
249
250bool ArgTrait::checkIfOptional(const std::string_view name, const std::string_view prefix)
251{
252 return !checkIfPositional(name, prefix);
253}
254
255bool ArgTrait::checkIfPositional(const std::string_view name, const std::string_view prefix)
256{
257 const int first = lookAhead(name);
258 if (first == eof)
259 {
260 return true;
261 }
262 if (prefix.find(c: static_cast<char>(first)) != std::string_view::npos)
263 {
264 std::string_view remain(name);
265 remain.remove_prefix(n: 1);
266 return remain.empty();
267 }
268 return true;
269}
270
271//! @brief Join a series of strings into a single string using a separator.
272//! @tparam StrIter - type of iterator
273//! @param first - iterator pointing to the beginning of the range
274//! @param last - iterator pointing to the end of the range
275//! @param separator - separator to be used between strings
276//! @return joined string
277template <typename StrIter>
278static std::string join(StrIter first, StrIter last, const std::string_view separator)
279{
280 if (first == last)
281 {
282 return {};
283 }
284
285 std::ostringstream out{};
286 out << *first;
287 ++first;
288 while (first != last)
289 {
290 out << separator << *first;
291 ++first;
292 }
293 return std::move(out).str();
294}
295
296//! @brief The operator (<<) overloading of the ArgTrait class.
297//! @param os - output stream object
298//! @param tra - specific ArgTrait object
299//! @return reference of the output stream object
300std::ostream& operator<<(std::ostream& os, const ArgTrait& tra)
301{
302 std::ostringstream out{};
303 if (ArgTrait::checkIfPositional(name: tra.names.at(n: 0), prefix: tra.prefixChars))
304 {
305 out << (tra.metaVar.empty() ? join(first: tra.names.cbegin(), last: tra.names.cend(), separator: " ") : tra.metaVar);
306 }
307 else
308 {
309 out << join(first: tra.names.cbegin(), last: tra.names.cend(), separator: ", ");
310 if (!tra.metaVar.empty() && (tra.argsNumRange == ArgsNumRange{1, 1}))
311 {
312 out << ' ' << tra.metaVar;
313 }
314 }
315
316 const auto streamWidth = os.width();
317 const auto namePadding = std::string(out.str().length(), ' ');
318 os << out.str();
319
320 std::size_t pos = 0;
321 std::size_t prev = 0;
322 bool firstLine = true;
323 const std::string_view helpView = tra.helpMsg;
324 const char* tabSpace = " ";
325 while ((pos = tra.helpMsg.find(c: '\n', pos: prev)) != std::string::npos)
326 {
327 const auto line = helpView.substr(pos: prev, n: pos - prev + 1);
328 if (firstLine)
329 {
330 os << tabSpace << line;
331 firstLine = false;
332 }
333 else
334 {
335 os.width(wide: streamWidth);
336 os << namePadding << tabSpace << line;
337 }
338 prev += pos - prev + 1;
339 }
340
341 if (firstLine)
342 {
343 os << tabSpace << tra.helpMsg;
344 }
345 else
346 {
347 const auto remain = helpView.substr(pos: prev, n: tra.helpMsg.length() - prev);
348 if (!remain.empty())
349 {
350 os.width(wide: streamWidth);
351 os << namePadding << tabSpace << remain;
352 }
353 }
354
355 if (!tra.helpMsg.empty())
356 {
357 os << ' ';
358 }
359 os << tra.argsNumRange;
360
361 if (tra.defaultVal.has_value() && (tra.argsNumRange != ArgsNumRange{0, 0}))
362 {
363 os << "[default: " << tra.representedDefVal << ']';
364 }
365 else if (tra.isRequired)
366 {
367 os << "[required]";
368 }
369 os << '\n';
370 return os;
371}
372
373Argument::Argument(const Argument& arg) :
374 titleName{arg.titleName},
375 versionNumber{arg.versionNumber},
376 descrText{arg.descrText},
377 prefixChars{arg.prefixChars},
378 assignChars{arg.assignChars},
379 isParsed{arg.isParsed},
380 optionalArgs{arg.optionalArgs},
381 positionalArgs{arg.positionalArgs},
382 parserPath{arg.parserPath},
383 subParsers{arg.subParsers}
384{
385 for (auto iterator = optionalArgs.begin(); iterator != optionalArgs.end(); ++iterator)
386 {
387 indexArgument(iterator);
388 }
389 for (auto iterator = positionalArgs.begin(); iterator != positionalArgs.end(); ++iterator)
390 {
391 indexArgument(iterator);
392 }
393 for (auto iterator = subParsers.begin(); iterator != subParsers.end(); ++iterator)
394 {
395 subParserMap.insert_or_assign(k: iterator->get().titleName, obj&: iterator);
396 subParserUsed.insert_or_assign(k: iterator->get().titleName, obj: false);
397 }
398}
399
400Argument& Argument::operator=(const Argument& arg)
401{
402 if (&arg != this)
403 {
404 auto temp = arg;
405 std::swap(a&: *this, b&: temp);
406 }
407 return *this;
408}
409
410Argument::operator bool() const
411{
412 const bool isArgUsed = std::ranges::any_of(argumentMap, [](const auto& iter) { return iter.second->isUsed; });
413 const bool isSubParserUsed = std::ranges::any_of(subParserUsed, [](const auto& iter) { return iter.second; });
414 return isParsed && (isArgUsed || isSubParserUsed);
415}
416
417ArgTrait& Argument::operator[](const std::string_view argName) const
418{
419 auto iterator = argumentMap.find(x: argName);
420 if (iterator != argumentMap.cend())
421 {
422 return *(iterator->second);
423 }
424
425 if (!isValidPrefixChar(c: argName.at(pos: 0)))
426 {
427 const char legalPrefixChar = getAnyValidPrefixChar();
428 const auto prefix = std::string(1, legalPrefixChar);
429
430 const auto name = prefix + argName.data();
431 iterator = argumentMap.find(x: name);
432 if (iterator != argumentMap.cend())
433 {
434 return *(iterator->second);
435 }
436
437 iterator = argumentMap.find(x: prefix + name);
438 if (iterator != argumentMap.cend())
439 {
440 return *(iterator->second);
441 }
442 }
443 throw std::runtime_error{"No such argument: " + std::string{argName} + '.'};
444}
445
446Argument& Argument::addDescription(const std::string_view text)
447{
448 descrText = text;
449 return *this;
450}
451
452void Argument::parseArgs(const std::vector<std::string>& arguments)
453{
454 parseArgsInternal(rawArguments: arguments);
455 for (const auto& argument : std::views::values(argumentMap))
456 {
457 argument->validate();
458 }
459}
460
461void Argument::parseArgs(const int argc, const char* const argv[])
462{
463 if (argc < 0)
464 {
465 throw std::runtime_error{"The argc must be non-negative."};
466 }
467 if (!argv)
468 {
469 throw std::runtime_error{"The argv is null."};
470 }
471 for (int i = 0; i < argc; ++i)
472 {
473 if (!argv[i])
474 {
475 throw std::runtime_error{"The argv contains a null pointer at index " + std::to_string(val: i)};
476 }
477 }
478
479 parseArgs(arguments: std::vector<std::string>{argv, argv + argc});
480}
481
482bool Argument::isUsed(const std::string_view argName) const
483{
484 return (*this)[argName].isUsed;
485}
486
487bool Argument::isSubcommandUsed(const std::string_view parserName) const
488{
489 return subParserUsed.at(k: parserName);
490}
491
492bool Argument::isSubcommandUsed(const Argument& parser) const
493{
494 return isSubcommandUsed(parserName: parser.titleName);
495}
496
497void Argument::clearUsed()
498{
499 isParsed = false;
500 constexpr auto resetting = [](ArgTrait& tra) constexpr
501 {
502 tra.isUsed = false;
503 tra.usedName.clear();
504 tra.values.clear();
505 };
506
507 for (auto& argument : optionalArgs)
508 {
509 resetting(argument);
510 }
511 for (auto& argument : positionalArgs)
512 {
513 resetting(argument);
514 }
515
516 for ([[maybe_unused]] auto& [name, isUsed] : subParserUsed)
517 {
518 isUsed = false;
519 }
520 for (auto& subParser : subParsers)
521 {
522 subParser.get().clearUsed();
523 }
524}
525
526std::string Argument::title() const
527{
528 return titleName;
529}
530
531std::string Argument::version() const
532{
533 return versionNumber;
534}
535
536std::ostringstream Argument::help() const
537{
538 std::ostringstream out{};
539 out << *this;
540 return out;
541}
542
543std::string Argument::usage() const
544{
545 std::ostringstream out{};
546 out << "usage: " << ((parserPath.find(str: ' ' + titleName) == std::string::npos) ? titleName : parserPath);
547
548 for (const auto& argument : optionalArgs)
549 {
550 out << ' ' << argument.getInlineUsage();
551 }
552 for (const auto& argument : positionalArgs)
553 {
554 out << ' ' << (argument.metaVar.empty() ? argument.names.at(n: 0) : argument.metaVar);
555 }
556
557 if (!subParserMap.empty())
558 {
559 out << " {";
560 for (std::size_t i = 0; const auto& command : std::views::keys(subParserMap))
561 {
562 if (i != 0)
563 {
564 out << ',';
565 }
566 out << command;
567 ++i;
568 }
569 out << '}';
570 }
571 return std::move(out).str();
572}
573
574void Argument::addSubParser(Argument& parser)
575{
576 parser.parserPath = titleName + ' ' + parser.titleName;
577 const auto iterator = subParsers.emplace(position: subParsers.cend(), args&: parser);
578 subParserMap.insert_or_assign(k: parser.titleName, obj: iterator);
579 subParserUsed.insert_or_assign(k: parser.titleName, obj: false);
580}
581
582bool Argument::isValidPrefixChar(const char c) const
583{
584 return prefixChars.find(c: c) != std::string::npos;
585}
586
587char Argument::getAnyValidPrefixChar() const
588{
589 return prefixChars.at(n: 0);
590}
591
592std::vector<std::string> Argument::preprocessArguments(const std::vector<std::string>& rawArguments) const
593{
594 std::vector<std::string> arguments{};
595 const auto startWithPrefixChars = [this](const std::string_view str)
596 {
597 const auto legalPrefix = [this](const char c) { return prefixChars.find(c: c) != std::string::npos; };
598 return (str.length() > 1) && (legalPrefix(str.at(pos: 0)) && legalPrefix(str.at(pos: 1)));
599 };
600
601 for (const auto& arg : rawArguments)
602 {
603 if (const auto assignCharPos = arg.find_first_of(str: assignChars); (argumentMap.find(x: arg) == argumentMap.cend())
604 && startWithPrefixChars(arg) && (assignCharPos != std::string::npos))
605 {
606 if (const auto optName = arg.substr(pos: 0, n: assignCharPos); argumentMap.find(x: optName) != argumentMap.cend())
607 {
608 arguments.emplace_back(args: optName);
609 arguments.emplace_back(args: arg.substr(pos: assignCharPos + 1));
610 continue;
611 }
612 }
613 arguments.emplace_back(args: arg);
614 }
615 return arguments;
616}
617
618void Argument::parseArgsInternal(const std::vector<std::string>& rawArguments)
619{
620 const auto arguments = preprocessArguments(rawArguments);
621 if (titleName.empty() && !arguments.empty())
622 {
623 titleName = arguments.front();
624 }
625
626 const auto ending = arguments.cend();
627 auto positionalArgIter = positionalArgs.begin();
628 for (auto iterator = std::next(x: arguments.cbegin()); iterator != ending;)
629 {
630 const auto& currentArg = *iterator;
631 if (ArgTrait::checkIfPositional(name: currentArg, prefix: prefixChars))
632 {
633 if (positionalArgIter != positionalArgs.cend())
634 {
635 const auto argument = positionalArgIter++;
636 iterator = argument->consume(start: iterator, end: ending);
637 continue;
638 }
639
640 const std::string_view maybeCommand = currentArg;
641 if (const auto subParserIter = subParserMap.find(x: maybeCommand); subParserIter != subParserMap.cend())
642 {
643 const auto unprocessedArgs = std::vector<std::string>(iterator, ending);
644 isParsed = true;
645 subParserUsed[maybeCommand] = true;
646 subParserIter->second->get().parseArgs(arguments: unprocessedArgs);
647 return;
648 }
649 throw std::runtime_error{"Maximum number of positional arguments exceeded."};
650 }
651
652 iterator = processRegArgument(current: iterator, end: ending, argName: currentArg);
653 }
654 isParsed = true;
655}
656
657std::size_t Argument::getLengthOfLongestArgument() const
658{
659 if (argumentMap.empty())
660 {
661 return 0;
662 }
663
664 std::size_t maxSize = 0;
665 for (const auto& argument : std::views::values(argumentMap))
666 {
667 maxSize = std::max<std::size_t>(a: maxSize, b: argument->getArgumentsLength());
668 }
669 for (const auto& command : std::views::keys(subParserMap))
670 {
671 maxSize = std::max<std::size_t>(a: maxSize, b: command.length());
672 }
673 return maxSize;
674}
675
676void Argument::indexArgument(const TraitIter& iterator)
677{
678 for (const auto& name : std::as_const(t&: iterator->names))
679 {
680 argumentMap.insert_or_assign(k: name, obj: iterator);
681 }
682}
683
684//! @brief The operator (<<) overloading of the Argument class.
685//! @param os - output stream object
686//! @param arg - specific Argument object
687//! @return reference of the output stream object
688std::ostream& operator<<(std::ostream& os, const Argument& arg)
689{
690 os.setf(std::ios_base::left);
691 os << arg.usage() << "\n\n";
692
693 if (!arg.descrText.empty())
694 {
695 os << arg.descrText << "\n\n";
696 }
697
698 if (!arg.optionalArgs.empty())
699 {
700 os << "optional:\n";
701 }
702 const auto longestArgLen = arg.getLengthOfLongestArgument();
703 for (const auto& argument : arg.optionalArgs)
704 {
705 os.width(wide: static_cast<std::streamsize>(longestArgLen));
706 os << argument;
707 }
708
709 if (!arg.positionalArgs.empty())
710 {
711 os << (arg.optionalArgs.empty() ? "" : "\n") << "positional:\n";
712 }
713 for (const auto& argument : arg.positionalArgs)
714 {
715 os.width(wide: static_cast<std::streamsize>(longestArgLen));
716 os << argument;
717 }
718
719 if (!arg.subParserMap.empty())
720 {
721 os << (arg.optionalArgs.empty() ? (arg.positionalArgs.empty() ? "" : "\n") : "\n") << "sub-command:\n";
722 for (const auto& [command, subParser] : arg.subParserMap)
723 {
724 os << std::setw(static_cast<int>(longestArgLen)) << command << " " << subParser->get().descrText << '\n';
725 }
726 }
727 os << std::flush;
728 return os;
729}
730} // namespace utility::argument
731