diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9903963086..82ed616e46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,6 +63,10 @@ repos: [.](cmake|cpp|cu|cuh|h|hpp|sh|pxd|py|pyx)$| CMakeLists[.]txt$| meta[.]yaml$ + exclude: | + (?x)^( + ^cpp/tests/utilities/cxxopts.hpp + ) - repo: https://github.com/pre-commit/mirrors-clang-format rev: v20.1.4 hooks: diff --git a/cpp/tests/utilities/base_fixture.hpp b/cpp/tests/utilities/base_fixture.hpp index 41d9b40c45..00422d72f6 100644 --- a/cpp/tests/utilities/base_fixture.hpp +++ b/cpp/tests/utilities/base_fixture.hpp @@ -104,7 +104,7 @@ inline auto parse_test_options(int argc, char** argv) "rmm_mode", "RMM allocation mode", cxxopts::value()->default_value("pool")); return options.parse(argc, argv); - } catch (const cxxopts::OptionException& e) { + } catch (const std::exception& e) { cuopt_assert(false, "Error parsing command line options"); } diff --git a/cpp/tests/utilities/cxxopts.hpp b/cpp/tests/utilities/cxxopts.hpp index d42fd89b12..9c66cf58c0 100644 --- a/cpp/tests/utilities/cxxopts.hpp +++ b/cpp/tests/utilities/cxxopts.hpp @@ -1,44 +1,129 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights - * reserved. SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + +Copyright (c) 2014-2022 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// vim: ts=2:sw=2:expandtab #ifndef CXXOPTS_HPP_INCLUDED #define CXXOPTS_HPP_INCLUDED -#include +#include +#include +#include #include #include -#include +#include #include +#include #include #include -#include #include #include #include #include +#include #include +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +#if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +#define CXXOPTS_NO_REGEX true +#endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +#include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +#if __has_include() +#include +#ifdef __cpp_lib_optional +#define CXXOPTS_HAS_OPTIONAL +#endif +#endif +#if __has_include() +#include +#ifdef __cpp_lib_filesystem +#define CXXOPTS_HAS_FILESYSTEM +#endif +#endif +#endif + +#define CXXOPTS_FALLTHROUGH +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(fallthrough) +#undef CXXOPTS_FALLTHROUGH +#define CXXOPTS_FALLTHROUGH [[fallthrough]] +#endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + #ifndef CXXOPTS_VECTOR_DELIMITER #define CXXOPTS_VECTOR_DELIMITER ',' #endif -#define CXXOPTS__VERSION_MAJOR 2 -#define CXXOPTS__VERSION_MINOR 2 -#define CXXOPTS__VERSION_PATCH 0 +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 3 +#define CXXOPTS__VERSION_PATCH 1 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 +#define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif namespace cxxopts { static constexpr struct { @@ -56,12 +141,25 @@ static constexpr struct { #include namespace cxxopts { -typedef icu::UnicodeString String; + +using String = icu::UnicodeString; inline String toLocalString(std::string s) { return icu::UnicodeString::fromUTF8(std::move(s)); } -class UnicodeStringIterator : public std::iterator { +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible +// non-virtual destructor +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") +// This will be ignored under other compilers like LLVM clang. +class UnicodeStringIterator { public: + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) : s(string), i(pos) {} value_type operator*() const { return s->char32At(i); } @@ -82,12 +180,13 @@ class UnicodeStringIterator : public std::iterator(s.length()); } inline std::string toUTF8String(const String& s) { @@ -116,9 +215,11 @@ inline std::string toUTF8String(const String& s) } inline bool empty(const String& s) { return s.isEmpty(); } + } // namespace cxxopts namespace std { + inline cxxopts::UnicodeStringIterator begin(const icu::UnicodeString& s) { return cxxopts::UnicodeStringIterator(&s, 0); @@ -128,13 +229,15 @@ inline cxxopts::UnicodeStringIterator end(const icu::UnicodeString& s) { return cxxopts::UnicodeStringIterator(&s, s.length()); } + } // namespace std // ifdef CXXOPTS_USE_UNICODE #else namespace cxxopts { -typedef std::string String; + +using String = std::string; template T toLocalString(T&& t) @@ -142,11 +245,11 @@ T toLocalString(T&& t) return std::forward(t); } -inline size_t stringLength(const String& s) { return s.length(); } +inline std::size_t stringLength(const String& s) { return s.length(); } -inline String& stringAppend(String& s, String a) { return s.append(std::move(a)); } +inline String& stringAppend(String& s, const String& a) { return s.append(a); } -inline String& stringAppend(String& s, size_t n, char c) { return s.append(n, c); } +inline String& stringAppend(String& s, std::size_t n, char c) { return s.append(n, c); } template String& stringAppend(String& s, Iterator begin, Iterator end) @@ -161,28 +264,36 @@ std::string toUTF8String(T&& t) } inline bool empty(const std::string& s) { return s.empty(); } + } // namespace cxxopts // ifdef CXXOPTS_USE_UNICODE #endif namespace cxxopts { + namespace { -#ifdef _WIN32 -const std::string LQUOTE("\'"); -const std::string RQUOTE("\'"); -#else -const std::string LQUOTE("‘"); -const std::string RQUOTE("’"); -#endif +CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); } // namespace +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we +// want to silence it: warning: base class 'class +// std::enable_shared_from_this' has accessible non-virtual +// destructor This will be ignored under other compilers like LLVM clang. +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + +// some older versions of GCC warn under this warning +CXXOPTS_IGNORE_WARNING("-Weffc++") class Value : public std::enable_shared_from_this { public: virtual ~Value() = default; virtual std::shared_ptr clone() const = 0; + virtual void add(const std::string& text) const = 0; + virtual void parse(const std::string& text) const = 0; virtual void parse() const = 0; @@ -206,109 +317,115 @@ class Value : public std::enable_shared_from_this { virtual bool is_boolean() const = 0; }; -class OptionException : public std::exception { +CXXOPTS_DIAGNOSTIC_POP + +namespace exceptions { + +class exception : public std::exception { public: - OptionException(const std::string& message) : m_message(message) {} + explicit exception(std::string message) : m_message(std::move(message)) {} - virtual const char* what() const noexcept { return m_message.c_str(); } + CXXOPTS_NODISCARD + const char* what() const noexcept override { return m_message.c_str(); } private: std::string m_message; }; -class OptionSpecException : public OptionException { +class specification : public exception { public: - OptionSpecException(const std::string& message) : OptionException(message) {} + explicit specification(const std::string& message) : exception(message) {} }; -class OptionParseException : public OptionException { +class parsing : public exception { public: - OptionParseException(const std::string& message) : OptionException(message) {} + explicit parsing(const std::string& message) : exception(message) {} }; -class option_exists_error : public OptionSpecException { +class option_already_exists : public specification { public: - option_exists_error(const std::string& option) - : OptionSpecException("Option " + LQUOTE + option + RQUOTE + " already exists") + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") { } }; -class invalid_option_format_error : public OptionSpecException { +class invalid_option_format : public specification { public: - invalid_option_format_error(const std::string& format) - : OptionSpecException("Invalid option format " + LQUOTE + format + RQUOTE) + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) { } }; -class option_syntax_exception : public OptionParseException { +class invalid_option_syntax : public parsing { public: - option_syntax_exception(const std::string& text) - : OptionParseException("Argument " + LQUOTE + text + RQUOTE + - " starts with a - but has incorrect syntax") + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + " starts with a - but has incorrect syntax") { } }; -class option_not_exists_exception : public OptionParseException { +class no_such_option : public parsing { public: - option_not_exists_exception(const std::string& option) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + " does not exist") + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") { } }; -class missing_argument_exception : public OptionParseException { +class missing_argument : public parsing { public: - missing_argument_exception(const std::string& option) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + " is missing an argument") + explicit missing_argument(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " is missing an argument") { } }; -class option_requires_argument_exception : public OptionParseException { +class option_requires_argument : public parsing { public: - option_requires_argument_exception(const std::string& option) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + " requires an argument") + explicit option_requires_argument(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " requires an argument") { } }; -class option_not_has_argument_exception : public OptionParseException { +class gratuitous_argument_for_option : public parsing { public: - option_not_has_argument_exception(const std::string& option, const std::string& arg) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + - " does not take an argument, but argument " + LQUOTE + arg + RQUOTE + - " given") + gratuitous_argument_for_option(const std::string& option, const std::string& arg) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given") { } }; -class option_not_present_exception : public OptionParseException { +class requested_option_not_present : public parsing { public: - option_not_present_exception(const std::string& option) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + " not present") + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") { } }; -class argument_incorrect_type : public OptionParseException { +class option_has_no_value : public exception { public: - argument_incorrect_type(const std::string& arg) - : OptionParseException("Argument " + LQUOTE + arg + RQUOTE + " failed to parse") + explicit option_has_no_value(const std::string& option) + : exception(!option.empty() ? ("Option " + LQUOTE + option + RQUOTE + " has no value") + : "Option has no value") { } }; -class option_required_exception : public OptionParseException { +class incorrect_argument_type : public parsing { public: - option_required_exception(const std::string& option) - : OptionParseException("Option " + LQUOTE + option + RQUOTE + " is required but not present") + explicit incorrect_argument_type(const std::string& arg) + : parsing("Argument " + LQUOTE + arg + RQUOTE + " failed to parse") { } }; +} // namespace exceptions + template void throw_or_mimic(const std::string& text) { @@ -321,22 +438,259 @@ void throw_or_mimic(const std::string& text) throw T{text}; #else // Otherwise manually instantiate the exception, print what() to stderr, - // and abort + // and exit T exception{text}; std::cerr << exception.what() << std::endl; - std::cerr << "Aborting (exceptions disabled)..." << std::endl; - std::abort(); + std::exit(EXIT_FAILURE); #endif } +using OptionNames = std::vector; + namespace values { + +namespace parser_tool { + +struct IntegerDesc { + std::string negative = ""; + std::string base = ""; + std::string value = ""; +}; +struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; +}; + +#ifdef CXXOPTS_NO_REGEX +inline IntegerDesc SplitInteger(const std::string& text) +{ + if (text.empty()) { throw_or_mimic(text); } + IntegerDesc desc; + const char* pdata = text.c_str(); + if (*pdata == '-') { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') { + desc.value = std::string(pdata); + } else { + throw_or_mimic(text); + } + return desc; +} + +inline bool IsTrueText(const std::string& text) +{ + const char* pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) { return true; } + } else if (strncmp(pdata, "1\0", 2) == 0) { + return true; + } + return false; +} + +inline bool IsFalseText(const std::string& text) +{ + const char* pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) { return true; } + } else if (strncmp(pdata, "0\0", 2) == 0) { + return true; + } + return false; +} + +inline OptionNames split_option_names(const std::string& text) +{ + OptionNames split_names; + + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) { throw_or_mimic(text); } + + while (token_start_pos < length) { + const auto& npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { throw_or_mimic(text); } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { next_delimiter_pos = length; } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ + { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } + } + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; +} + +inline ArguDesc ParseArgument(const char* arg, bool& matched) +{ + ArguDesc argu_desc; + const char* pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) { + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_') { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) { + if (*pdata == '=') { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') { argu_desc.value = std::string(pdata); } + matched = true; + } else if (*pdata == '\0') { + matched = true; + } + } + } + } else if (strncmp(pdata, "-", 1) == 0) { + pdata += 1; + argu_desc.grouping = true; + while (isalnum(*pdata, std::locale::classic())) { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + matched = !argu_desc.arg_name.empty() && *pdata == '\0'; + } + return argu_desc; +} + +#else // CXXOPTS_NO_REGEX + namespace { -std::basic_regex integer_pattern("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); -std::basic_regex truthy_pattern("(t|T)(rue)?|1"); -std::basic_regex falsy_pattern("(f|F)(alse)?|0"); +CXXOPTS_LINKONCE +const char* const integer_pattern = "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; +CXXOPTS_LINKONCE +const char* const truthy_pattern = "(t|T)(rue)?|1"; +CXXOPTS_LINKONCE +const char* const falsy_pattern = "(f|F)(alse)?|0"; +CXXOPTS_LINKONCE +const char* const option_pattern = "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; +CXXOPTS_LINKONCE +const char* const option_specifier_pattern = + "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; +CXXOPTS_LINKONCE +const char* const option_specifier_separator_pattern = ", *"; + } // namespace +inline IntegerDesc SplitInteger(const std::string& text) +{ + static const std::basic_regex integer_matcher(integer_pattern); + + std::smatch match; + std::regex_match(text, match, integer_matcher); + + if (match.length() == 0) { throw_or_mimic(text); } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; +} + +inline bool IsTrueText(const std::string& text) +{ + static const std::basic_regex truthy_matcher(truthy_pattern); + std::smatch result; + std::regex_match(text, result, truthy_matcher); + return !result.empty(); +} + +inline bool IsFalseText(const std::string& text) +{ + static const std::basic_regex falsy_matcher(falsy_pattern); + std::smatch result; + std::regex_match(text, result, falsy_matcher); + return !result.empty(); +} + +// Gets the option names specified via a single, comma-separated string, +// and returns the separate, space-discarded, non-empty names +// (without considering which or how many are single-character) +inline OptionNames split_option_names(const std::string& text) +{ + static const std::basic_regex option_specifier_matcher(option_specifier_pattern); + if (!std::regex_match(text.c_str(), option_specifier_matcher)) { + throw_or_mimic(text); + } + + OptionNames split_names; + + static const std::basic_regex option_specifier_separator_matcher( + option_specifier_separator_pattern); + constexpr int use_non_matches{-1}; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator_matcher, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; +} + +inline ArguDesc ParseArgument(const char* arg, bool& matched) +{ + static const std::basic_regex option_matcher(option_pattern); + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; +} + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX +} // namespace parser_tool + namespace detail { + template struct SignedCheck; @@ -347,11 +701,11 @@ struct SignedCheck { { if (negative) { if (u > static_cast((std::numeric_limits::min)())) { - throw_or_mimic(text); + throw_or_mimic(text); } } else { if (u > static_cast((std::numeric_limits::max)())) { - throw_or_mimic(text); + throw_or_mimic(text); } } } @@ -360,7 +714,7 @@ struct SignedCheck { template struct SignedCheck { template - void operator()(bool, U, const std::string&) + void operator()(bool, U, const std::string&) const { } }; @@ -370,70 +724,72 @@ void check_signed_range(bool negative, U value, const std::string& text) { SignedCheck::is_signed>()(negative, value, text); } + } // namespace detail template -R checked_negate(T&& t, const std::string&, std::true_type) +void checked_negate(R& r, T&& t, const std::string&, std::true_type) { // if we got to here, then `t` is a positive number that fits into // `R`. So to avoid MSVC C4146, we first cast it to `R`. // See https://github.com/jarro2783/cxxopts/issues/62 for more details. - return static_cast(-static_cast(t - 1) - 1); + r = static_cast(-static_cast(t - 1) - 1); } template -T checked_negate(T&& t, const std::string& text, std::false_type) +void checked_negate(R&, T&&, const std::string& text, std::false_type) { - throw_or_mimic(text); - return t; + throw_or_mimic(text); } template void integer_parser(const std::string& text, T& value) { - std::smatch match; - std::regex_match(text, match, integer_pattern); - - if (match.length() == 0) { throw_or_mimic(text); } - - if (match.length(4) > 0) { - value = 0; - return; - } - - using US = typename std::make_unsigned::type; + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + using US = typename std::make_unsigned::type; constexpr bool is_signed = std::numeric_limits::is_signed; - const bool negative = match.length(1) > 0; - const uint8_t base = match.length(2) > 0 ? 16 : 10; - auto value_match = match[3]; + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string& value_match = int_desc.value; US result = 0; - for (auto iter = value_match.first; iter != value_match.second; ++iter) { + for (char ch : value_match) { US digit = 0; - if (*iter >= '0' && *iter <= '9') { - digit = static_cast(*iter - '0'); - } else if (base == 16 && *iter >= 'a' && *iter <= 'f') { - digit = static_cast(*iter - 'a' + 10); - } else if (base == 16 && *iter >= 'A' && *iter <= 'F') { - digit = static_cast(*iter - 'A' + 10); + if (ch >= '0' && ch <= '9') { + digit = static_cast(ch - '0'); + } else if (base == 16 && ch >= 'a' && ch <= 'f') { + digit = static_cast(ch - 'a' + 10); + } else if (base == 16 && ch >= 'A' && ch <= 'F') { + digit = static_cast(ch - 'A' + 10); } else { - throw_or_mimic(text); + throw_or_mimic(text); } - const US next = static_cast(result * base + digit); - if (result > next) { throw_or_mimic(text); } + US limit = 0; + if (negative) { + limit = static_cast(std::abs(static_cast((std::numeric_limits::min)()))); + } else { + limit = (std::numeric_limits::max)(); + } + + if (base != 0 && result > limit / base) { + throw_or_mimic(text); + } + if (result * base > limit - digit) { + throw_or_mimic(text); + } - result = next; + result = static_cast(result * base + digit); } detail::check_signed_range(negative, result, text); if (negative) { - value = checked_negate(result, text, std::integral_constant()); + checked_negate(value, result, text, std::integral_constant()); } else { value = static_cast(result); } @@ -444,42 +800,28 @@ void stringstream_parser(const std::string& text, T& value) { std::stringstream in(text); in >> value; - if (!in) { throw_or_mimic(text); } + if (!in) { throw_or_mimic(text); } } -inline void parse_value(const std::string& text, uint8_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, int8_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, uint16_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, int16_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, uint32_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, int32_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, uint64_t& value) { integer_parser(text, value); } - -inline void parse_value(const std::string& text, int64_t& value) { integer_parser(text, value); } +template ::value>::type* = nullptr> +void parse_value(const std::string& text, T& value) +{ + integer_parser(text, value); +} inline void parse_value(const std::string& text, bool& value) { - std::smatch result; - std::regex_match(text, result, truthy_pattern); - - if (!result.empty()) { + if (parser_tool::IsTrueText(text)) { value = true; return; } - std::regex_match(text, result, falsy_pattern); - if (!result.empty()) { + if (parser_tool::IsFalseText(text)) { value = false; return; } - throw_or_mimic(text); + throw_or_mimic(text); } inline void parse_value(const std::string& text, std::string& value) { value = text; } @@ -487,29 +829,66 @@ inline void parse_value(const std::string& text, std::string& value) { value = t // The fallback parser. It uses the stringstream parser to parse all types // that have not been overloaded explicitly. It has to be placed in the // source code before all other more specialized templates. -template +template ::value>::type* = nullptr> void parse_value(const std::string& text, T& value) { stringstream_parser(text, value); } +#ifdef CXXOPTS_HAS_OPTIONAL +template +void parse_value(const std::string& text, std::optional& value) +{ + T result; + parse_value(text, result); + value = std::move(result); +} +#endif + +#ifdef CXXOPTS_HAS_FILESYSTEM +inline void parse_value(const std::string& text, std::filesystem::path& value) +{ + value.assign(text); +} +#endif + +inline void parse_value(const std::string& text, char& c) +{ + if (text.length() != 1) { throw_or_mimic(text); } + + c = text[0]; +} + template void parse_value(const std::string& text, std::vector& value) { + if (text.empty()) { + T v; + parse_value(text, v); + value.emplace_back(std::move(v)); + return; + } std::stringstream in(text); std::string token; - while (in.eof() == false && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + while (!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { T v; parse_value(token, v); value.emplace_back(std::move(v)); } } -inline void parse_value(const std::string& text, char& c) +template +void add_value(const std::string& text, T& value) { - if (text.length() != 1) { throw_or_mimic(text); } + parse_value(text, value); +} - c = text[0]; +template +void add_value(const std::string& text, std::vector& value) +{ + T v; + add_value(text, v); + value.emplace_back(std::move(v)); } template @@ -529,9 +908,11 @@ class abstract_value : public Value { public: abstract_value() : m_result(std::make_shared()), m_store(m_result.get()) {} - abstract_value(T* t) : m_store(t) {} + explicit abstract_value(T* t) : m_store(t) {} + + ~abstract_value() override = default; - virtual ~abstract_value() = default; + abstract_value& operator=(const abstract_value&) = default; abstract_value(const abstract_value& rhs) { @@ -548,60 +929,59 @@ class abstract_value : public Value { m_implicit_value = rhs.m_implicit_value; } - void parse(const std::string& text) const { parse_value(text, *m_store); } + void add(const std::string& text) const override { add_value(text, *m_store); } - bool is_container() const { return type_is_container::value; } + void parse(const std::string& text) const override { parse_value(text, *m_store); } - void parse() const { parse_value(m_default_value, *m_store); } + bool is_container() const override { return type_is_container::value; } - bool has_default() const { return m_default; } + void parse() const override { parse_value(m_default_value, *m_store); } - bool has_implicit() const { return m_implicit; } + bool has_default() const override { return m_default; } - std::shared_ptr default_value(const std::string& value) + bool has_implicit() const override { return m_implicit; } + + std::shared_ptr default_value(const std::string& value) override { m_default = true; m_default_value = value; return shared_from_this(); } - std::shared_ptr implicit_value(const std::string& value) + std::shared_ptr implicit_value(const std::string& value) override { m_implicit = true; m_implicit_value = value; return shared_from_this(); } - std::shared_ptr no_implicit_value() + std::shared_ptr no_implicit_value() override { m_implicit = false; return shared_from_this(); } - std::string get_default_value() const { return m_default_value; } + std::string get_default_value() const override { return m_default_value; } - std::string get_implicit_value() const { return m_implicit_value; } + std::string get_implicit_value() const override { return m_implicit_value; } - bool is_boolean() const { return std::is_same::value; } + bool is_boolean() const override { return std::is_same::value; } const T& get() const { - if (m_store == nullptr) { - return *m_result; - } else { - return *m_store; - } + if (m_store == nullptr) { return *m_result; } + return *m_store; } protected: - std::shared_ptr m_result; - T* m_store; + std::shared_ptr m_result{}; + T* m_store{}; bool m_default = false; bool m_implicit = false; - std::string m_default_value; - std::string m_implicit_value; + std::string m_default_value{}; + std::string m_implicit_value{}; }; template @@ -609,19 +989,30 @@ class standard_value : public abstract_value { public: using abstract_value::abstract_value; - std::shared_ptr clone() const { return std::make_shared>(*this); } + CXXOPTS_NODISCARD + std::shared_ptr clone() const override + { + return std::make_shared>(*this); + } }; template <> class standard_value : public abstract_value { public: - ~standard_value() = default; + ~standard_value() override = default; standard_value() { set_default_and_implicit(); } - standard_value(bool* b) : abstract_value(b) { set_default_and_implicit(); } + explicit standard_value(bool* b) : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } - std::shared_ptr clone() const { return std::make_shared>(*this); } + std::shared_ptr clone() const override + { + return std::make_shared>(*this); + } private: void set_default_and_implicit() @@ -632,6 +1023,7 @@ class standard_value : public abstract_value { m_implicit_value = "true"; } }; + } // namespace values template @@ -648,44 +1040,71 @@ std::shared_ptr value(T& t) class OptionAdder; +CXXOPTS_NODISCARD +inline const std::string& first_or_empty(const OptionNames& long_names) +{ + static const std::string empty{""}; + return long_names.empty() ? empty : long_names.front(); +} + class OptionDetails { public: - OptionDetails(const std::string& short_, - const std::string& long_, - const String& desc, + OptionDetails(std::string short_, + OptionNames long_, + String desc, std::shared_ptr val) - : m_short(short_), m_long(long_), m_desc(desc), m_value(val), m_count(0) + : m_short(std::move(short_)), + m_long(std::move(long_)), + m_desc(std::move(desc)), + m_value(std::move(val)), + m_count(0) { + m_hash = std::hash{}(first_long_name() + m_short); } - OptionDetails(const OptionDetails& rhs) : m_desc(rhs.m_desc), m_count(rhs.m_count) + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc), m_value(rhs.m_value->clone()), m_count(rhs.m_count) { - m_value = rhs.m_value->clone(); } OptionDetails(OptionDetails&& rhs) = default; + CXXOPTS_NODISCARD const String& description() const { return m_desc; } + CXXOPTS_NODISCARD const Value& value() const { return *m_value; } + CXXOPTS_NODISCARD std::shared_ptr make_storage() const { return m_value->clone(); } + CXXOPTS_NODISCARD const std::string& short_name() const { return m_short; } - const std::string& long_name() const { return m_long; } + CXXOPTS_NODISCARD + const std::string& first_long_name() const { return first_or_empty(m_long); } + + CXXOPTS_NODISCARD + const std::string& essential_name() const { return m_long.empty() ? m_short : m_long.front(); } + + CXXOPTS_NODISCARD + const OptionNames& long_names() const { return m_long; } + + std::size_t hash() const { return m_hash; } private: - std::string m_short; - std::string m_long; - String m_desc; - std::shared_ptr m_value; + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; int m_count; + + std::size_t m_hash{}; }; struct HelpOptionDetails { std::string s; - std::string l; + OptionNames l; String desc; bool has_default; std::string default_value; @@ -697,64 +1116,103 @@ struct HelpOptionDetails { }; struct HelpGroupDetails { - std::string name; - std::string description; - std::vector options; + std::string name{}; + std::string description{}; + std::vector options{}; }; class OptionValue { public: - void parse(std::shared_ptr details, const std::string& text) + void add(const std::shared_ptr& details, const std::string& text) + { + ensure_value(details); + ++m_count; + m_value->add(text); + m_long_names = &details->long_names(); + } + + void parse(const std::shared_ptr& details, const std::string& text) { ensure_value(details); ++m_count; m_value->parse(text); + m_long_names = &details->long_names(); } - void parse_default(std::shared_ptr details) + void parse_default(const std::shared_ptr& details) { ensure_value(details); - m_default = true; + m_default = true; + m_long_names = &details->long_names(); m_value->parse(); } - size_t count() const noexcept { return m_count; } + void parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif + + CXXOPTS_NODISCARD + std::size_t count() const noexcept { return m_count; } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) + CXXOPTS_DIAGNOSTIC_POP +#endif // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD bool has_default() const noexcept { return m_default; } template const T& as() const { - if (m_value == nullptr) { throw_or_mimic("No value"); } + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } -#ifdef CXXOPTS_NO_RTTI - return static_cast&>(*m_value).get(); -#else - return dynamic_cast&>(*m_value).get(); -#endif + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional as_optional() const + { + if (m_value == nullptr) { return std::nullopt; } + return as(); } +#endif private: - void ensure_value(std::shared_ptr details) + void ensure_value(const std::shared_ptr& details) { if (m_value == nullptr) { m_value = details->make_storage(); } } - std::shared_ptr m_value; - size_t m_count = 0; - bool m_default = false; + const OptionNames* m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; }; class KeyValue { public: - KeyValue(std::string key_, std::string value_) + KeyValue(std::string key_, std::string value_) noexcept : m_key(std::move(key_)), m_value(std::move(value_)) { } + CXXOPTS_NODISCARD const std::string& key() const { return m_key; } + CXXOPTS_NODISCARD const std::string& value() const { return m_value; } template @@ -770,74 +1228,178 @@ class KeyValue { std::string m_value; }; +using ParsedHashMap = std::unordered_map; +using NameHashMap = std::unordered_map; + class ParseResult { public: - ParseResult( - const std::shared_ptr>>, - std::vector, - bool allow_unrecognised, - int&, - char**&); - - size_t count(const std::string& o) const - { - auto iter = m_options->find(o); - if (iter == m_options->end()) { return 0; } + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + + // GCC complains about m_iter not being initialised in the member + // initializer list + CXXOPTS_DIAGNOSTIC_PUSH + CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult* pr, bool end = false) : m_pr(pr) + { + if (end) { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } else { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } + } + CXXOPTS_DIAGNOSTIC_POP + + Iterator& operator++() + { + ++m_iter; + if (m_sequential && m_iter == m_pr->m_sequential.end()) { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); + } + + bool operator!=(const Iterator& other) const { return !(*this == other); } + + const KeyValue& operator*() { return *m_iter; } - auto riter = m_results.find(iter->second); + const KeyValue* operator->() { return m_iter.operator->(); } - return riter->second.count(); + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, + ParsedHashMap&& values, + std::vector sequential, + std::vector default_opts, + std::vector&& unmatched_args) + : m_keys(std::move(keys)), + m_values(std::move(values)), + m_sequential(std::move(sequential)), + m_defaults(std::move(default_opts)), + m_unmatched(std::move(unmatched_args)) + { } - const OptionValue& operator[](const std::string& option) const + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator begin() const { return Iterator(this); } + + Iterator end() const { return Iterator(this, true); } + + std::size_t count(const std::string& o) const { - auto iter = m_options->find(option); + auto iter = m_keys.find(o); + if (iter == m_keys.end()) { return 0; } - if (iter == m_options->end()) { throw_or_mimic(option); } + auto viter = m_values.find(iter->second); - auto riter = m_results.find(iter->second); + if (viter == m_values.end()) { return 0; } - return riter->second; + return viter->second.count(); } - const std::vector& arguments() const { return m_sequential; } + bool contains(const std::string& o) const { return static_cast(count(o)); } - private: - void parse(int& argc, char**& argv); + const OptionValue& operator[](const std::string& option) const + { + auto iter = m_keys.find(option); - void add_to_option(const std::string& option, const std::string& arg); + if (iter == m_keys.end()) { throw_or_mimic(option); } - bool consume_positional(std::string a); + auto viter = m_values.find(iter->second); - void parse_option(std::shared_ptr value, - const std::string& name, - const std::string& arg = ""); + if (viter == m_values.end()) { + throw_or_mimic(option); + } + + return viter->second; + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional as_optional(const std::string& option) const + { + auto iter = m_keys.find(option); + if (iter != m_keys.end()) { + auto viter = m_values.find(iter->second); + if (viter != m_values.end()) { return viter->second.as_optional(); } + } + return std::nullopt; + } +#endif - void parse_default(std::shared_ptr details); + const std::vector& arguments() const { return m_sequential; } - void checked_parse_arg(int argc, - char* argv[], - int& current, - std::shared_ptr value, - const std::string& name); + const std::vector& unmatched() const { return m_unmatched; } - const std::shared_ptr>> m_options; - std::vector m_positional; - std::vector::iterator m_next_positional; - std::unordered_set m_positional_set; - std::unordered_map, OptionValue> m_results; + const std::vector& defaults() const { return m_defaults; } - bool m_allow_unrecognised; + const std::string arguments_string() const + { + std::string result; + for (const auto& kv : m_sequential) { + result += kv.key() + " = " + kv.value() + "\n"; + } + for (const auto& kv : m_defaults) { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; + } + return result; + } - std::vector m_sequential; + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; }; struct Option { - Option(const std::string& opts, - const std::string& desc, - const std::shared_ptr& value = ::cxxopts::value(), - const std::string& arg_help = "") - : opts_(opts), desc_(desc), value_(value), arg_help_(arg_help) + Option(std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "") + : opts_(std::move(opts)), + desc_(std::move(desc)), + value_(std::move(value)), + arg_help_(std::move(arg_help)) { } @@ -847,19 +1409,63 @@ struct Option { std::string arg_help_; }; -class Options { - typedef std::unordered_map> OptionMap; +using OptionMap = std::unordered_map>; +using PositionalList = std::vector; +using PositionalListIterator = PositionalList::const_iterator; +class OptionParser { public: - Options(std::string program, std::string help_string = "") - : m_program(std::move(program)), + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options), m_positional(positional), m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult parse(int argc, const char* const* argv); + + bool consume_positional(const std::string& a, PositionalListIterator& next); + + void checked_parse_arg(int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name); + + void add_to_option(const std::shared_ptr& value, const std::string& arg); + + void parse_option(const std::shared_ptr& value, + const std::string& name, + const std::string& arg = ""); + + void parse_default(const std::shared_ptr& details); + + void parse_no_value(const std::shared_ptr& details); + + private: + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; +}; + +class Options { + public: + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)), m_help_string(toLocalString(std::move(help_string))), m_custom_help("[OPTION...]"), m_positional_help("positional parameters"), m_show_positional(false), m_allow_unrecognised(false), - m_options(std::make_shared()), - m_next_positional(m_positional.end()) + m_width(76), + m_tab_expansion(false), + m_options(std::make_shared()) { } @@ -887,7 +1493,19 @@ class Options { return *this; } - ParseResult parse(int& argc, char**& argv); + Options& set_width(std::size_t width) + { + m_width = width; + return *this; + } + + Options& set_tab_expansion(bool expansion = true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult parse(int argc, const char* const* argv); OptionAdder add_options(std::string group = ""); @@ -897,11 +1515,23 @@ class Options { void add_option(const std::string& group, const std::string& s, - const std::string& l, + const OptionNames& l, std::string desc, - std::shared_ptr value, + const std::shared_ptr& value, std::string arg_help); + void add_option(const std::string& group, + const std::string& short_name, + const std::string& single_long_name, + std::string desc, + const std::shared_ptr& value, + std::string arg_help) + { + OptionNames long_names; + long_names.emplace_back(single_long_name); + add_option(group, short_name, long_names, desc, value, arg_help); + } + // parse positional arguments into the given option void parse_positional(std::string option); @@ -915,14 +1545,16 @@ class Options { parse_positional(std::vector{begin, end}); } - std::string help(const std::vector& groups = {}) const; + std::string help(const std::vector& groups = {}, bool print_usage = true) const; - const std::vector groups() const; + std::vector groups() const; const HelpGroupDetails& group_help(const std::string& group) const; + const std::string& program() const { return m_program; } + private: - void add_one_option(const std::string& option, std::shared_ptr details); + void add_one_option(const std::string& option, const std::shared_ptr& details); String help_one_group(const std::string& group) const; @@ -930,20 +1562,22 @@ class Options { void generate_all_groups_help(String& result) const; - std::string m_program; - String m_help_string; - std::string m_custom_help; - std::string m_positional_help; + std::string m_program{}; + String m_help_string{}; + std::string m_custom_help{}; + std::string m_positional_help{}; bool m_show_positional; bool m_allow_unrecognised; + std::size_t m_width; + bool m_tab_expansion; std::shared_ptr m_options; - std::vector m_positional; - std::vector::iterator m_next_positional; - std::unordered_set m_positional_set; + std::vector m_positional{}; + std::unordered_set m_positional_set{}; // mapping from groups to help options - std::map m_help; + std::vector m_group{}; + std::map m_help{}; }; class OptionAdder { @@ -954,8 +1588,8 @@ class OptionAdder { OptionAdder& operator()(const std::string& opts, const std::string& desc, - std::shared_ptr value = ::cxxopts::value(), - std::string arg_help = ""); + const std::shared_ptr& value = ::cxxopts::value(), + std::string arg_help = ""); private: Options& m_options; @@ -963,29 +1597,26 @@ class OptionAdder { }; namespace { -constexpr int OPTION_LONGEST = 30; -constexpr int OPTION_DESC_GAP = 2; - -std::basic_regex option_matcher("--([[:alnum:]][-_[:alnum:]]+)(=(.*))?|-([[:alnum:]]+)"); - -std::basic_regex option_specifier("(([[:alnum:]]),)?[ ]*([[:alnum:]][-_[:alnum:]]*)?"); +constexpr std::size_t OPTION_LONGEST = 30; +constexpr std::size_t OPTION_DESC_GAP = 2; String format_option(const HelpOptionDetails& o) { - auto& s = o.s; - auto& l = o.l; + const auto& s = o.s; + const auto& l = first_or_empty(o.l); String result = " "; - if (s.size() > 0) { - result += "-" + toLocalString(s) + ","; + if (!s.empty()) { + result += "-" + toLocalString(s); + if (!l.empty()) { result += ","; } } else { result += " "; } - if (l.size() > 0) { result += " --" + toLocalString(l); } + if (!l.empty()) { result += " --" + toLocalString(l); } - auto arg = o.arg_help.size() > 0 ? toLocalString(o.arg_help) : "arg"; + auto arg = !o.arg_help.empty() ? toLocalString(o.arg_help) : "arg"; if (!o.is_boolean) { if (o.has_implicit) { @@ -998,12 +1629,15 @@ String format_option(const HelpOptionDetails& o) return result; } -String format_description(const HelpOptionDetails& o, size_t start, size_t width) +String format_description(const HelpOptionDetails& o, + std::size_t start, + std::size_t allowed, + bool tab_expansion) { auto desc = o.desc; if (o.has_default && (!o.is_boolean || o.default_value != "false")) { - if (o.default_value != "") { + if (!o.default_value.empty()) { desc += toLocalString(" (default: " + o.default_value + ")"); } else { desc += toLocalString(" (default: \"\")"); @@ -1012,60 +1646,83 @@ String format_description(const HelpOptionDetails& o, size_t start, size_t width String result; + if (tab_expansion) { + String desc2; + auto size = std::size_t{0}; + for (auto c = std::begin(desc); c != std::end(desc); ++c) { + if (*c == '\n') { + desc2 += *c; + size = 0; + } else if (*c == '\t') { + auto skip = 8 - size % 8; + stringAppend(desc2, skip, ' '); + size += skip; + } else { + desc2 += *c; + ++size; + } + } + desc = desc2; + } + + desc += " "; + auto current = std::begin(desc); + auto previous = current; auto startLine = current; auto lastSpace = current; - auto size = size_t{}; + auto size = std::size_t{}; + + bool appendNewLine; + bool onlyWhiteSpace = true; while (current != std::end(desc)) { - if (*current == ' ') { lastSpace = current; } - - if (*current == '\n') { - startLine = current + 1; - lastSpace = startLine; - } else if (size > width) { - if (lastSpace == startLine) { - stringAppend(result, startLine, current + 1); - stringAppend(result, "\n"); - stringAppend(result, start, ' '); - startLine = current + 1; - lastSpace = startLine; - } else { - stringAppend(result, startLine, lastSpace); - stringAppend(result, "\n"); - stringAppend(result, start, ' '); - startLine = lastSpace + 1; - lastSpace = startLine; + appendNewLine = false; + if (*previous == ' ' || *previous == '\t') { lastSpace = current; } + if (*current != ' ' && *current != '\t') { onlyWhiteSpace = false; } + + while (*current == '\n') { + previous = current; + ++current; + appendNewLine = true; + } + + if (!appendNewLine && size >= allowed) { + if (lastSpace != startLine) { + current = lastSpace; + previous = current; } - size = 0; - } else { - ++size; + appendNewLine = true; } + if (appendNewLine) { + stringAppend(result, startLine, current); + startLine = current; + lastSpace = current; + + if (*previous != '\n') { stringAppend(result, "\n"); } + + stringAppend(result, start, ' '); + + if (*previous != '\n') { stringAppend(result, lastSpace, current); } + + onlyWhiteSpace = true; + size = 0; + } + + previous = current; ++current; + ++size; } - // append whatever is left - stringAppend(result, startLine, current); + // append whatever is left but ignore whitespace + if (!onlyWhiteSpace) { stringAppend(result, startLine, previous); } return result; } -} // namespace -inline ParseResult::ParseResult( - const std::shared_ptr>> options, - std::vector positional, - bool allow_unrecognised, - int& argc, - char**& argv) - : m_options(options), - m_positional(std::move(positional)), - m_next_positional(m_positional.begin()), - m_allow_unrecognised(allow_unrecognised) -{ - parse(argc, argv); -} +} // namespace inline void Options::add_options(const std::string& group, std::initializer_list