SeqAn3 3.4.0-rc.1
The Modern C++ library for sequence analysis.
Loading...
Searching...
No Matches
format_parse.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin
2// SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik
3// SPDX-License-Identifier: BSD-3-Clause
4
10#pragma once
11
12#include <concepts>
13#include <seqan3/std/charconv>
14#include <sstream>
15#include <string>
16#include <vector>
17
20
21namespace seqan3::detail
22{
23
50class format_parse : public format_base
51{
52public:
56 format_parse() = delete;
57 format_parse(format_parse const & pf) = default;
58 format_parse & operator=(format_parse const & pf) = default;
59 format_parse(format_parse &&) = default;
60 format_parse & operator=(format_parse &&) = default;
61 ~format_parse() = default;
62
66 format_parse(int const, std::vector<std::string> argv_) : argv{std::move(argv_)}
67 {}
69
73 template <typename option_type, typename validator_type>
74 void add_option(option_type & value,
75 char const short_id,
76 std::string const & long_id,
77 std::string const & SEQAN3_DOXYGEN_ONLY(desc),
78 option_spec const spec,
79 validator_type && option_validator)
80 {
81 option_calls.push_back(
82 [this, &value, short_id, long_id, spec, option_validator]()
83 {
84 get_option(value, short_id, long_id, spec, option_validator);
85 });
86 }
87
91 void add_flag(bool & value,
92 char const short_id,
93 std::string const & long_id,
94 std::string const & SEQAN3_DOXYGEN_ONLY(desc),
95 option_spec const & SEQAN3_DOXYGEN_ONLY(spec))
96 {
97 flag_calls.push_back(
98 [this, &value, short_id, long_id]()
99 {
100 get_flag(value, short_id, long_id);
101 });
102 }
103
107 template <typename option_type, typename validator_type>
108 void add_positional_option(option_type & value,
109 std::string const & SEQAN3_DOXYGEN_ONLY(desc),
110 validator_type && option_validator)
111 {
112 positional_option_calls.push_back(
113 [this, &value, option_validator]()
114 {
115 get_positional_option(value, option_validator);
116 });
117 }
118
120 void parse(argument_parser_meta_data const & /*meta*/)
121 {
122 end_of_options_it = std::find(argv.begin(), argv.end(), "--");
123
124 // parse options first, because we need to rule out -keyValue pairs
125 // (e.g. -AnoSpaceAfterIdentifierA) before parsing flags
126 for (auto && f : option_calls)
127 f();
128
129 for (auto && f : flag_calls)
130 f();
131
132 check_for_unknown_ids();
133
134 if (end_of_options_it != argv.end())
135 *end_of_options_it = ""; // remove -- before parsing positional arguments
136
137 for (auto && f : positional_option_calls)
138 f();
139
140 check_for_left_over_args();
141 }
142
143 // functions are not needed for command line parsing but are part of the format interface.
145 void add_section(std::string const &, option_spec const)
146 {}
147 void add_subsection(std::string const &, option_spec const)
148 {}
149 void add_line(std::string const &, bool, option_spec const)
150 {}
151 void add_list_item(std::string const &, std::string const &, option_spec const)
152 {}
154
156 template <typename id_type>
157 static bool is_empty_id(id_type const & id)
158 {
159 if constexpr (std::same_as<std::remove_cvref_t<id_type>, std::string>)
160 return id.empty();
161 else // char
162 return is_char<'\0'>(id);
163 }
164
186 template <typename iterator_type, typename id_type>
187 static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, id_type const & id)
188 {
189 if (is_empty_id(id))
190 return end_it;
191
192 return (std::find_if(begin_it,
193 end_it,
194 [&](std::string const & current_arg)
195 {
196 std::string full_id = prepend_dash(id);
197
198 if constexpr (std::same_as<id_type, char>) // short id
199 {
200 // check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
201 // "-ovalue", "-o=value", and "-o value".
202 return current_arg.substr(0, full_id.size()) == full_id;
203 }
204 else
205 {
206 // only "--opt Value" or "--opt=Value" are valid
207 return current_arg.substr(0, full_id.size()) == full_id && // prefix is the same
208 (current_arg.size() == full_id.size()
209 || current_arg[full_id.size()] == '='); // space or `=`
210 }
211 }));
212 }
213
214private:
216 enum class option_parse_result
217 {
218 success,
219 error,
220 overflow_error
221 };
222
227 static std::string prepend_dash(std::string const & long_id)
228 {
229 return {"--" + long_id};
230 }
231
236 static std::string prepend_dash(char const short_id)
237 {
238 return {'-', short_id};
239 }
240
246 std::string combine_option_names(char const short_id, std::string const & long_id)
247 {
248 if (short_id == '\0')
249 return prepend_dash(long_id);
250 else if (long_id.empty())
251 return prepend_dash(short_id);
252 else // both are set (note: both cannot be empty, this is caught before)
253 return prepend_dash(short_id) + "/" + prepend_dash(long_id);
254 }
255
259 bool flag_is_set(std::string const & long_id)
260 {
261 auto it = std::find(argv.begin(), end_of_options_it, prepend_dash(long_id));
262
263 if (it != end_of_options_it)
264 *it = ""; // remove seen flag
265
266 return (it != end_of_options_it);
267 }
268
272 bool flag_is_set(char const short_id)
273 {
274 // short flags need special attention, since they could be grouped (-rGv <=> -r -G -v)
275 for (std::string & arg : argv)
276 {
277 if (arg[0] == '-' && arg.size() > 1 && arg[1] != '-') // is option && not dash && no long option
278 {
279 auto pos = arg.find(short_id);
280
281 if (pos != std::string::npos)
282 {
283 arg.erase(pos, 1); // remove seen bool
284
285 if (arg == "-") // if flag is empty now
286 arg = "";
287
288 return true;
289 }
290 }
291 }
292 return false;
293 }
294
302 template <typename option_t>
304 option_parse_result parse_option_value(option_t & value, std::string const & in)
305 {
306 std::istringstream stream{in};
307 stream >> value;
308
309 if (stream.fail() || !stream.eof())
310 return option_parse_result::error;
311
312 return option_parse_result::success;
313 }
314
322 template <named_enumeration option_t>
323 option_parse_result parse_option_value(option_t & value, std::string const & in)
324 {
325 auto map = seqan3::enumeration_names<option_t>;
326
327 if (auto it = map.find(in); it == map.end())
328 {
329 std::vector<std::pair<std::string_view, option_t>> key_value_pairs(map.begin(), map.end());
330 std::ranges::sort(key_value_pairs,
331 [](auto pair1, auto pair2)
332 {
333 if constexpr (std::totally_ordered<option_t>)
334 {
335 if (pair1.second != pair2.second)
336 return pair1.second < pair2.second;
337 }
338 return pair1.first < pair2.first;
339 });
340
341 throw user_input_error{detail::to_string("You have chosen an invalid input value: ",
342 in,
343 ". Please use one of: ",
344 key_value_pairs | std::views::keys)};
345 }
346 else
347 {
348 value = it->second;
349 }
350
351 return option_parse_result::success;
352 }
353
355 option_parse_result parse_option_value(std::string & value, std::string const & in)
356 {
357 value = in;
358 return option_parse_result::success;
359 }
361
373 template <detail::is_container_option container_option_t, typename format_parse_t = format_parse>
374 requires requires (format_parse_t fp,
375 typename container_option_t::value_type & container_value,
376 std::string const & in) {
377 { fp.parse_option_value(container_value, in) } -> std::same_as<option_parse_result>;
378 }
379 option_parse_result parse_option_value(container_option_t & value, std::string const & in)
380 {
381 typename container_option_t::value_type tmp{};
382
383 auto res = parse_option_value(tmp, in);
384
385 if (res == option_parse_result::success)
386 value.push_back(tmp);
387
388 return res;
389 }
390
403 template <arithmetic option_t>
405 option_parse_result parse_option_value(option_t & value, std::string const & in)
406 {
407 auto res = std::from_chars(&in[0], &in[in.size()], value);
408
409 if (res.ec == std::errc::result_out_of_range)
410 return option_parse_result::overflow_error;
411 else if (res.ec == std::errc::invalid_argument || res.ptr != &in[in.size()])
412 return option_parse_result::error;
413
414 return option_parse_result::success;
415 }
416
427 option_parse_result parse_option_value(bool & value, std::string const & in)
428 {
429 if (in == "0")
430 value = false;
431 else if (in == "1")
432 value = true;
433 else if (in == "true")
434 value = true;
435 else if (in == "false")
436 value = false;
437 else
438 return option_parse_result::error;
439
440 return option_parse_result::success;
441 }
442
450 template <typename option_type>
451 void throw_on_input_error(option_parse_result const res,
452 std::string const & option_name,
453 std::string const & input_value)
454 {
455 std::string msg{"Value parse failed for " + option_name + ": "};
456
457 if (res == option_parse_result::error)
458 {
459 throw user_input_error{msg + "Argument " + input_value + " could not be parsed as type "
460 + get_type_name_as_string(option_type{}) + "."};
461 }
462
463 if constexpr (arithmetic<option_type>)
464 {
465 if (res == option_parse_result::overflow_error)
466 {
467 throw user_input_error{msg + "Numeric argument " + input_value + " is not in the valid range ["
470 }
471 }
472
473 assert(res == option_parse_result::success); // if nothing was thrown, the result must have been a success
474 }
475
493 template <typename option_type, typename id_type>
494 bool identify_and_retrieve_option_value(option_type & value,
496 id_type const & id)
497 {
498 if (option_it != end_of_options_it)
499 {
500 std::string input_value;
501 size_t id_size = (prepend_dash(id)).size();
502
503 if ((*option_it).size() > id_size) // identifier includes value (-keyValue or -key=value)
504 {
505 if ((*option_it)[id_size] == '=') // -key=value
506 {
507 if ((*option_it).size() == id_size + 1) // malformed because no value follows '-i='
508 throw too_few_arguments("Missing value for option " + prepend_dash(id));
509 input_value = (*option_it).substr(id_size + 1);
510 }
511 else // -kevValue
512 {
513 input_value = (*option_it).substr(id_size);
514 }
515
516 *option_it = ""; // remove used identifier-value pair
517 }
518 else // -key value
519 {
520 *option_it = ""; // remove used identifier
521 ++option_it;
522 if (option_it == end_of_options_it) // should not happen
523 throw too_few_arguments("Missing value for option " + prepend_dash(id));
524 input_value = *option_it;
525 *option_it = ""; // remove value
526 }
527
528 auto res = parse_option_value(value, input_value);
529 throw_on_input_error<option_type>(res, prepend_dash(id), input_value);
530
531 return true;
532 }
533 return false;
534 }
535
553 template <typename option_type, typename id_type>
554 bool get_option_by_id(option_type & value, id_type const & id)
555 {
556 auto it = find_option_id(argv.begin(), end_of_options_it, id);
557
558 if (it != end_of_options_it)
559 identify_and_retrieve_option_value(value, it, id);
560
561 if (find_option_id(it, end_of_options_it, id) != end_of_options_it) // should not be found again
562 throw option_declared_multiple_times("Option " + prepend_dash(id)
563 + " is no list/container but declared multiple times.");
564
565 return (it != end_of_options_it); // first search was successful or not
566 }
567
579 template <detail::is_container_option option_type, typename id_type>
580 bool get_option_by_id(option_type & value, id_type const & id)
581 {
582 auto it = find_option_id(argv.begin(), end_of_options_it, id);
583 bool seen_at_least_once{it != end_of_options_it};
584
585 if (seen_at_least_once)
586 value.clear();
587
588 while (it != end_of_options_it)
589 {
590 identify_and_retrieve_option_value(value, it, id);
591 it = find_option_id(it, end_of_options_it, id);
592 }
593
594 return seen_at_least_once;
595 }
596
610 void check_for_unknown_ids()
611 {
612 for (auto it = argv.begin(); it != end_of_options_it; ++it)
613 {
614 std::string arg{*it};
615 if (!arg.empty() && arg[0] == '-') // may be an identifier
616 {
617 if (arg == "-")
618 {
619 continue; // positional option
620 }
621 else if (arg[1] != '-' && arg.size() > 2) // one dash, but more than one character (-> multiple flags)
622 {
623 throw unknown_option("Unknown flags " + expand_multiple_flags(arg)
624 + ". In case this is meant to be a non-option/argument/parameter, "
625 + "please specify the start of arguments with '--'. "
626 + "See -h/--help for program information.");
627 }
628 else // unknown short or long option
629 {
630 throw unknown_option("Unknown option " + arg
631 + ". In case this is meant to be a non-option/argument/parameter, "
632 + "please specify the start of non-options with '--'. "
633 + "See -h/--help for program information.");
634 }
635 }
636 }
637 }
638
650 void check_for_left_over_args()
651 {
652 if (std::find_if(argv.begin(),
653 argv.end(),
654 [](std::string const & s)
655 {
656 return (s != "");
657 })
658 != argv.end())
659 throw too_many_arguments("Too many arguments provided. Please see -h/--help for more information.");
660 }
661
682 template <typename option_type, typename validator_type>
683 void get_option(option_type & value,
684 char const short_id,
685 std::string const & long_id,
686 option_spec const spec,
687 validator_type && validator)
688 {
689 bool short_id_is_set{get_option_by_id(value, short_id)};
690 bool long_id_is_set{get_option_by_id(value, long_id)};
691
692 // if value is no container we need to check for multiple declarations
693 if (short_id_is_set && long_id_is_set && !detail::is_container_option<option_type>)
694 throw option_declared_multiple_times("Option " + combine_option_names(short_id, long_id)
695 + " is no list/container but specified multiple times");
696
697 if (short_id_is_set || long_id_is_set)
698 {
699 try
700 {
701 validator(value);
702 }
703 catch (std::exception & ex)
704 {
705 throw validation_error(std::string("Validation failed for option ")
706 + combine_option_names(short_id, long_id) + ": " + ex.what());
707 }
708 }
709 else // option is not set
710 {
711 // check if option is required
712 if (spec & option_spec::required)
713 throw required_option_missing("Option " + combine_option_names(short_id, long_id)
714 + " is required but not set.");
715 }
716 }
717
725 void get_flag(bool & value, char const short_id, std::string const & long_id)
726 {
727 value = flag_is_set(short_id) || flag_is_set(long_id);
728 }
729
752 template <typename option_type, typename validator_type>
753 void get_positional_option(option_type & value, validator_type && validator)
754 {
755 ++positional_option_count;
756 auto it = std::find_if(argv.begin(),
757 argv.end(),
758 [](std::string const & s)
759 {
760 return (s != "");
761 });
762
763 if (it == argv.end())
764 throw too_few_arguments("Not enough positional arguments provided (Need at least "
765 + std::to_string(positional_option_calls.size())
766 + "). See -h/--help for more information.");
767
768 if constexpr (detail::is_container_option<
769 option_type>) // vector/list will be filled with all remaining arguments
770 {
771 assert(positional_option_count == positional_option_calls.size()); // checked on set up.
772
773 value.clear();
774
775 while (it != argv.end())
776 {
777 auto res = parse_option_value(value, *it);
778 std::string id = "positional option" + std::to_string(positional_option_count);
779 throw_on_input_error<option_type>(res, id, *it);
780
781 *it = ""; // remove arg from argv
782 it = std::find_if(it,
783 argv.end(),
784 [](std::string const & s)
785 {
786 return (s != "");
787 });
788 ++positional_option_count;
789 }
790 }
791 else
792 {
793 auto res = parse_option_value(value, *it);
794 std::string id = "positional option" + std::to_string(positional_option_count);
795 throw_on_input_error<option_type>(res, id, *it);
796
797 *it = ""; // remove arg from argv
798 }
799
800 try
801 {
802 validator(value);
803 }
804 catch (std::exception & ex)
805 {
806 throw validation_error("Validation failed for positional option " + std::to_string(positional_option_count)
807 + ": " + ex.what());
808 }
809 }
810
812 std::vector<std::function<void()>> option_calls;
814 std::vector<std::function<void()>> flag_calls;
816 std::vector<std::function<void()>> positional_option_calls;
818 unsigned positional_option_count{0};
822 std::vector<std::string>::iterator end_of_options_it;
823};
824
825} // namespace seqan3::detail
The <charconv> header from C++17's standard library.
T empty(T... args)
T find(T... args)
Provides the format_base struct containing all helper functions that are needed in all formats.
T from_chars(T... args)
option_spec
Used to further specify argument_parser options/flags.
Definition auxiliary.hpp:247
@ required
Definition auxiliary.hpp:249
constexpr size_t size
The size of a type pack.
Definition type_pack/traits.hpp:143
A type that satisfies std::is_arithmetic_v<t>.
Concept for input streams.
The concept for option validators passed to add_option/positional_option.
T move(T... args)
SeqAn specific customisations in the standard namespace.
T parse(T... args)
Provides character predicates for tokenisation.
T size(T... args)
T sort(T... args)
T substr(T... args)
T to_string(T... args)
T what(T... args)
Hide me