Lector v1.0.0
C++ library for parsing command line arguments.
arguments.hpp
1// Copyright © 2026, Alexandre Coderre-Chabot.
2
3// This file is part of Lector (https://github.com/acodcha/lector), a C++ library for parsing
4// command line arguments. Lector is licensed under the MIT License (https://mit-license.org).
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7// associated documentation files (the "Software"), to deal in the Software without restriction,
8// including without limitation the rights to use, copy, modify, merge, publish, distribute,
9// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11// - The above copyright notice and this permission notice shall be included in all copies or
12// substantial portions of the Software.
13// - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
14// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM
17// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
19#ifndef LECTOR_ARGUMENTS_HPP
20#define LECTOR_ARGUMENTS_HPP
21
22#include <algorithm>
23#include <array>
24#include <cstddef>
25#include <cstdint>
26#include <filesystem>
27#include <initializer_list>
28#include <iostream>
29#include <limits>
30#include <optional>
31#include <sstream>
32#include <stdexcept>
33#include <string>
34#include <string_view>
35#include <type_traits>
36#include <unordered_set>
37#include <utility>
38#include <vector>
39
40#include "lector/parse.hpp"
41#include "lector/print.hpp"
42#include "lector/text.hpp"
43
44/// @brief The Lector library's namespace.
45namespace lector {
46
47/// @brief Importance of a command line argument.
48enum class Importance : ::std::int8_t {
49 /// @brief The command line argument is optional; it may or may not be provided by the user.
50 Optional = 0,
51
52 /// @brief The command line argument is required; it must be provided by the user.
53 Required = 1,
54};
55
56/// @brief Specialization of the lector::Names constant for the lector::Importance enumeration.
57template <>
58inline constexpr ::std::array<::lector::Name<::lector::Importance>, 2> Names<::lector::Importance>{
59 {
62 }
63};
64
65/// @brief Specialization of the lector::Spellings constant for the lector::Importance enumeration.
66template <>
67inline constexpr ::std::array<::lector::Spelling<::lector::Importance>, 6>
68 Spellings<::lector::Importance>{
69 {
76 }
77};
78
79/// @brief A command line argument, including its label, value type, keys, description, importance,
80/// default value, and parsed value.
81/// @tparam LabelValue Value of this command line argument's label. The label is used to uniquely
82/// identify this command line argument in a collection of command line arguments.
83/// @tparam Type The type of the value stored in this command line argument.
84template <auto LabelValue, typename Type>
85class Argument final {
86public:
87 using ValueType = Type;
88
89 /// @brief Default constructor. Initializes the command line argument with no keys, an empty
90 /// description, required importance, and no default value.
91 Argument() noexcept = default;
92
93 /// @brief Constructor for a required command line argument or a boolean command line argument. No
94 /// default value is needed. Boolean command line arguments are always optional and always default
95 /// to false.
96 /// @param[in] keys The keys that can be used to specify the command line argument.
97 /// @param[in] description The description of the command line argument.
98 /// @throws std::invalid_argument if any of the parameters are invalid.
99 Argument(const ::std::initializer_list<::std::string>& keys, const ::std::string& description)
100 : keys_{keys}, description_{description},
101 importance_{::std::is_same_v<Type, bool> ? ::lector::Importance::Optional :
103 set_boolean_default();
104 validate_keys();
105 validate_description();
106 }
107
108 /// @brief Constructor for an optional non-boolean command line argument. A default value must be
109 /// provided.
110 /// @param[in] keys The keys that can be used to specify the command line argument.
111 /// @param[in] description The description of the command line argument.
112 /// @param[in] default_value The default value of the command line argument.
113 /// @throws std::invalid_argument if any of the parameters are invalid.
114 Argument(const ::std::initializer_list<::std::string>& keys, const ::std::string& description,
115 const Type& default_value)
116 : keys_{keys}, description_{description}, default_value_{default_value},
117 importance_{::lector::Importance::Optional} {
118 validate_keys();
119 validate_description();
120 validate_default_value();
121 }
122
123 /// @brief Destructor. Destroys this command line argument.
124 ~Argument() noexcept = default;
125
126 /// @brief Copy constructor. Constructs a command line argument by copying another one.
127 Argument(const ::lector::Argument<LabelValue, Type>&) = default;
128
129 /// @brief Copy assignment operator. Assigns this command line argument by copying another one.
130 /// @return This command line argument after the assignment.
131 ::lector::Argument<LabelValue, Type>& operator=(
132 const ::lector::Argument<LabelValue, Type>&) = default;
133
134 /// @brief Move constructor. Constructs a command line argument by moving another one.
135 Argument(::lector::Argument<LabelValue, Type>&&) noexcept = default;
136
137 /// @brief Move assignment operator. Assigns this command line argument by moving another one.
138 /// @return This command line argument after the assignment.
139 ::lector::Argument<LabelValue, Type>& operator=(
140 ::lector::Argument<LabelValue, Type>&&) noexcept = default;
141
142 /// @brief Label of this command line argument. Used to uniquely identify this command line
143 /// argument. Set at construction.
144 /// @return The label of this command line argument.
145 [[nodiscard]] static constexpr auto label() noexcept {
146 return LabelValue;
147 }
148
149 /// @brief Keys that can be used to specify this command line argument. Set at construction.
150 /// @return The keys that can be used to specify this command line argument.
151 [[nodiscard]] const ::std::vector<::std::string>& keys() const noexcept {
152 return keys_;
153 }
154
155 /// @brief Description of this command line argument. Set at construction.
156 /// @return The description of this command line argument.
157 [[nodiscard]] ::std::string_view description() const noexcept {
158 return description_;
159 }
160
161 /// @brief Default value of this command line argument. Only relevant for optional non-boolean
162 /// arguments. Set at construction.
163 /// @return The default value of this command line argument.
164 [[nodiscard]] const ::std::optional<Type>& default_value() const noexcept {
165 return default_value_;
166 }
167
168 /// @brief Parsed value of this command line argument. Set when this command line argument is
169 /// parsed.
170 /// @return The parsed value of this command line argument.
171 [[nodiscard]] const ::std::optional<Type>& parsed_value() const noexcept {
172 return parsed_value_;
173 }
174
175 /// @brief Importance of this command line argument. Required arguments must be provided by the
176 /// user, while optional arguments may or may not be provided by the user. Set at construction.
177 /// @return The importance of this command line argument.
178 [[nodiscard]] ::lector::Importance importance() const noexcept {
179 return importance_;
180 }
181
182 /// @brief Value of this command line argument. Returns the parsed value if it exists; otherwise,
183 /// returns the default value.
184 /// @return The value of this command line argument.
185 /// @throws std::logic_error if this command line argument is missing both its parsed value and
186 /// its default value.
187 [[nodiscard]] const Type& parsed_or_default_value() const {
188 if (parsed_value_.has_value()) {
189 return parsed_value_.value();
190 }
191 if (default_value_.has_value()) {
192 return default_value_.value();
193 }
194 throw ::std::logic_error(
195 "No parsed or default value for argument '" + longest_key_with_value_type() + "'.");
196 }
197
198 /// @brief Sets the parsed value of this command line argument.
199 /// @param[in] value The parsed value to set.
200 void set_parsed_value(const Type& value) {
201 parsed_value_ = value;
202 }
203
204 /// @brief Prints the longest key of this command line argument with its associated value type as
205 /// a string.
206 /// @return The string that contains the longest key and its associated value type.
207 [[nodiscard]] ::std::string longest_key_with_value_type() const {
208 ::std::string result{longest_key()};
209 const ::std::string type{value_type()};
210 if (!type.empty()) {
211 result.push_back(' ');
212 result.append(type);
213 }
214 return result;
215 }
216
217 /// @brief Prints the keys and value type of this command line argument as a string of text.
218 /// @return The string of text that contains the keys and value type of this command line
219 /// argument.
220 [[nodiscard]] ::std::string keys_with_value_type() const {
221 ::std::string result;
222 for (::std::size_t index{0UL}; index < keys_.size(); ++index) {
223 const ::std::string type{value_type()};
224 result.append(keys_.at(index));
225 if (!type.empty()) {
226 result.push_back(' ');
227 result.append(type);
228 }
229 if (index + 1 < keys_.size()) {
230 result.append(", ");
231 }
232 }
233 return result;
234 }
235
236 /// @brief Prints the usage of this command line argument as a string of text. The usage consists
237 /// of this command line argument's longest key and value type, enclosed in square braces if this
238 /// command line argument is optional.
239 /// @return The string of text that contains the usage of this command line argument.
240 [[nodiscard]] ::std::string usage() const {
241 if (keys_.empty()) {
242 return ::std::string{};
243 }
245 return "[" + longest_key_with_value_type() + "]";
246 }
248 }
249
250 /// @brief Prints the options of this command line argument as a string of text. The options
251 /// consist of this command line argument's list of keys and value type along with its
252 /// description.
253 /// @return The string of text that contains the options of this command line argument.
254 [[nodiscard]] ::std::string options() const {
255 if (keys_.empty() && description_.empty()) {
256 return ::std::string{};
257 }
258 return keys_with_value_type() + " " + description_;
259 }
260
261 /// @brief Prints the execution of this command line argument as a string of text. The execution
262 /// consists of this command line argument's longest key and parsed value, if any.
263 /// @return The string of text that contains the execution of this command line argument.
264 [[nodiscard]] ::std::string execution() const {
265 if constexpr (::std::is_same_v<Type, bool>) {
266 if (parsed_value_.has_value() && parsed_value_.value()) {
267 return longest_key();
268 }
269 return "";
270 } else {
271 if (parsed_value_.has_value()) {
272 return longest_key() + " " + ::lector::print<Type>(parsed_value_.value());
273 }
274 return "";
275 }
276 }
277
278private:
279 /// @brief If this command line argument is a boolean argument, sets its default value to false.
280 /// Called by the constructor that does not specify a default value. Boolean arguments are always
281 /// optional and always default to false.
282 void set_boolean_default() {
283 if constexpr (::std::is_same_v<Type, bool>) {
284 default_value_ = false;
285 }
286 }
287
288 /// @brief Validates the keys of this command line argument. Called by both constructors.
289 /// @throws std::logic_error if this command line argument has no keys or if any of its keys are
290 /// invalid.
291 void validate_keys() const {
292 if (keys_.empty()) {
293 throw ::std::logic_error("All arguments must each have at least one key.");
294 }
295 for (const ::std::string& key : keys_) {
296 if (key.empty()) {
297 if (longest_key().empty()) {
298 throw ::std::logic_error("Arguments cannot have empty keys.");
299 }
300 throw ::std::logic_error("Empty key in argument '" + longest_key_with_value_type()
301 + "'. Arguments cannot have empty keys.");
302 }
303 }
304 const ::std::size_t keys_size{keys_.size()};
305 for (::std::size_t first{0UL}; first < keys_size; ++first) {
306 for (::std::size_t second{first + static_cast<::std::size_t>(1UL)}; second < keys_size;
307 ++second) {
308 if (keys_[first] == keys_[second]) {
309 throw ::std::logic_error(
310 "Duplicated key '" + keys_[first] + "' in argument '" + longest_key_with_value_type()
311 + "'. Arguments cannot have duplicate keys.");
312 }
313 }
314 }
315 };
316
317 /// @brief Validates the description of this command line argument. Called by both constructors.
318 /// @throws std::logic_error if this command line argument's description is empty.
319 void validate_description() const {
320 if (description_.empty()) {
321 throw ::std::logic_error("Empty description in argument '" + longest_key_with_value_type()
322 + "'. All arguments must have descriptions.");
323 }
324 };
325
326 /// @brief Validates the default value of this command line argument. Called by the constructor
327 /// that specifies a default value.
328 /// @throws std::logic_error if this command line argument is boolean but specifies a default
329 /// value.
330 constexpr void validate_default_value() const {
331 if constexpr (::std::is_same_v<Type, bool>) {
332 throw ::std::logic_error(
333 "Specified default value for boolean argument '" + longest_key_with_value_type()
334 + "'. Boolean arguments are always false by default and cannot specify default values.");
335 }
336 }
337
338 /// @brief Prints the value type of this command line argument as a string of text.
339 /// @return The string of text that contains the value type.
340 [[nodiscard]] constexpr ::std::string_view value_type() const {
341 if constexpr (::std::is_same_v<Type, bool>) {
342 return "";
343 } else if constexpr (::std::numeric_limits<Type>::is_integer) {
344 return "<number>";
345 } else if constexpr (::std::is_floating_point_v<Type>) {
346 return "<value>";
347 } else if constexpr (
348 ::std::is_same_v<Type, ::std::string> || ::std::is_same_v<Type, ::std::string_view>) {
349 return "<text>";
350 } else if constexpr (::std::is_same_v<Type, ::std::filesystem::path>) {
351 return "<path>";
352 } else {
353 return "<value>";
354 }
355 }
356
357 /// @brief Returns the longest key of this command line argument.
358 /// @return The longest key of this command line argument.
359 /// @throws std::logic_error if this command line argument has no keys.
360 [[nodiscard]] const ::std::string& longest_key() const {
361 if (keys_.empty()) {
362 throw ::std::logic_error("All arguments must each have at least one key.");
363 }
364 ::std::size_t longest_key_index{0UL};
365 for (::std::size_t index{1UL}; index < keys_.size(); ++index) {
366 if (keys_[index].size() > keys_[longest_key_index].size()) {
367 longest_key_index = index;
368 }
369 }
370 return keys_[longest_key_index];
371 }
372
373 /// @brief Keys that can be used to specify this command line argument. Set at construction.
374 ::std::vector<::std::string> keys_;
375
376 /// @brief Description of this command line argument. Set at construction.
377 ::std::string description_;
378
379 /// @brief Default value of this command line argument. Only relevant for optional non-boolean
380 /// arguments. Set at construction.
381 ::std::optional<Type> default_value_;
382
383 /// @brief Parsed value of this command line argument. Set when this command line argument is
384 /// parsed.
385 ::std::optional<Type> parsed_value_;
386
387 /// @brief Importance of this command line argument. Required arguments must be provided by the
388 /// user, while optional arguments may or may not be provided by the user. Set at construction.
390};
391
392/// @brief Type trait used to extract a command line argument from a collection of command line
393/// arguments, using only its Label.
394/// @tparam Label The label of the command line argument to extract.
395/// @tparam ...ArgumentTypes The variadic list of argument types in the collection of command line
396/// arguments.
397template <auto Label, typename... ArgumentTypes>
398struct FindArgumentByLabel;
399
400/// @brief Type trait specialization used to extract a command line argument from a collection of
401/// command line arguments, using its Label, its type, and the types of the remaining command line
402/// arguments in the collection.
403/// @tparam Label The label of the command line argument to extract.
404/// @tparam Type The type of the command line argument to extract.
405/// @tparam ...OtherArgumentTypes The variadic list of argument types in the collection of command
406/// line arguments, excluding the command line argument to extract.
407template <auto Label, typename Type, typename... OtherArgumentTypes>
408struct FindArgumentByLabel<Label, ::lector::Argument<Label, Type>, OtherArgumentTypes...> {
410};
411
412/// @brief Type trait specialization used to extract a command line argument from a collection of
413/// command line arguments, using its Label and the types of the remaining command line arguments in
414/// the collection.
415/// @tparam Label The label of the command line argument to extract.
416/// @tparam OtherLabel The label of the command line argument to compare against.
417/// @tparam OtherType The type of the command line argument to compare against.
418/// @tparam ...RemainingArgumentTypes The variadic list of argument types in the collection of
419/// command line arguments, excluding the command line argument to extract.
420template <auto Label, auto OtherLabel, typename OtherType, typename... RemainingArgumentTypes>
421struct FindArgumentByLabel<Label, ::lector::Argument<OtherLabel, OtherType>,
422 RemainingArgumentTypes...>
423 final {
424 using type = typename ::lector::FindArgumentByLabel<Label, RemainingArgumentTypes...>::type;
425};
426
427/// @brief Data structure that validates at compilation time that a specified variadic list of types
428/// are unique. Base data structure that contains an empty list of types and returns true.
429/// @tparam ...Types Variadic list of types to check for uniqueness.
430template <auto... Types>
431struct AreUnique : std::true_type {};
432
433/// @brief Data structure that validates at compilation time that a specified variadic list of types
434/// are unique. Recursively compares a first type against the remaining variadic list of types.
435/// @tparam FirstType The first type to compare.
436/// @tparam ...RemainingTypes The remaining types in the variadic list of types to compare.
437template <auto FirstType, auto... RemainingTypes>
438struct AreUnique<FirstType, RemainingTypes...>
439 : std::bool_constant<((FirstType != RemainingTypes) && ...)
440 && AreUnique<RemainingTypes...>::value> {};
441
442/// @brief A collection of command line arguments that can be parsed from argc and argv.
443/// @tparam ...ArgumentTypes Variadic list of the types of the command line arguments in this
444/// collection.
445template <typename... ArgumentTypes>
446class Arguments final {
447public:
448 /// @brief Compile-time check that all arguments have unique labels.
449 static_assert(AreUnique<ArgumentTypes::label()...>::value,
450 "Duplicate argument labels detected. Each argument must have a unique label.");
451
452 /// @brief Constructor. Constructs a collection of command line arguments by moving a variadic
453 /// list of command line arguments.
454 /// @param[in] ...arguments The variadic list of command line arguments.
455 explicit Arguments(ArgumentTypes... arguments) : arguments_{::std::move(arguments)...} {
456 validate_keys();
457 }
458
459 /// @brief Destructor. Destroys this collection of command line arguments.
460 ~Arguments() noexcept = default;
461
462 /// @brief Copy constructor. Constructs a collection of command line argument by copying another
463 /// one.
464 Arguments(const ::lector::Arguments<ArgumentTypes...>&) = default;
465
466 /// @brief Copy assignment operator. Assigns this collection of command line argument by copying
467 /// another one.
468 /// @return This collection of command line argument after the assignment.
469 ::lector::Arguments<ArgumentTypes...>& operator=(
470 const ::lector::Arguments<ArgumentTypes...>&) = default;
471
472 /// @brief Move constructor. Constructs a collection of command line argument by moving another
473 /// one.
474 Arguments(::lector::Arguments<ArgumentTypes...>&&) noexcept = default;
475
476 /// @brief Move assignment operator. Assigns this collection of command line argument by moving
477 /// another one.
478 /// @return This collection of command line argument after the assignment.
479 ::lector::Arguments<ArgumentTypes...>& operator=(
480 ::lector::Arguments<ArgumentTypes...>&&) noexcept = default;
481
482 /// @brief Parses argc and argv to populate the parsed values of the command line arguments in
483 /// this collection.
484 /// @param[in] argc The number of command line arguments, including the executable path.
485 /// @param[in] argv The array of C-strings that represents the command line arguments, starting
486 /// with the executable path.
487 /// @throws std::invalid_argument if an invalid, unknown, duplicated, or missing argument is
488 /// encountered.
489 void parse(const int argc, char* argv[]) {
490 parse_executable_path(argc, argv);
491 parse_arguments(argc, argv);
492 validate_all_required_arguments_have_parsed_values();
493 }
494
495 /// @brief Returns the executable path of this collection of command line arguments. If the
496 /// command line arguments have not yet been parsed from argc and argv, this path is empty.
497 /// @return The executable path of this collection of command line arguments.
498 [[nodiscard]] const ::std::filesystem::path& executable_path() const {
499 return executable_path_;
500 }
501
502 /// @brief Returns a specified command line argument from this collection.
503 /// @tparam Label The label of the command line argument to return.
504 /// @return The specified command line argument.
505 template <auto Label>
506 [[nodiscard]] const auto& get() const {
507 using Type = typename ::lector::FindArgumentByLabel<Label, ArgumentTypes...>::type;
508 return ::std::get<Type>(arguments_);
509 }
510
511 /// @brief Prints the usage of this collection of command line arguments as a string of text. The
512 /// usage consists of each argument's longest key and value type, enclosed in square braces for
513 /// optional command line arguments. Required command line arguments are printed first, followed
514 /// by optional ones.
515 /// @return The string of text that contains the usage of this collection of command line
516 /// arguments.
517 [[nodiscard]] ::std::string usage() const {
518 ::std::string result;
519 result.append(executable_path_.filename().string());
520 ::std::apply(
521 [&](const auto&... argument) {
522 (..., [&]() {
523 if (argument.importance() == ::lector::Importance::Required) {
524 result.push_back(' ');
525 result.append(argument.usage());
526 }
527 }());
528 },
529 arguments_);
530 ::std::apply(
531 [&](const auto&... argument) {
532 (..., [&]() {
533 if (argument.importance() == ::lector::Importance::Optional) {
534 result.push_back(' ');
535 result.append(argument.usage());
536 }
537 }());
538 },
539 arguments_);
540 return ::lector::wrap_and_left_align(result, maximum_line_length_);
541 }
542
543 /// @brief Prints the options of this collection of command line argument as a string of text. The
544 /// options consist of each command line argument's list of keys and value type along with their
545 /// descriptions. Required command line arguments are printed first, followed by optional ones.
546 /// @return The string of text that contains the options of this collection of command line
547 /// argument.
548 [[nodiscard]] ::std::string options() const {
549 // Compute the text formatting dimensions.
550 constexpr ::std::size_t gutter_width{2UL};
551 constexpr ::std::size_t maximum_first_column_width{
552 (maximum_line_length_ - gutter_width) / static_cast<::std::size_t>(2UL)};
553 const ::std::size_t maximum_length_of_keys_with_value_types_{
554 maximum_length_of_keys_with_value_type()};
555 const ::std::size_t first_column_width{
556 ::std::min(maximum_length_of_keys_with_value_types_, maximum_first_column_width)};
557 const ::std::size_t second_column_width{
558 maximum_line_length_ - gutter_width - first_column_width};
559 // Obtain the number of command line arguments.
560 constexpr ::std::size_t argument_count{::std::tuple_size_v<decltype(arguments_)>};
561 // Iterate through the command line arguments and update the resulting string of text.
562 ::std::string result;
563 ::std::size_t argument_index{0UL};
564 ::std::apply(
565 [&](const auto&... argument) {
566 (..., [&]() {
567 if (argument.importance() == ::lector::Importance::Required) {
569 argument.keys_with_value_type(), first_column_width, argument.description(),
570 second_column_width));
571 ++argument_index;
572 if (argument_index < argument_count) {
573 result.push_back('\n');
574 }
575 }
576 }());
577 },
578 arguments_);
579 ::std::apply(
580 [&](const auto&... argument) {
581 (..., [&]() {
582 if (argument.importance() == ::lector::Importance::Optional) {
584 argument.keys_with_value_type(), first_column_width, argument.description(),
585 second_column_width));
586 ++argument_index;
587 if (argument_index < argument_count) {
588 result.push_back('\n');
589 }
590 }
591 }());
592 },
593 arguments_);
594 return result;
595 }
596
597 /// @brief Prints the execution of this collection of command line argument as a string of text.
598 /// The execution consists of the executable path followed by each argument's longest key and
599 /// corresponding parsed value, if any.
600 /// @return The string of text that contains the execution of this collection of command line
601 /// argument.
602 [[nodiscard]] ::std::string execution() const {
603 ::std::string printed_execution_arguments;
604 ::std::apply(
605 [&](const auto&... argument) {
606 (..., [&]() {
607 const std::string argument_execution{argument.execution()};
608 if (!printed_execution_arguments.empty() && !argument_execution.empty()) {
609 printed_execution_arguments.push_back(' ');
610 }
611 printed_execution_arguments.append(argument_execution);
612 }());
613 },
614 arguments_);
615 if (printed_execution_arguments.empty()) {
616 return executable_path_.string();
617 }
618 return executable_path_.string() + " " + printed_execution_arguments;
619 }
620
621private:
622 /// @brief Maximum length of a line when printing usage information or options information.
623 static constexpr ::std::size_t maximum_line_length_{80UL};
624
625 /// @brief The best matching argument for a command line argument token during parsing. The best
626 /// matching argument is the argument with the longest matching key, and if there are multiple
627 /// arguments with keys of the same length that match, then the best matching argument is the one
628 /// that is matched by a non-inline key rather than an inline key. Used to avoid shadowing when
629 /// multiple arguments have keys that are prefixes of each other, and to prefer non-inline matches
630 /// over inline matches when the key lengths are equal.
631 struct BestArgument final {
632 /// @brief Index of this argument in the tuple of arguments. Used to identify this argument
633 /// during parsing.
634 ::std::size_t index{0UL};
635
636 /// @brief Length of the longest matching key for this argument. Used to avoid shadowing when
637 /// multiple arguments have keys that are prefixes of each other. For example, if one argument
638 /// has the key "key" and another argument has the key "key_long", then the argument with the
639 /// key "key_long" should be matched for the command line argument "key_long=value", not the
640 /// argument with the key "key".
641 ::std::size_t key_length{0UL};
642
643 /// @brief Whether this argument was matched by an inline key of the form "key=value" rather
644 /// than a whitespace-separated key-value pair of the form "key value". Used to prefer
645 /// non-inline matches over inline matches when the key lengths are equal. For example, if one
646 /// argument has the key "key" and another argument has the key "key_long", then the argument
647 /// with the key "key" should be matched for the command line argument "key=value", not the
648 /// argument with the key "key_long".
649 bool is_inline{false};
650 };
651
652 /// @brief Parses the executable path from argc and argv. Called by the lector::Arguments::parse
653 /// method.
654 /// @param[in] argc The number of command line arguments, including the executable path.
655 /// @param[in] argv The array of C-strings that represents the command line arguments, starting
656 /// with the executable path.
657 void parse_executable_path(const int argc, char* argv[]) {
658 if (argc > 0) {
659 executable_path_ = argv[0];
660 }
661 }
662
663 /// @brief Parses the command line arguments from argc and argv, except for the executable path.
664 /// Starts at the second argument in argv. Called by the lector::Arguments::parse method.
665 /// @param[in] argc The number of command line arguments, including the executable path.
666 /// @param[in] argv The array of C-strings that represents the command line arguments, starting
667 /// with the executable path.
668 /// @throws std::invalid_argument if an invalid, unknown, duplicated, or missing argument is
669 /// encountered.
670 void parse_arguments(const int argc, char* argv[]) {
671 const ::std::size_t count{static_cast<::std::size_t>(argc)};
672 for (::std::size_t argv_index{1UL}; argv_index < count; ++argv_index) {
673 const ::std::string_view token{argv[argv_index]};
674 const BestArgument best_argument{find_best_argument(token)};
675 ::std::size_t current_index{0UL};
676 ::std::apply(
677 [&](auto&... argument) {
678 (..., [&]() {
679 if (current_index == best_argument.index) {
680 populate_argument(argument, best_argument, argc, argv, argv_index);
681 }
682 ++current_index;
683 }());
684 },
685 arguments_);
686 }
687 }
688
689 /// @brief Finds the best matching argument for a command line argument token.
690 /// @param[in] token The command line argument token to match.
691 /// @return The best matching argument.
692 /// @throws std::invalid_argument if an unknown argument is encountered.
693 [[nodiscard]] BestArgument find_best_argument(const ::std::string_view token) const {
694 ::std::optional<BestArgument> best;
695 ::std::size_t argument_index{0UL};
696 ::std::apply(
697 [&](const auto&... argument) {
698 (..., [&]() {
699 for (const ::std::string& argument_key : argument.keys()) {
700 const ::std::optional<BestArgument> exact_match{
701 try_exact_match(token, argument_index, argument_key)};
702 if (exact_match.has_value()) {
703 if (!best.has_value() || exact_match->key_length > best->key_length
704 || (exact_match->key_length == best->key_length && best->is_inline)) {
705 best = exact_match;
706 }
707 continue;
708 }
709 const ::std::optional<BestArgument> inline_match{
710 try_inline_match<decltype(argument)>(token, argument_index, argument_key)};
711 if (inline_match.has_value()
712 && (!best.has_value() || inline_match->key_length > best->key_length)) {
713 best = inline_match;
714 }
715 }
716 ++argument_index;
717 }());
718 },
719 arguments_);
720 if (!best.has_value()) {
721 throw ::std::invalid_argument("Unknown argument '" + ::std::string{token} + "'.");
722 }
723 return best.value();
724 }
725
726 /// @brief Checks whether a token is the exact match of an argument's key. Called by
727 /// lector::Arguments::find_best_argument().
728 /// @param[in] token The token to check.
729 /// @param[in] argument_index The index of the argument that has the key.
730 /// @param[in] argument_key The argument key against which to compare.
731 /// @return A populated lector::Arguments::BestArgument data structure if the specified token is
732 /// an exact match for the key, or std::nullopt otherwise.
733 [[nodiscard]] static ::std::optional<BestArgument> try_exact_match(
734 const ::std::string_view token, const ::std::size_t argument_index,
735 const ::std::string_view argument_key) noexcept {
736 if (token == argument_key) {
737 return BestArgument{argument_index, argument_key.size(), false};
738 }
739 return ::std::nullopt;
740 }
741
742 /// @brief Checks whether a token contains an inline match of an argument's key of the form
743 /// "key=value". Called by lector::Arguments::find_best_argument().
744 /// @tparam Argument The type of the argument that has the key to be used in the comparison.
745 /// @param[in] token The token to check.
746 /// @param[in] argument_index The index of the argument that has the key to be used in the
747 /// comparison.
748 /// @param[in] argument_key The argument key against which to compare.
749 /// @return A populated lector::Arguments::BestArgument data structure if the specified token
750 /// contains an inline match for the key, or std::nullopt otherwise.
751 template <typename Argument>
752 [[nodiscard]] static ::std::optional<BestArgument> try_inline_match(
753 const ::std::string_view token, const ::std::size_t argument_index,
754 const ::std::string_view argument_key) noexcept {
755 using ArgumentType = typename ::std::decay_t<Argument>::ValueType;
756 // Inline matching is strictly disabled for boolean arguments because they are key-only flags
757 // that do not have values.
758 if constexpr (!::std::is_same_v<ArgumentType, bool>) {
759 if (token.size() > argument_key.size()
760 && token.compare(0, argument_key.size(), argument_key) == 0
761 && token[argument_key.size()] == '=') {
762 return BestArgument{argument_index, argument_key.size(), true};
763 }
764 }
765 return ::std::nullopt;
766 }
767
768 /// @brief Populates an argument with its parsed value. Called by
769 /// lector::Arguments::parse_arguments().
770 /// @tparam Argument The type of the argument to be populated.
771 /// @param[in,out] argument The argument to be populated.
772 /// @param[in] best_argument The lector::Arguments::BestArgument data structure that corresponds
773 /// to the argument to be populated.
774 /// @param[in] argc The number of command line arguments, including the executable path.
775 /// @param[in] argv The array of C-strings that represents the command line arguments, starting
776 /// with the executable path.
777 /// @param[in,out] argv_index The index of the argument in the array of C-string command line
778 /// arguments whose value is to be extracted and used to populate the argument.
779 /// @throws std::invalid_argument if the parsed value is invalid for this argument type.
780 template <typename Argument>
781 static void populate_argument(Argument& argument, const BestArgument& best_argument,
782 const int argc, char* argv[], ::std::size_t& argv_index) {
783 if (argument.parsed_value().has_value()) {
784 throw ::std::invalid_argument(
785 "Duplicated argument '" + argument.longest_key_with_value_type() + "'.");
786 }
787 using Type = typename ::std::decay_t<Argument>::ValueType;
788 if constexpr (::std::is_same_v<Type, bool>) {
789 // Boolean arguments are key-only flags; their presence implies true.
790 argument.set_parsed_value(true);
791 } else {
792 // This is a non-boolean argument. Extract and parse its value.
793 const ::std::string raw_value{extract_raw_value(
794 best_argument, argument.longest_key_with_value_type(), argc, argv, argv_index)};
795 const ::std::optional<Type> parsed_value{::lector::parse<Type>(raw_value)};
796 if (parsed_value.has_value()) {
797 argument.set_parsed_value(parsed_value.value());
798 } else {
799 throw ::std::invalid_argument("Invalid value '" + raw_value + "' for argument '"
800 + argument.longest_key_with_value_type() + "'.");
801 }
802 }
803 }
804
805 /// @brief Extracts the raw string value from an argv token for a non-boolean best argument.
806 /// Called by lector::Arguments::populate_argument().
807 /// @param[in] best_argument The best argument.
808 /// @param[in] best_argument_longest_key_with_value_type The longest key with value type of the
809 /// best argument.
810 /// @param[in] argc The number of command line arguments, including the executable path.
811 /// @param[in] argv The array of C-strings that represents the command line arguments, starting
812 /// with the executable path.
813 /// @param[in,out] argv_index The index of the argument in the array of C-string command line
814 /// arguments whose raw string value is to be extracted.
815 /// @return The extracted raw string value.
816 /// @throws std::invalid_argument if the command line arguments are missing the value for this
817 /// best argument.
818 [[nodiscard]] static ::std::string extract_raw_value(
819 const BestArgument& best_argument,
820 const ::std::string& best_argument_longest_key_with_value_type, const int argc, char* argv[],
821 ::std::size_t& argv_index) {
822 const ::std::string_view token{argv[argv_index]};
823 if (best_argument.is_inline) {
824 // The token contains an inline value of the form "key=value". Extract the last portion of the
825 // token.
826 return ::std::string{token.substr(best_argument.key_length + 1)};
827 }
828 if (argv_index + 1 < static_cast<::std::size_t>(argc)) {
829 // The token is a standalone value of the form "key", and the next token is of the form
830 // "value". Advance the argv index to consume the next argv element that contains the value.
831 ++argv_index;
832 return ::std::string{argv[argv_index]};
833 }
834 throw ::std::invalid_argument(
835 "Missing value for argument '" + best_argument_longest_key_with_value_type + "'.");
836 }
837
838 /// @brief Validates that the same key is never duplicated across two or more arguments. Called by
839 /// the constructor.
840 /// @throws std::logic_error if the same key is duplicated across two or more arguments.
841 void validate_keys() const {
842 ::std::unordered_set<::std::string> unique_keys;
843 ::std::apply(
844 [&](const auto&... argument) {
845 (..., [&]() {
846 for (const ::std::string& key : argument.keys()) {
847 const ::std::pair<::std::unordered_set<::std::string>::const_iterator, bool> result{
848 unique_keys.insert(key)};
849 if (!result.second) {
850 throw std::logic_error("Duplicate key '" + key + "' across two arguments.");
851 }
852 }
853 }());
854 },
855 arguments_);
856 }
857
858 /// @brief Validates that all required arguments have each successfully parsed a value from argc
859 /// and argv. Called by the lector::Arguments::parse method.
860 /// @throws std::invalid_argument if one or more required arguments are missing a parsed value.
861 void validate_all_required_arguments_have_parsed_values() const {
862 ::std::apply(
863 [&](const auto&... argument) {
864 (..., [&]() {
865 if (argument.importance() == ::lector::Importance::Required
866 && !argument.parsed_value().has_value()) {
867 throw ::std::invalid_argument(
868 "Missing required argument '" + argument.longest_key_with_value_type() + "'.");
869 }
870 }());
871 },
872 arguments_);
873 }
874
875 /// @brief Computes and returns the maximum length of the printed keys and value type across all
876 /// arguments in this collection of command line arguments.
877 /// @return The maximum length of the printed keys and value type across all arguments in this
878 /// collection.
879 [[nodiscard]] ::std::size_t maximum_length_of_keys_with_value_type() const {
880 ::std::size_t maximum_length{0UL};
881 ::std::apply(
882 [&](const auto&... argument) {
883 (..., [&]() {
884 const ::std::size_t length{::lector::code_points(argument.keys_with_value_type())};
885 maximum_length = ::std::max(maximum_length, length);
886 }());
887 },
888 arguments_);
889 return maximum_length;
890 }
891
892 /// @brief Variadic collection of command line arguments.
893 ::std::tuple<ArgumentTypes...> arguments_;
894
895 /// @brief Executable path of this collection of command line arguments. If the command line
896 /// arguments have not yet been parsed from argc and argv, this path is empty.
897 ::std::filesystem::path executable_path_;
898};
899
900} // namespace lector
901
902#endif // LECTOR_ARGUMENTS_HPP
A command line argument, including its label, value type, keys, description, importance,...
Definition arguments.hpp:85
void set_parsed_value(const Type &value)
Sets the parsed value of this command line argument.
Definition arguments.hpp:200
~Argument() noexcept=default
Destructor. Destroys this command line argument.
const ::std::optional< Type > & parsed_value() const noexcept
Parsed value of this command line argument. Set when this command line argument is parsed.
Definition arguments.hpp:171
::std::string longest_key_with_value_type() const
Prints the longest key of this command line argument with its associated value type as a string.
Definition arguments.hpp:207
::std::string keys_with_value_type() const
Prints the keys and value type of this command line argument as a string of text.
Definition arguments.hpp:220
::lector::Importance importance() const noexcept
Importance of this command line argument. Required arguments must be provided by the user,...
Definition arguments.hpp:178
::std::string execution() const
Prints the execution of this command line argument as a string of text. The execution consists of thi...
Definition arguments.hpp:264
::std::string options() const
Prints the options of this command line argument as a string of text. The options consist of this com...
Definition arguments.hpp:254
Argument(const ::std::initializer_list<::std::string > &keys, const ::std::string &description, const Type &default_value)
Constructor for an optional non-boolean command line argument. A default value must be provided.
Definition arguments.hpp:114
const ::std::vector<::std::string > & keys() const noexcept
Keys that can be used to specify this command line argument. Set at construction.
Definition arguments.hpp:151
static constexpr auto label() noexcept
Label of this command line argument. Used to uniquely identify this command line argument....
Definition arguments.hpp:145
const ::std::optional< Type > & default_value() const noexcept
Default value of this command line argument. Only relevant for optional non-boolean arguments....
Definition arguments.hpp:164
Argument() noexcept=default
Default constructor. Initializes the command line argument with no keys, an empty description,...
const Type & parsed_or_default_value() const
Value of this command line argument. Returns the parsed value if it exists; otherwise,...
Definition arguments.hpp:187
::std::string_view description() const noexcept
Description of this command line argument. Set at construction.
Definition arguments.hpp:157
::std::string usage() const
Prints the usage of this command line argument as a string of text. The usage consists of this comman...
Definition arguments.hpp:240
A collection of command line arguments that can be parsed from argc and argv.
Definition arguments.hpp:446
::std::string execution() const
Prints the execution of this collection of command line argument as a string of text....
Definition arguments.hpp:602
Arguments(ArgumentTypes... arguments)
Compile-time check that all arguments have unique labels.
Definition arguments.hpp:455
const ::std::filesystem::path & executable_path() const
Returns the executable path of this collection of command line arguments. If the command line argumen...
Definition arguments.hpp:498
~Arguments() noexcept=default
Destructor. Destroys this collection of command line arguments.
::std::string usage() const
Prints the usage of this collection of command line arguments as a string of text....
Definition arguments.hpp:517
const auto & get() const
Returns a specified command line argument from this collection.
Definition arguments.hpp:506
void parse(const int argc, char *argv[])
Parses argc and argv to populate the parsed values of the command line arguments in this collection.
Definition arguments.hpp:489
::std::string options() const
Prints the options of this collection of command line argument as a string of text....
Definition arguments.hpp:548
The Lector library's namespace.
Definition arguments.hpp:45
Importance
Importance of a command line argument.
Definition arguments.hpp:48
@ Required
The command line argument is required; it must be provided by the user.
@ Optional
The command line argument is optional; it may or may not be provided by the user.
::std::size_t code_points(const ::std::string_view text)
Counts and returns the number of UTF-8 code points in a string of text. The number of UTF-8 code poin...
Definition text.hpp:36
::std::string combine_and_left_align(const ::std::string_view first_column_text, const ::std::size_t first_column_width, const ::std::string_view second_column_text, const ::std::size_t second_column_width)
Combines two strings of text, each representing a column, into a single vector of strings that contai...
Definition text.hpp:278