/***************************************************************************** * ___ _ _ ___ ___ * | _|| | | | | _ \ _ \ CLIPP - command line interfaces for modern C++ * | |_ | |_ | | | _/ _/ version 1.2.3 * |___||___||_| |_| |_| https://github.com/muellan/clipp * * Licensed under the MIT License . * Copyright (c) 2017-2018 André Müller * * --------------------------------------------------------------------------- * 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. * *****************************************************************************/ #ifndef AM_CLIPP_H__ #define AM_CLIPP_H__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*************************************************************************//** * * @brief primary namespace * *****************************************************************************/ namespace clipp { /***************************************************************************** * * basic constants and datatype definitions * *****************************************************************************/ using arg_index = int; using arg_string = std::string; using doc_string = std::string; using arg_list = std::vector; /*************************************************************************//** * * @brief tristate * *****************************************************************************/ enum class tri : char { no, yes, either }; inline constexpr bool operator == (tri t, bool b) noexcept { return b ? t != tri::no : t != tri::yes; } inline constexpr bool operator == (bool b, tri t) noexcept { return (t == b); } inline constexpr bool operator != (tri t, bool b) noexcept { return !(t == b); } inline constexpr bool operator != (bool b, tri t) noexcept { return !(t == b); } /*************************************************************************//** * * @brief (start,size) index range * *****************************************************************************/ class subrange { public: using size_type = arg_string::size_type; /** @brief default: no match */ explicit constexpr subrange() noexcept : at_{arg_string::npos}, length_{0} {} /** @brief match length & position within subject string */ explicit constexpr subrange(size_type pos, size_type len) noexcept : at_{pos}, length_{len} {} /** @brief position of the match within the subject string */ constexpr size_type at() const noexcept { return at_; } /** @brief length of the matching subsequence */ constexpr size_type length() const noexcept { return length_; } /** @brief returns true, if query string is a prefix of the subject string */ constexpr bool prefix() const noexcept { return at_ == 0; } /** @brief returns true, if query is a substring of the query string */ constexpr explicit operator bool () const noexcept { return at_ != arg_string::npos; } private: size_type at_; size_type length_; }; /*************************************************************************//** * * @brief match predicates * *****************************************************************************/ using match_predicate = std::function; using match_function = std::function; /*************************************************************************//** * * @brief type traits (NOT FOR DIRECT USE IN CLIENT CODE!) * no interface guarantees; might be changed or removed in the future * *****************************************************************************/ namespace traits { /*************************************************************************//** * * @brief function (class) signature type trait * *****************************************************************************/ template constexpr auto check_is_callable(int) -> decltype( std::declval()(std::declval()...), std::integral_constant::type>::value>{} ); template constexpr auto check_is_callable(long) -> std::false_type; template constexpr auto check_is_callable_without_arg(int) -> decltype( std::declval()(), std::integral_constant::type>::value>{} ); template constexpr auto check_is_callable_without_arg(long) -> std::false_type; template constexpr auto check_is_void_callable(int) -> decltype( std::declval()(std::declval()...), std::true_type{}); template constexpr auto check_is_void_callable(long) -> std::false_type; template constexpr auto check_is_void_callable_without_arg(int) -> decltype( std::declval()(), std::true_type{}); template constexpr auto check_is_void_callable_without_arg(long) -> std::false_type; template struct is_callable; template struct is_callable : decltype(check_is_callable(0)) {}; template struct is_callable : decltype(check_is_callable_without_arg(0)) {}; template struct is_callable : decltype(check_is_void_callable(0)) {}; template struct is_callable : decltype(check_is_void_callable_without_arg(0)) {}; /*************************************************************************//** * * @brief input range type trait * *****************************************************************************/ template constexpr auto check_is_input_range(int) -> decltype( begin(std::declval()), end(std::declval()), std::true_type{}); template constexpr auto check_is_input_range(char) -> decltype( std::begin(std::declval()), std::end(std::declval()), std::true_type{}); template constexpr auto check_is_input_range(long) -> std::false_type; template struct is_input_range : decltype(check_is_input_range(0)) {}; /*************************************************************************//** * * @brief size() member type trait * *****************************************************************************/ template constexpr auto check_has_size_getter(int) -> decltype(std::declval().size(), std::true_type{}); template constexpr auto check_has_size_getter(long) -> std::false_type; template struct has_size_getter : decltype(check_has_size_getter(0)) {}; } // namespace traits /*************************************************************************//** * * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) * no interface guarantees; might be changed or removed in the future * *****************************************************************************/ namespace detail { /*************************************************************************//** * @brief forwards string to first non-whitespace char; * std string -> unsigned conv yields max value, but we want 0; * also checks for nullptr *****************************************************************************/ inline bool fwd_to_unsigned_int(const char*& s) { if(!s) return false; for(; std::isspace(*s); ++s); if(!s[0] || s[0] == '-') return false; if(s[0] == '-') return false; return true; } /*************************************************************************//** * * @brief value limits clamping * *****************************************************************************/ template sizeof(T))> struct limits_clamped { static T from(const V& v) { if(v >= V(std::numeric_limits::max())) { return std::numeric_limits::max(); } if(v <= V(std::numeric_limits::lowest())) { return std::numeric_limits::lowest(); } return T(v); } }; template struct limits_clamped { static T from(const V& v) { return T(v); } }; /*************************************************************************//** * * @brief returns value of v as a T, clamped at T's maximum * *****************************************************************************/ template inline T clamped_on_limits(const V& v) { return limits_clamped::from(v); } /*************************************************************************//** * * @brief type conversion helpers * *****************************************************************************/ template struct make { static inline T from(const char* s) { if(!s) return false; //a conversion from const char* to / must exist return static_cast(s); } }; template<> struct make { static inline bool from(const char* s) { if(!s) return false; return static_cast(s); } }; template<> struct make { static inline unsigned char from(const char* s) { if(!fwd_to_unsigned_int(s)) return (0); return clamped_on_limits(std::strtoull(s,nullptr,10)); } }; template<> struct make { static inline unsigned short int from(const char* s) { if(!fwd_to_unsigned_int(s)) return (0); return clamped_on_limits(std::strtoull(s,nullptr,10)); } }; template<> struct make { static inline unsigned int from(const char* s) { if(!fwd_to_unsigned_int(s)) return (0); return clamped_on_limits(std::strtoull(s,nullptr,10)); } }; template<> struct make { static inline unsigned long int from(const char* s) { if(!fwd_to_unsigned_int(s)) return (0); return clamped_on_limits(std::strtoull(s,nullptr,10)); } }; template<> struct make { static inline unsigned long long int from(const char* s) { if(!fwd_to_unsigned_int(s)) return (0); return clamped_on_limits(std::strtoull(s,nullptr,10)); } }; template<> struct make { static inline char from(const char* s) { //parse as single character? const auto n = std::strlen(s); if(n == 1) return s[0]; //parse as integer return clamped_on_limits(std::strtoll(s,nullptr,10)); } }; template<> struct make { static inline short int from(const char* s) { return clamped_on_limits(std::strtoll(s,nullptr,10)); } }; template<> struct make { static inline int from(const char* s) { return clamped_on_limits(std::strtoll(s,nullptr,10)); } }; template<> struct make { static inline long int from(const char* s) { return clamped_on_limits(std::strtoll(s,nullptr,10)); } }; template<> struct make { static inline long long int from(const char* s) { return (std::strtoll(s,nullptr,10)); } }; template<> struct make { static inline float from(const char* s) { return (std::strtof(s,nullptr)); } }; template<> struct make { static inline double from(const char* s) { return (std::strtod(s,nullptr)); } }; template<> struct make { static inline long double from(const char* s) { return (std::strtold(s,nullptr)); } }; template<> struct make { static inline std::string from(const char* s) { return std::string(s); } }; /*************************************************************************//** * * @brief assigns boolean constant to one or multiple target objects * *****************************************************************************/ template class assign_value { public: template explicit constexpr assign_value(T& target, X&& value) noexcept : t_{std::addressof(target)}, v_{std::forward(value)} {} void operator () () const { if(t_) *t_ = v_; } private: T* t_; V v_; }; /*************************************************************************//** * * @brief flips bools * *****************************************************************************/ class flip_bool { public: explicit constexpr flip_bool(bool& target) noexcept : b_{&target} {} void operator () () const { if(b_) *b_ = !*b_; } private: bool* b_; }; /*************************************************************************//** * * @brief increments using operator ++ * *****************************************************************************/ template class increment { public: explicit constexpr increment(T& target) noexcept : t_{std::addressof(target)} {} void operator () () const { if(t_) ++(*t_); } private: T* t_; }; /*************************************************************************//** * * @brief decrements using operator -- * *****************************************************************************/ template class decrement { public: explicit constexpr decrement(T& target) noexcept : t_{std::addressof(target)} {} void operator () () const { if(t_) --(*t_); } private: T* t_; }; /*************************************************************************//** * * @brief increments by a fixed amount using operator += * *****************************************************************************/ template class increment_by { public: explicit constexpr increment_by(T& target, T by) noexcept : t_{std::addressof(target)}, by_{std::move(by)} {} void operator () () const { if(t_) (*t_) += by_; } private: T* t_; T by_; }; /*************************************************************************//** * * @brief makes a value from a string and assigns it to an object * *****************************************************************************/ template class map_arg_to { public: explicit constexpr map_arg_to(T& target) noexcept : t_{std::addressof(target)} {} void operator () (const char* s) const { if(t_ && s) *t_ = detail::make::from(s); } private: T* t_; }; //------------------------------------------------------------------- /** * @brief specialization for vectors: append element */ template class map_arg_to> { public: map_arg_to(std::vector& target): t_{std::addressof(target)} {} void operator () (const char* s) const { if(t_ && s) t_->push_back(detail::make::from(s)); } private: std::vector* t_; }; //------------------------------------------------------------------- /** * @brief specialization for bools: * set to true regardless of string content */ template<> class map_arg_to { public: map_arg_to(bool& target): t_{&target} {} void operator () (const char* s) const { if(t_ && s) *t_ = true; } private: bool* t_; }; } // namespace detail /*************************************************************************//** * * @brief string matching and processing tools * *****************************************************************************/ namespace str { /*************************************************************************//** * * @brief converts string to value of target type 'T' * *****************************************************************************/ template T make(const arg_string& s) { return detail::make::from(s); } /*************************************************************************//** * * @brief removes trailing whitespace from string * *****************************************************************************/ template inline void trimr(std::basic_string& s) { if(s.empty()) return; s.erase( std::find_if_not(s.rbegin(), s.rend(), [](char c) { return std::isspace(c);} ).base(), s.end() ); } /*************************************************************************//** * * @brief removes leading whitespace from string * *****************************************************************************/ template inline void triml(std::basic_string& s) { if(s.empty()) return; s.erase( s.begin(), std::find_if_not(s.begin(), s.end(), [](char c) { return std::isspace(c);}) ); } /*************************************************************************//** * * @brief removes leading and trailing whitespace from string * *****************************************************************************/ template inline void trim(std::basic_string& s) { triml(s); trimr(s); } /*************************************************************************//** * * @brief removes all whitespaces from string * *****************************************************************************/ template inline void remove_ws(std::basic_string& s) { if(s.empty()) return; s.erase(std::remove_if(s.begin(), s.end(), [](char c) { return std::isspace(c); }), s.end() ); } /*************************************************************************//** * * @brief returns true, if the 'prefix' argument * is a prefix of the 'subject' argument * *****************************************************************************/ template inline bool has_prefix(const std::basic_string& subject, const std::basic_string& prefix) { if(prefix.size() > subject.size()) return false; return subject.find(prefix) == 0; } /*************************************************************************//** * * @brief returns true, if the 'postfix' argument * is a postfix of the 'subject' argument * *****************************************************************************/ template inline bool has_postfix(const std::basic_string& subject, const std::basic_string& postfix) { if(postfix.size() > subject.size()) return false; return (subject.size() - postfix.size()) == subject.find(postfix); } /*************************************************************************//** * * @brief returns longest common prefix of several * sequential random access containers * * @details InputRange require begin and end (member functions or overloads) * the elements of InputRange require a size() member * *****************************************************************************/ template auto longest_common_prefix(const InputRange& strs) -> typename std::decay::type { static_assert(traits::is_input_range(), "parameter must satisfy the InputRange concept"); static_assert(traits::has_size_getter< typename std::decay::type>(), "elements of input range must have a ::size() member function"); using std::begin; using std::end; using item_t = typename std::decay::type; using str_size_t = typename std::decaysize())>::type; const auto n = size_t(distance(begin(strs), end(strs))); if(n < 1) return item_t(""); if(n == 1) return *begin(strs); //length of shortest string auto m = std::min_element(begin(strs), end(strs), [](const item_t& a, const item_t& b) { return a.size() < b.size(); })->size(); //check each character until we find a mismatch for(str_size_t i = 0; i < m; ++i) { for(str_size_t j = 1; j < n; ++j) { if(strs[j][i] != strs[j-1][i]) return strs[0].substr(0, i); } } return strs[0].substr(0, m); } /*************************************************************************//** * * @brief returns longest substring range that could be found in 'arg' * * @param arg string to be searched in * @param substrings range of candidate substrings * *****************************************************************************/ template subrange longest_substring_match(const std::basic_string& arg, const InputRange& substrings) { using string_t = std::basic_string; static_assert(traits::is_input_range(), "parameter must satisfy the InputRange concept"); static_assert(std::is_same::type>(), "substrings must have same type as 'arg'"); auto i = string_t::npos; auto n = string_t::size_type(0); for(const auto& s : substrings) { auto j = arg.find(s); if(j != string_t::npos && s.size() > n) { i = j; n = s.size(); } } return subrange{i,n}; } /*************************************************************************//** * * @brief returns longest prefix range that could be found in 'arg' * * @param arg string to be searched in * @param prefixes range of candidate prefix strings * *****************************************************************************/ template subrange longest_prefix_match(const std::basic_string& arg, const InputRange& prefixes) { using string_t = std::basic_string; using s_size_t = typename string_t::size_type; static_assert(traits::is_input_range(), "parameter must satisfy the InputRange concept"); static_assert(std::is_same::type>(), "prefixes must have same type as 'arg'"); auto i = string_t::npos; auto n = s_size_t(0); for(const auto& s : prefixes) { auto j = arg.find(s); if(j == 0 && s.size() > n) { i = 0; n = s.size(); } } return subrange{i,n}; } /*************************************************************************//** * * @brief returns the first occurrence of 'query' within 'subject' * *****************************************************************************/ template inline subrange substring_match(const std::basic_string& subject, const std::basic_string& query) { if(subject.empty() && query.empty()) return subrange(0,0); if(subject.empty() || query.empty()) return subrange{}; auto i = subject.find(query); if(i == std::basic_string::npos) return subrange{}; return subrange{i,query.size()}; } /*************************************************************************//** * * @brief returns first substring match (pos,len) within the input string * that represents a number * (with at maximum one decimal point and digit separators) * *****************************************************************************/ template subrange first_number_match(std::basic_string s, C digitSeparator = C(','), C decimalPoint = C('.'), C exponential = C('e')) { using string_t = std::basic_string; str::trim(s); if(s.empty()) return subrange{}; auto i = s.find_first_of("0123456789+-"); if(i == string_t::npos) { i = s.find(decimalPoint); if(i == string_t::npos) return subrange{}; } bool point = false; bool sep = false; auto exp = string_t::npos; auto j = i + 1; for(; j < s.size(); ++j) { if(s[j] == digitSeparator) { if(!sep) sep = true; else break; } else { sep = false; if(s[j] == decimalPoint) { //only one decimal point before exponent allowed if(!point && exp == string_t::npos) point = true; else break; } else if(std::tolower(s[j]) == std::tolower(exponential)) { //only one exponent separator allowed if(exp == string_t::npos) exp = j; else break; } else if(exp != string_t::npos && (exp+1) == j) { //only sign or digit after exponent separator if(s[j] != '+' && s[j] != '-' && !std::isdigit(s[j])) break; } else if(!std::isdigit(s[j])) { break; } } } //if length == 1 then must be a digit if(j-i == 1 && !std::isdigit(s[i])) return subrange{}; return subrange{i,j-i}; } /*************************************************************************//** * * @brief returns first substring match (pos,len) * that represents an integer (with optional digit separators) * *****************************************************************************/ template subrange first_integer_match(std::basic_string s, C digitSeparator = C(',')) { using string_t = std::basic_string; str::trim(s); if(s.empty()) return subrange{}; auto i = s.find_first_of("0123456789+-"); if(i == string_t::npos) return subrange{}; bool sep = false; auto j = i + 1; for(; j < s.size(); ++j) { if(s[j] == digitSeparator) { if(!sep) sep = true; else break; } else { sep = false; if(!std::isdigit(s[j])) break; } } //if length == 1 then must be a digit if(j-i == 1 && !std::isdigit(s[i])) return subrange{}; return subrange{i,j-i}; } /*************************************************************************//** * * @brief returns true if candidate string represents a number * *****************************************************************************/ template bool represents_number(const std::basic_string& candidate, C digitSeparator = C(','), C decimalPoint = C('.'), C exponential = C('e')) { const auto match = str::first_number_match(candidate, digitSeparator, decimalPoint, exponential); return (match && match.length() == candidate.size()); } /*************************************************************************//** * * @brief returns true if candidate string represents an integer * *****************************************************************************/ template bool represents_integer(const std::basic_string& candidate, C digitSeparator = C(',')) { const auto match = str::first_integer_match(candidate, digitSeparator); return (match && match.length() == candidate.size()); } } // namespace str /*************************************************************************//** * * @brief makes function object with a const char* parameter * that assigns a value to a ref-captured object * *****************************************************************************/ template inline detail::assign_value set(T& target, V value) { return detail::assign_value{target, std::move(value)}; } /*************************************************************************//** * * @brief makes parameter-less function object * that assigns value(s) to a ref-captured object; * value(s) are obtained by converting the const char* argument to * the captured object types; * bools are always set to true if the argument is not nullptr * *****************************************************************************/ template inline detail::map_arg_to set(T& target) { return detail::map_arg_to{target}; } /*************************************************************************//** * * @brief makes function object that sets a bool to true * *****************************************************************************/ inline detail::assign_value set(bool& target) { return detail::assign_value{target,true}; } /*************************************************************************//** * * @brief makes function object that sets a bool to false * *****************************************************************************/ inline detail::assign_value unset(bool& target) { return detail::assign_value{target,false}; } /*************************************************************************//** * * @brief makes function object that flips the value of a ref-captured bool * *****************************************************************************/ inline detail::flip_bool flip(bool& b) { return detail::flip_bool(b); } /*************************************************************************//** * * @brief makes function object that increments using operator ++ * *****************************************************************************/ template inline detail::increment increment(T& target) { return detail::increment{target}; } /*************************************************************************//** * * @brief makes function object that decrements using operator -- * *****************************************************************************/ template inline detail::increment_by increment(T& target, T by) { return detail::increment_by{target, std::move(by)}; } /*************************************************************************//** * * @brief makes function object that increments by a fixed amount using operator += * *****************************************************************************/ template inline detail::decrement decrement(T& target) { return detail::decrement{target}; } /*************************************************************************//** * * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) * *****************************************************************************/ namespace detail { /*************************************************************************//** * * @brief mixin that provides action definition and execution * *****************************************************************************/ template class action_provider { private: //--------------------------------------------------------------- using simple_action = std::function; using arg_action = std::function; using index_action = std::function; //----------------------------------------------------- class simple_action_adapter { public: simple_action_adapter() = default; simple_action_adapter(const simple_action& a): action_(a) {} simple_action_adapter(simple_action&& a): action_(std::move(a)) {} void operator() (const char*) const { action_(); } void operator() (int) const { action_(); } private: simple_action action_; }; public: //--------------------------------------------------------------- /** @brief adds an action that has an operator() that is callable * with a 'const char*' argument */ Derived& call(arg_action a) { argActions_.push_back(std::move(a)); return *static_cast(this); } /** @brief adds an action that has an operator()() */ Derived& call(simple_action a) { argActions_.push_back(simple_action_adapter(std::move(a))); return *static_cast(this); } /** @brief adds an action that has an operator() that is callable * with a 'const char*' argument */ Derived& operator () (arg_action a) { return call(std::move(a)); } /** @brief adds an action that has an operator()() */ Derived& operator () (simple_action a) { return call(std::move(a)); } //--------------------------------------------------------------- /** @brief adds an action that will set the value of 't' from * a 'const char*' arg */ template Derived& set(Target& t) { static_assert(!std::is_pointer::value, "parameter target type must not be a pointer"); return call(clipp::set(t)); } /** @brief adds an action that will set the value of 't' to 'v' */ template Derived& set(Target& t, Value&& v) { return call(clipp::set(t, std::forward(v))); } //--------------------------------------------------------------- /** @brief adds an action that will be called if a parameter * matches an argument for the 2nd, 3rd, 4th, ... time */ Derived& if_repeated(simple_action a) { repeatActions_.push_back(simple_action_adapter{std::move(a)}); return *static_cast(this); } /** @brief adds an action that will be called with the argument's * index if a parameter matches an argument for * the 2nd, 3rd, 4th, ... time */ Derived& if_repeated(index_action a) { repeatActions_.push_back(std::move(a)); return *static_cast(this); } //--------------------------------------------------------------- /** @brief adds an action that will be called if a required parameter * is missing */ Derived& if_missing(simple_action a) { missingActions_.push_back(simple_action_adapter{std::move(a)}); return *static_cast(this); } /** @brief adds an action that will be called if a required parameter * is missing; the action will get called with the index of * the command line argument where the missing event occurred first */ Derived& if_missing(index_action a) { missingActions_.push_back(std::move(a)); return *static_cast(this); } //--------------------------------------------------------------- /** @brief adds an action that will be called if a parameter * was matched, but was unreachable in the current scope */ Derived& if_blocked(simple_action a) { blockedActions_.push_back(simple_action_adapter{std::move(a)}); return *static_cast(this); } /** @brief adds an action that will be called if a parameter * was matched, but was unreachable in the current scope; * the action will be called with the index of * the command line argument where the problem occurred */ Derived& if_blocked(index_action a) { blockedActions_.push_back(std::move(a)); return *static_cast(this); } //--------------------------------------------------------------- /** @brief adds an action that will be called if a parameter match * was in conflict with a different alternative parameter */ Derived& if_conflicted(simple_action a) { conflictActions_.push_back(simple_action_adapter{std::move(a)}); return *static_cast(this); } /** @brief adds an action that will be called if a parameter match * was in conflict with a different alternative parameter; * the action will be called with the index of * the command line argument where the problem occurred */ Derived& if_conflicted(index_action a) { conflictActions_.push_back(std::move(a)); return *static_cast(this); } //--------------------------------------------------------------- /** @brief adds targets = either objects whose values should be * set by command line arguments or actions that should * be called in case of a match */ template Derived& target(T&& t, Ts&&... ts) { target(std::forward(t)); target(std::forward(ts)...); return *static_cast(this); } /** @brief adds action that should be called in case of a match */ template::type>() && (traits::is_callable() || traits::is_callable() ) >::type> Derived& target(T&& t) { call(std::forward(t)); return *static_cast(this); } /** @brief adds object whose value should be set by command line arguments */ template::type>() || (!traits::is_callable() && !traits::is_callable() ) >::type> Derived& target(T& t) { set(t); return *static_cast(this); } //TODO remove ugly empty param list overload Derived& target() { return *static_cast(this); } //--------------------------------------------------------------- /** @brief adds target, see member function 'target' */ template inline friend Derived& operator << (Target&& t, Derived& p) { p.target(std::forward(t)); return p; } /** @brief adds target, see member function 'target' */ template inline friend Derived&& operator << (Target&& t, Derived&& p) { p.target(std::forward(t)); return std::move(p); } //----------------------------------------------------- /** @brief adds target, see member function 'target' */ template inline friend Derived& operator >> (Derived& p, Target&& t) { p.target(std::forward(t)); return p; } /** @brief adds target, see member function 'target' */ template inline friend Derived&& operator >> (Derived&& p, Target&& t) { p.target(std::forward(t)); return std::move(p); } //--------------------------------------------------------------- /** @brief executes all argument actions */ void execute_actions(const arg_string& arg) const { int i = 0; for(const auto& a : argActions_) { ++i; a(arg.c_str()); } } /** @brief executes repeat actions */ void notify_repeated(arg_index idx) const { for(const auto& a : repeatActions_) a(idx); } /** @brief executes missing error actions */ void notify_missing(arg_index idx) const { for(const auto& a : missingActions_) a(idx); } /** @brief executes blocked error actions */ void notify_blocked(arg_index idx) const { for(const auto& a : blockedActions_) a(idx); } /** @brief executes conflict error actions */ void notify_conflict(arg_index idx) const { for(const auto& a : conflictActions_) a(idx); } private: //--------------------------------------------------------------- std::vector argActions_; std::vector repeatActions_; std::vector missingActions_; std::vector blockedActions_; std::vector conflictActions_; }; /*************************************************************************//** * * @brief mixin that provides basic common settings of parameters and groups * *****************************************************************************/ template class token { public: //--------------------------------------------------------------- using doc_string = clipp::doc_string; //--------------------------------------------------------------- /** @brief returns documentation string */ const doc_string& doc() const noexcept { return doc_; } /** @brief sets documentations string */ Derived& doc(const doc_string& txt) { doc_ = txt; return *static_cast(this); } /** @brief sets documentations string */ Derived& doc(doc_string&& txt) { doc_ = std::move(txt); return *static_cast(this); } //--------------------------------------------------------------- /** @brief returns if a group/parameter is repeatable */ bool repeatable() const noexcept { return repeatable_; } /** @brief sets repeatability of group/parameter */ Derived& repeatable(bool yes) noexcept { repeatable_ = yes; return *static_cast(this); } //--------------------------------------------------------------- /** @brief returns if a group/parameter is blocking/positional */ bool blocking() const noexcept { return blocking_; } /** @brief determines, if a group/parameter is blocking/positional */ Derived& blocking(bool yes) noexcept { blocking_ = yes; return *static_cast(this); } private: //--------------------------------------------------------------- doc_string doc_; bool repeatable_ = false; bool blocking_ = false; }; /*************************************************************************//** * * @brief sets documentation strings on a token * *****************************************************************************/ template inline T& operator % (doc_string docstr, token& p) { return p.doc(std::move(docstr)); } //--------------------------------------------------------- template inline T&& operator % (doc_string docstr, token&& p) { return std::move(p.doc(std::move(docstr))); } //--------------------------------------------------------- template inline T& operator % (token& p, doc_string docstr) { return p.doc(std::move(docstr)); } //--------------------------------------------------------- template inline T&& operator % (token&& p, doc_string docstr) { return std::move(p.doc(std::move(docstr))); } /*************************************************************************//** * * @brief sets documentation strings on a token * *****************************************************************************/ template inline T& doc(doc_string docstr, token& p) { return p.doc(std::move(docstr)); } //--------------------------------------------------------- template inline T&& doc(doc_string docstr, token&& p) { return std::move(p.doc(std::move(docstr))); } } // namespace detail /*************************************************************************//** * * @brief contains parameter matching functions and function classes * *****************************************************************************/ namespace match { /*************************************************************************//** * * @brief predicate that is always true * *****************************************************************************/ inline bool any(const arg_string&) { return true; } /*************************************************************************//** * * @brief predicate that is always false * *****************************************************************************/ inline bool none(const arg_string&) { return false; } /*************************************************************************//** * * @brief predicate that returns true if the argument string is non-empty string * *****************************************************************************/ inline bool nonempty(const arg_string& s) { return !s.empty(); } /*************************************************************************//** * * @brief predicate that returns true if the argument is a non-empty * string that consists only of alphanumeric characters * *****************************************************************************/ inline bool alphanumeric(const arg_string& s) { if(s.empty()) return false; return std::all_of(s.begin(), s.end(), [](char c) {return std::isalnum(c); }); } /*************************************************************************//** * * @brief predicate that returns true if the argument is a non-empty * string that consists only of alphabetic characters * *****************************************************************************/ inline bool alphabetic(const arg_string& s) { return std::all_of(s.begin(), s.end(), [](char c) {return std::isalpha(c); }); } /*************************************************************************//** * * @brief predicate that returns false if the argument string is * equal to any string from the exclusion list * *****************************************************************************/ class none_of { public: none_of(arg_list strs): excluded_{std::move(strs)} {} template none_of(arg_string str, Strings&&... strs): excluded_{std::move(str), std::forward(strs)...} {} template none_of(const char* str, Strings&&... strs): excluded_{arg_string(str), std::forward(strs)...} {} bool operator () (const arg_string& arg) const { return (std::find(begin(excluded_), end(excluded_), arg) == end(excluded_)); } private: arg_list excluded_; }; /*************************************************************************//** * * @brief predicate that returns the first substring match within the input * string that rmeepresents a number * (with at maximum one decimal point and digit separators) * *****************************************************************************/ class numbers { public: explicit numbers(char decimalPoint = '.', char digitSeparator = ' ', char exponentSeparator = 'e') : decpoint_{decimalPoint}, separator_{digitSeparator}, exp_{exponentSeparator} {} subrange operator () (const arg_string& s) const { return str::first_number_match(s, separator_, decpoint_, exp_); } private: char decpoint_; char separator_; char exp_; }; /*************************************************************************//** * * @brief predicate that returns true if the input string represents an integer * (with optional digit separators) * *****************************************************************************/ class integers { public: explicit integers(char digitSeparator = ' '): separator_{digitSeparator} {} subrange operator () (const arg_string& s) const { return str::first_integer_match(s, separator_); } private: char separator_; }; /*************************************************************************//** * * @brief predicate that returns true if the input string represents * a non-negative integer (with optional digit separators) * *****************************************************************************/ class positive_integers { public: explicit positive_integers(char digitSeparator = ' '): separator_{digitSeparator} {} subrange operator () (const arg_string& s) const { auto match = str::first_integer_match(s, separator_); if(!match) return subrange{}; if(s[match.at()] == '-') return subrange{}; return match; } private: char separator_; }; /*************************************************************************//** * * @brief predicate that returns true if the input string * contains a given substring * *****************************************************************************/ class substring { public: explicit substring(arg_string str): str_{std::move(str)} {} subrange operator () (const arg_string& s) const { return str::substring_match(s, str_); } private: arg_string str_; }; /*************************************************************************//** * * @brief predicate that returns true if the input string starts * with a given prefix * *****************************************************************************/ class prefix { public: explicit prefix(arg_string p): prefix_{std::move(p)} {} bool operator () (const arg_string& s) const { return s.find(prefix_) == 0; } private: arg_string prefix_; }; /*************************************************************************//** * * @brief predicate that returns true if the input string does not start * with a given prefix * *****************************************************************************/ class prefix_not { public: explicit prefix_not(arg_string p): prefix_{std::move(p)} {} bool operator () (const arg_string& s) const { return s.find(prefix_) != 0; } private: arg_string prefix_; }; /** @brief alias for prefix_not */ using noprefix = prefix_not; /*************************************************************************//** * * @brief predicate that returns true if the length of the input string * is wihtin a given interval * *****************************************************************************/ class length { public: explicit length(std::size_t exact): min_{exact}, max_{exact} {} explicit length(std::size_t min, std::size_t max): min_{min}, max_{max} {} bool operator () (const arg_string& s) const { return s.size() >= min_ && s.size() <= max_; } private: std::size_t min_; std::size_t max_; }; /*************************************************************************//** * * @brief makes function object that returns true if the input string has a * given minimum length * *****************************************************************************/ inline length min_length(std::size_t min) { return length{min, arg_string::npos-1}; } /*************************************************************************//** * * @brief makes function object that returns true if the input string is * not longer than a given maximum length * *****************************************************************************/ inline length max_length(std::size_t max) { return length{0, max}; } } // namespace match /*************************************************************************//** * * @brief command line parameter that can match one or many arguments. * *****************************************************************************/ class parameter : public detail::token, public detail::action_provider { /** @brief adapts a 'match_predicate' to the 'match_function' interface */ class predicate_adapter { public: explicit predicate_adapter(match_predicate pred): match_{std::move(pred)} {} subrange operator () (const arg_string& arg) const { return match_(arg) ? subrange{0,arg.size()} : subrange{}; } private: match_predicate match_; }; public: //--------------------------------------------------------------- /** @brief makes default parameter, that will match nothing */ parameter(): flags_{}, matcher_{predicate_adapter{match::none}}, label_{}, required_{false}, greedy_{false} {} /** @brief makes "flag" parameter */ template explicit parameter(arg_string str, Strings&&... strs): flags_{}, matcher_{predicate_adapter{match::none}}, label_{}, required_{false}, greedy_{false} { add_flags(std::move(str), std::forward(strs)...); } /** @brief makes "flag" parameter from range of strings */ explicit parameter(const arg_list& flaglist): flags_{}, matcher_{predicate_adapter{match::none}}, label_{}, required_{false}, greedy_{false} { add_flags(flaglist); } //----------------------------------------------------- /** @brief makes "value" parameter with custom match predicate * (= yes/no matcher) */ explicit parameter(match_predicate filter): flags_{}, matcher_{predicate_adapter{std::move(filter)}}, label_{}, required_{false}, greedy_{false} {} /** @brief makes "value" parameter with custom match function * (= partial matcher) */ explicit parameter(match_function filter): flags_{}, matcher_{std::move(filter)}, label_{}, required_{false}, greedy_{false} {} //--------------------------------------------------------------- /** @brief returns if a parameter is required */ bool required() const noexcept { return required_; } /** @brief determines if a parameter is required */ parameter& required(bool yes) noexcept { required_ = yes; return *this; } //--------------------------------------------------------------- /** @brief returns if a parameter should match greedily */ bool greedy() const noexcept { return greedy_; } /** @brief determines if a parameter should match greedily */ parameter& greedy(bool yes) noexcept { greedy_ = yes; return *this; } //--------------------------------------------------------------- /** @brief returns parameter label; * will be used for documentation, if flags are empty */ const doc_string& label() const { return label_; } /** @brief sets parameter label; * will be used for documentation, if flags are empty */ parameter& label(const doc_string& lbl) { label_ = lbl; return *this; } /** @brief sets parameter label; * will be used for documentation, if flags are empty */ parameter& label(doc_string&& lbl) { label_ = lbl; return *this; } //--------------------------------------------------------------- /** @brief returns either longest matching prefix of 'arg' in any * of the flags or the result of the custom match operation */ subrange match(const arg_string& arg) const { if(flags_.empty()) { return matcher_(arg); } else { //empty flags are not allowed if(arg.empty()) return subrange{}; if(std::find(flags_.begin(), flags_.end(), arg) != flags_.end()) { return subrange{0,arg.size()}; } return str::longest_prefix_match(arg, flags_); } } //--------------------------------------------------------------- /** @brief access range of flag strings */ const arg_list& flags() const noexcept { return flags_; } /** @brief access custom match operation */ const match_function& matcher() const noexcept { return matcher_; } //--------------------------------------------------------------- /** @brief prepend prefix to each flag */ inline friend parameter& with_prefix(const arg_string& prefix, parameter& p) { if(prefix.empty() || p.flags().empty()) return p; for(auto& f : p.flags_) { if(f.find(prefix) != 0) f.insert(0, prefix); } return p; } /** @brief prepend prefix to each flag */ inline friend parameter& with_prefixes_short_long( const arg_string& shortpfx, const arg_string& longpfx, parameter& p) { if(shortpfx.empty() && longpfx.empty()) return p; if(p.flags().empty()) return p; for(auto& f : p.flags_) { if(f.size() == 1) { if(f.find(shortpfx) != 0) f.insert(0, shortpfx); } else { if(f.find(longpfx) != 0) f.insert(0, longpfx); } } return p; } //--------------------------------------------------------------- /** @brief prepend suffix to each flag */ inline friend parameter& with_suffix(const arg_string& suffix, parameter& p) { if(suffix.empty() || p.flags().empty()) return p; for(auto& f : p.flags_) { if(f.find(suffix) + suffix.size() != f.size()) { f.insert(f.end(), suffix.begin(), suffix.end()); } } return p; } /** @brief prepend suffix to each flag */ inline friend parameter& with_suffixes_short_long( const arg_string& shortsfx, const arg_string& longsfx, parameter& p) { if(shortsfx.empty() && longsfx.empty()) return p; if(p.flags().empty()) return p; for(auto& f : p.flags_) { if(f.size() == 1) { if(f.find(shortsfx) + shortsfx.size() != f.size()) { f.insert(f.end(), shortsfx.begin(), shortsfx.end()); } } else { if(f.find(longsfx) + longsfx.size() != f.size()) { f.insert(f.end(), longsfx.begin(), longsfx.end()); } } } return p; } private: //--------------------------------------------------------------- void add_flags(arg_string str) { //empty flags are not allowed str::remove_ws(str); if(!str.empty()) flags_.push_back(std::move(str)); } //--------------------------------------------------------------- void add_flags(const arg_list& strs) { if(strs.empty()) return; flags_.reserve(flags_.size() + strs.size()); for(const auto& s : strs) add_flags(s); } template void add_flags(String1&& s1, String2&& s2, Strings&&... ss) { flags_.reserve(2 + sizeof...(ss)); add_flags(std::forward(s1)); add_flags(std::forward(s2), std::forward(ss)...); } arg_list flags_; match_function matcher_; doc_string label_; bool required_ = false; bool greedy_ = false; }; /*************************************************************************//** * * @brief makes required non-blocking exact match parameter * *****************************************************************************/ template inline parameter command(String&& flag, Strings&&... flags) { return parameter{std::forward(flag), std::forward(flags)...} .required(true).blocking(true).repeatable(false); } /*************************************************************************//** * * @brief makes required non-blocking exact match parameter * *****************************************************************************/ template inline parameter required(String&& flag, Strings&&... flags) { return parameter{std::forward(flag), std::forward(flags)...} .required(true).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes optional, non-blocking exact match parameter * *****************************************************************************/ template inline parameter option(String&& flag, Strings&&... flags) { return parameter{std::forward(flag), std::forward(flags)...} .required(false).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes required, blocking, repeatable value parameter; * matches any non-empty string * *****************************************************************************/ template inline parameter value(const doc_string& label, Targets&&... tgts) { return parameter{match::nonempty} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(false); } template::value || traits::is_callable::value>::type> inline parameter value(Filter&& filter, doc_string label, Targets&&... tgts) { return parameter{std::forward(filter)} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(false); } /*************************************************************************//** * * @brief makes required, blocking, repeatable value parameter; * matches any non-empty string * *****************************************************************************/ template inline parameter values(const doc_string& label, Targets&&... tgts) { return parameter{match::nonempty} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(true); } template::value || traits::is_callable::value>::type> inline parameter values(Filter&& filter, doc_string label, Targets&&... tgts) { return parameter{std::forward(filter)} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(true); } /*************************************************************************//** * * @brief makes optional, blocking value parameter; * matches any non-empty string * *****************************************************************************/ template inline parameter opt_value(const doc_string& label, Targets&&... tgts) { return parameter{match::nonempty} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(false); } template::value || traits::is_callable::value>::type> inline parameter opt_value(Filter&& filter, doc_string label, Targets&&... tgts) { return parameter{std::forward(filter)} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes optional, blocking, repeatable value parameter; * matches any non-empty string * *****************************************************************************/ template inline parameter opt_values(const doc_string& label, Targets&&... tgts) { return parameter{match::nonempty} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } template::value || traits::is_callable::value>::type> inline parameter opt_values(Filter&& filter, doc_string label, Targets&&... tgts) { return parameter{std::forward(filter)} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief makes required, blocking value parameter; * matches any string consisting of alphanumeric characters * *****************************************************************************/ template inline parameter word(const doc_string& label, Targets&&... tgts) { return parameter{match::alphanumeric} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(false); } /*************************************************************************//** * * @brief makes required, blocking, repeatable value parameter; * matches any string consisting of alphanumeric characters * *****************************************************************************/ template inline parameter words(const doc_string& label, Targets&&... tgts) { return parameter{match::alphanumeric} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(true); } /*************************************************************************//** * * @brief makes optional, blocking value parameter; * matches any string consisting of alphanumeric characters * *****************************************************************************/ template inline parameter opt_word(const doc_string& label, Targets&&... tgts) { return parameter{match::alphanumeric} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes optional, blocking, repeatable value parameter; * matches any string consisting of alphanumeric characters * *****************************************************************************/ template inline parameter opt_words(const doc_string& label, Targets&&... tgts) { return parameter{match::alphanumeric} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief makes required, blocking value parameter; * matches any string that represents a number * *****************************************************************************/ template inline parameter number(const doc_string& label, Targets&&... tgts) { return parameter{match::numbers{}} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(false); } /*************************************************************************//** * * @brief makes required, blocking, repeatable value parameter; * matches any string that represents a number * *****************************************************************************/ template inline parameter numbers(const doc_string& label, Targets&&... tgts) { return parameter{match::numbers{}} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(true); } /*************************************************************************//** * * @brief makes optional, blocking value parameter; * matches any string that represents a number * *****************************************************************************/ template inline parameter opt_number(const doc_string& label, Targets&&... tgts) { return parameter{match::numbers{}} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes optional, blocking, repeatable value parameter; * matches any string that represents a number * *****************************************************************************/ template inline parameter opt_numbers(const doc_string& label, Targets&&... tgts) { return parameter{match::numbers{}} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief makes required, blocking value parameter; * matches any string that represents an integer * *****************************************************************************/ template inline parameter integer(const doc_string& label, Targets&&... tgts) { return parameter{match::integers{}} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(false); } /*************************************************************************//** * * @brief makes required, blocking, repeatable value parameter; * matches any string that represents an integer * *****************************************************************************/ template inline parameter integers(const doc_string& label, Targets&&... tgts) { return parameter{match::integers{}} .label(label) .target(std::forward(tgts)...) .required(true).blocking(true).repeatable(true); } /*************************************************************************//** * * @brief makes optional, blocking value parameter; * matches any string that represents an integer * *****************************************************************************/ template inline parameter opt_integer(const doc_string& label, Targets&&... tgts) { return parameter{match::integers{}} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(false); } /*************************************************************************//** * * @brief makes optional, blocking, repeatable value parameter; * matches any string that represents an integer * *****************************************************************************/ template inline parameter opt_integers(const doc_string& label, Targets&&... tgts) { return parameter{match::integers{}} .label(label) .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief makes catch-all value parameter * *****************************************************************************/ template inline parameter any_other(Targets&&... tgts) { return parameter{match::any} .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief makes catch-all value parameter with custom filter * *****************************************************************************/ template::value || traits::is_callable::value>::type> inline parameter any(Filter&& filter, Targets&&... tgts) { return parameter{std::forward(filter)} .target(std::forward(tgts)...) .required(false).blocking(false).repeatable(true); } /*************************************************************************//** * * @brief group of parameters and/or other groups; * can be configured to act as a group of alternatives (exclusive match) * *****************************************************************************/ class group : public detail::token { //--------------------------------------------------------------- /** * @brief tagged union type that either stores a parameter or a group * and provides a common interface to them * could be replaced by std::variant in the future * * Note to future self: do NOT try again to do this with * dynamic polymorphism; there are a couple of * nasty problems associated with it and the implementation * becomes bloated and needlessly complicated. */ template struct child_t { enum class type : char {param, group}; public: explicit child_t(const Param& v) : m_{v}, type_{type::param} {} child_t( Param&& v) noexcept : m_{std::move(v)}, type_{type::param} {} explicit child_t(const Group& g) : m_{g}, type_{type::group} {} child_t( Group&& g) noexcept : m_{std::move(g)}, type_{type::group} {} child_t(const child_t& src): type_{src.type_} { switch(type_) { default: case type::param: new(&m_)data{src.m_.param}; break; case type::group: new(&m_)data{src.m_.group}; break; } } child_t(child_t&& src) noexcept : type_{src.type_} { switch(type_) { default: case type::param: new(&m_)data{std::move(src.m_.param)}; break; case type::group: new(&m_)data{std::move(src.m_.group)}; break; } } child_t& operator = (const child_t& src) { destroy_content(); type_ = src.type_; switch(type_) { default: case type::param: new(&m_)data{src.m_.param}; break; case type::group: new(&m_)data{src.m_.group}; break; } return *this; } child_t& operator = (child_t&& src) noexcept { destroy_content(); type_ = src.type_; switch(type_) { default: case type::param: new(&m_)data{std::move(src.m_.param)}; break; case type::group: new(&m_)data{std::move(src.m_.group)}; break; } return *this; } ~child_t() { destroy_content(); } const doc_string& doc() const noexcept { switch(type_) { default: case type::param: return m_.param.doc(); case type::group: return m_.group.doc(); } } bool blocking() const noexcept { switch(type_) { case type::param: return m_.param.blocking(); case type::group: return m_.group.blocking(); default: return false; } } bool repeatable() const noexcept { switch(type_) { case type::param: return m_.param.repeatable(); case type::group: return m_.group.repeatable(); default: return false; } } bool required() const noexcept { switch(type_) { case type::param: return m_.param.required(); case type::group: return (m_.group.exclusive() && m_.group.all_required() ) || (!m_.group.exclusive() && m_.group.any_required() ); default: return false; } } bool exclusive() const noexcept { switch(type_) { case type::group: return m_.group.exclusive(); case type::param: default: return false; } } std::size_t param_count() const noexcept { switch(type_) { case type::group: return m_.group.param_count(); case type::param: default: return std::size_t(1); } } std::size_t depth() const noexcept { switch(type_) { case type::group: return m_.group.depth(); case type::param: default: return std::size_t(0); } } void execute_actions(const arg_string& arg) const { switch(type_) { default: case type::group: return; case type::param: m_.param.execute_actions(arg); break; } } void notify_repeated(arg_index idx) const { switch(type_) { default: case type::group: return; case type::param: m_.param.notify_repeated(idx); break; } } void notify_missing(arg_index idx) const { switch(type_) { default: case type::group: return; case type::param: m_.param.notify_missing(idx); break; } } void notify_blocked(arg_index idx) const { switch(type_) { default: case type::group: return; case type::param: m_.param.notify_blocked(idx); break; } } void notify_conflict(arg_index idx) const { switch(type_) { default: case type::group: return; case type::param: m_.param.notify_conflict(idx); break; } } bool is_param() const noexcept { return type_ == type::param; } bool is_group() const noexcept { return type_ == type::group; } Param& as_param() noexcept { return m_.param; } Group& as_group() noexcept { return m_.group; } const Param& as_param() const noexcept { return m_.param; } const Group& as_group() const noexcept { return m_.group; } private: void destroy_content() { switch(type_) { default: case type::param: m_.param.~Param(); break; case type::group: m_.group.~Group(); break; } } union data { data() {} data(const Param& v) : param{v} {} data( Param&& v) noexcept : param{std::move(v)} {} data(const Group& g) : group{g} {} data( Group&& g) noexcept : group{std::move(g)} {} ~data() {} Param param; Group group; }; data m_; type type_; }; public: //--------------------------------------------------------------- using child = child_t; using value_type = child; private: using children_store = std::vector; public: using const_iterator = children_store::const_iterator; using iterator = children_store::iterator; using size_type = children_store::size_type; //--------------------------------------------------------------- /** * @brief recursively iterates over all nodes */ class depth_first_traverser { public: //----------------------------------------------------- struct context { context() = default; context(const group& p): parent{&p}, cur{p.begin()}, end{p.end()} {} const group* parent = nullptr; const_iterator cur; const_iterator end; }; using context_list = std::vector; //----------------------------------------------------- class memento { friend class depth_first_traverser; int level_; context context_; public: int level() const noexcept { return level_; } const child* param() const noexcept { return &(*context_.cur); } }; depth_first_traverser() = default; explicit depth_first_traverser(const group& cur): stack_{} { if(!cur.empty()) stack_.emplace_back(cur); } explicit operator bool() const noexcept { return !stack_.empty(); } int level() const noexcept { return int(stack_.size()); } bool is_first_in_parent() const noexcept { if(stack_.empty()) return false; return (stack_.back().cur == stack_.back().parent->begin()); } bool is_last_in_parent() const noexcept { if(stack_.empty()) return false; return (stack_.back().cur+1 == stack_.back().end); } bool is_last_in_path() const noexcept { if(stack_.empty()) return false; for(const auto& t : stack_) { if(t.cur+1 != t.end) return false; } const auto& top = stack_.back(); //if we have to descend into group on next ++ => not last in path if(top.cur->is_group()) return false; return true; } /** @brief inside a group of alternatives >= minlevel */ bool is_alternative(int minlevel = 0) const noexcept { if(stack_.empty()) return false; if(minlevel > 0) minlevel -= 1; if(minlevel >= int(stack_.size())) return false; return std::any_of(stack_.begin() + minlevel, stack_.end(), [](const context& c) { return c.parent->exclusive(); }); } /** @brief repeatable or inside a repeatable group >= minlevel */ bool is_repeatable(int minlevel = 0) const noexcept { if(stack_.empty()) return false; if(stack_.back().cur->repeatable()) return true; if(minlevel > 0) minlevel -= 1; if(minlevel >= int(stack_.size())) return false; return std::any_of(stack_.begin() + minlevel, stack_.end(), [](const context& c) { return c.parent->repeatable(); }); } /** @brief inside a particular group */ bool is_inside(const group* g) const noexcept { if(!g) return false; return std::any_of(stack_.begin(), stack_.end(), [g](const context& c) { return c.parent == g; }); } /** @brief inside group with joinable flags */ bool joinable() const noexcept { if(stack_.empty()) return false; return std::any_of(stack_.begin(), stack_.end(), [](const context& c) { return c.parent->joinable(); }); } const context_list& stack() const { return stack_; } /** @brief innermost repeat group */ const group* innermost_repeat_group() const noexcept { auto i = std::find_if(stack_.rbegin(), stack_.rend(), [](const context& c) { return c.parent->repeatable(); }); return i != stack_.rend() ? i->parent : nullptr; } /** @brief innermost exclusive (alternatives) group */ const group* innermost_exclusive_group() const noexcept { auto i = std::find_if(stack_.rbegin(), stack_.rend(), [](const context& c) { return c.parent->exclusive(); }); return i != stack_.rend() ? i->parent : nullptr; } /** @brief innermost blocking group */ const group* innermost_blocking_group() const noexcept { auto i = std::find_if(stack_.rbegin(), stack_.rend(), [](const context& c) { return c.parent->blocking(); }); return i != stack_.rend() ? i->parent : nullptr; } /** @brief returns the outermost group that will be left on next ++*/ const group* outermost_blocking_group_fully_explored() const noexcept { if(stack_.empty()) return nullptr; const group* g = nullptr; for(auto i = stack_.rbegin(); i != stack_.rend(); ++i) { if(i->cur+1 == i->end) { if(i->parent->blocking()) g = i->parent; } else { return g; } } return g; } /** @brief outermost join group */ const group* outermost_join_group() const noexcept { auto i = std::find_if(stack_.begin(), stack_.end(), [](const context& c) { return c.parent->joinable(); }); return i != stack_.end() ? i->parent : nullptr; } const group* root() const noexcept { return stack_.empty() ? nullptr : stack_.front().parent; } /** @brief common flag prefix of all flags in current group */ arg_string common_flag_prefix() const noexcept { if(stack_.empty()) return ""; auto g = outermost_join_group(); return g ? g->common_flag_prefix() : arg_string(""); } const child& operator * () const noexcept { return *stack_.back().cur; } const child* operator -> () const noexcept { return &(*stack_.back().cur); } const group& parent() const noexcept { return *(stack_.back().parent); } /** @brief go to next element of depth first search */ depth_first_traverser& operator ++ () { if(stack_.empty()) return *this; //at group -> decend into group if(stack_.back().cur->is_group()) { stack_.emplace_back(stack_.back().cur->as_group()); } else { next_sibling(); } return *this; } /** @brief go to next sibling of current */ depth_first_traverser& next_sibling() { if(stack_.empty()) return *this; ++stack_.back().cur; //at the end of current group? while(stack_.back().cur == stack_.back().end) { //go to parent stack_.pop_back(); if(stack_.empty()) return *this; //go to next sibling in parent ++stack_.back().cur; } return *this; } /** @brief go to next position after siblings of current */ depth_first_traverser& next_after_siblings() { if(stack_.empty()) return *this; stack_.back().cur = stack_.back().end-1; next_sibling(); return *this; } /** * @brief */ depth_first_traverser& back_to_ancestor(const group* g) { if(!g) return *this; while(!stack_.empty()) { const auto& top = stack_.back().cur; if(top->is_group() && &(top->as_group()) == g) return *this; stack_.pop_back(); } return *this; } /** @brief don't visit next siblings, go back to parent on next ++ * note: renders siblings unreachable for *this **/ depth_first_traverser& skip_siblings() { if(stack_.empty()) return *this; //future increments won't visit subsequent siblings: stack_.back().end = stack_.back().cur+1; return *this; } /** @brief skips all other alternatives in surrounding exclusive groups * on next ++ * note: renders alternatives unreachable for *this */ depth_first_traverser& skip_alternatives() { if(stack_.empty()) return *this; //exclude all other alternatives in surrounding groups //by making their current position the last one for(auto& c : stack_) { if(c.parent && c.parent->exclusive() && c.cur < c.end) c.end = c.cur+1; } return *this; } void invalidate() { stack_.clear(); } inline friend bool operator == (const depth_first_traverser& a, const depth_first_traverser& b) { if(a.stack_.empty() || b.stack_.empty()) return false; //parents not the same -> different position if(a.stack_.back().parent != b.stack_.back().parent) return false; bool aEnd = a.stack_.back().cur == a.stack_.back().end; bool bEnd = b.stack_.back().cur == b.stack_.back().end; //either both at the end of the same parent => same position if(aEnd && bEnd) return true; //or only one at the end => not at the same position if(aEnd || bEnd) return false; return std::addressof(*a.stack_.back().cur) == std::addressof(*b.stack_.back().cur); } inline friend bool operator != (const depth_first_traverser& a, const depth_first_traverser& b) { return !(a == b); } memento undo_point() const { memento m; m.level_ = int(stack_.size()); if(!stack_.empty()) m.context_ = stack_.back(); return m; } void undo(const memento& m) { if(m.level_ < 1) return; if(m.level_ <= int(stack_.size())) { stack_.erase(stack_.begin() + m.level_, stack_.end()); stack_.back() = m.context_; } else if(stack_.empty() && m.level_ == 1) { stack_.push_back(m.context_); } } private: context_list stack_; }; //--------------------------------------------------------------- group() = default; template explicit group(doc_string docstr, Param param, Params... params): children_{}, exclusive_{false}, joinable_{false}, scoped_{true} { doc(std::move(docstr)); push_back(std::move(param), std::move(params)...); } template explicit group(parameter param, Params... params): children_{}, exclusive_{false}, joinable_{false}, scoped_{true} { push_back(std::move(param), std::move(params)...); } template explicit group(group p1, P2 p2, Ps... ps): children_{}, exclusive_{false}, joinable_{false}, scoped_{true} { push_back(std::move(p1), std::move(p2), std::move(ps)...); } //----------------------------------------------------- group(const group&) = default; group(group&&) = default; //--------------------------------------------------------------- group& operator = (const group&) = default; group& operator = (group&&) = default; //--------------------------------------------------------------- /** @brief determines if a command line argument can be matched by a * combination of (partial) matches through any number of children */ group& joinable(bool yes) { joinable_ = yes; return *this; } /** @brief returns if a command line argument can be matched by a * combination of (partial) matches through any number of children */ bool joinable() const noexcept { return joinable_; } //--------------------------------------------------------------- /** @brief turns explicit scoping on or off * operators , & | and other combinating functions will * not merge groups that are marked as scoped */ group& scoped(bool yes) { scoped_ = yes; return *this; } /** @brief returns true if operators , & | and other combinating functions * will merge groups and false otherwise */ bool scoped() const noexcept { return scoped_; } //--------------------------------------------------------------- /** @brief determines if children are mutually exclusive alternatives */ group& exclusive(bool yes) { exclusive_ = yes; return *this; } /** @brief returns if children are mutually exclusive alternatives */ bool exclusive() const noexcept { return exclusive_; } //--------------------------------------------------------------- /** @brief returns true, if any child is required to match */ bool any_required() const { return std::any_of(children_.begin(), children_.end(), [](const child& n){ return n.required(); }); } /** @brief returns true, if all children are required to match */ bool all_required() const { return std::all_of(children_.begin(), children_.end(), [](const child& n){ return n.required(); }); } //--------------------------------------------------------------- /** @brief returns true if any child is optional (=non-required) */ bool any_optional() const { return !all_required(); } /** @brief returns true if all children are optional (=non-required) */ bool all_optional() const { return !any_required(); } //--------------------------------------------------------------- /** @brief returns if the entire group is blocking / positional */ bool blocking() const noexcept { return token::blocking() || (exclusive() && all_blocking()); } //----------------------------------------------------- /** @brief determines if the entire group is blocking / positional */ group& blocking(bool yes) { return token::blocking(yes); } //--------------------------------------------------------------- /** @brief returns true if any child is blocking */ bool any_blocking() const { return std::any_of(children_.begin(), children_.end(), [](const child& n){ return n.blocking(); }); } //--------------------------------------------------------------- /** @brief returns true if all children is blocking */ bool all_blocking() const { return std::all_of(children_.begin(), children_.end(), [](const child& n){ return n.blocking(); }); } //--------------------------------------------------------------- /** @brief returns if any child is a value parameter (recursive) */ bool any_flagless() const { return std::any_of(children_.begin(), children_.end(), [](const child& p){ return p.is_param() && p.as_param().flags().empty(); }); } /** @brief returns if all children are value parameters (recursive) */ bool all_flagless() const { return std::all_of(children_.begin(), children_.end(), [](const child& p){ return p.is_param() && p.as_param().flags().empty(); }); } //--------------------------------------------------------------- /** @brief adds child parameter at the end */ group& push_back(const parameter& v) { children_.emplace_back(v); return *this; } //----------------------------------------------------- /** @brief adds child parameter at the end */ group& push_back(parameter&& v) { children_.emplace_back(std::move(v)); return *this; } //----------------------------------------------------- /** @brief adds child group at the end */ group& push_back(const group& g) { children_.emplace_back(g); return *this; } //----------------------------------------------------- /** @brief adds child group at the end */ group& push_back(group&& g) { children_.emplace_back(std::move(g)); return *this; } //----------------------------------------------------- /** @brief adds children (groups and/or parameters) */ template group& push_back(Param1&& param1, Param2&& param2, Params&&... params) { children_.reserve(children_.size() + 2 + sizeof...(params)); push_back(std::forward(param1)); push_back(std::forward(param2), std::forward(params)...); return *this; } //--------------------------------------------------------------- /** @brief adds child parameter at the beginning */ group& push_front(const parameter& v) { children_.emplace(children_.begin(), v); return *this; } //----------------------------------------------------- /** @brief adds child parameter at the beginning */ group& push_front(parameter&& v) { children_.emplace(children_.begin(), std::move(v)); return *this; } //----------------------------------------------------- /** @brief adds child group at the beginning */ group& push_front(const group& g) { children_.emplace(children_.begin(), g); return *this; } //----------------------------------------------------- /** @brief adds child group at the beginning */ group& push_front(group&& g) { children_.emplace(children_.begin(), std::move(g)); return *this; } //--------------------------------------------------------------- /** @brief adds all children of other group at the end */ group& merge(group&& g) { children_.insert(children_.end(), std::make_move_iterator(g.begin()), std::make_move_iterator(g.end())); return *this; } //----------------------------------------------------- /** @brief adds all children of several other groups at the end */ template group& merge(group&& g1, group&& g2, Groups&&... gs) { merge(std::move(g1)); merge(std::move(g2), std::forward(gs)...); return *this; } //--------------------------------------------------------------- /** @brief indexed, nutable access to child */ child& operator [] (size_type index) noexcept { return children_[index]; } /** @brief indexed, non-nutable access to child */ const child& operator [] (size_type index) const noexcept { return children_[index]; } //--------------------------------------------------------------- /** @brief mutable access to first child */ child& front() noexcept { return children_.front(); } /** @brief non-mutable access to first child */ const child& front() const noexcept { return children_.front(); } //----------------------------------------------------- /** @brief mutable access to last child */ child& back() noexcept { return children_.back(); } /** @brief non-mutable access to last child */ const child& back() const noexcept { return children_.back(); } //--------------------------------------------------------------- /** @brief returns true, if group has no children, false otherwise */ bool empty() const noexcept { return children_.empty(); } /** @brief returns number of children */ size_type size() const noexcept { return children_.size(); } /** @brief returns number of nested levels; 1 for a flat group */ size_type depth() const { size_type n = 0; for(const auto& c : children_) { auto l = 1 + c.depth(); if(l > n) n = l; } return n; } //--------------------------------------------------------------- /** @brief returns mutating iterator to position of first element */ iterator begin() noexcept { return children_.begin(); } /** @brief returns non-mutating iterator to position of first element */ const_iterator begin() const noexcept { return children_.begin(); } /** @brief returns non-mutating iterator to position of first element */ const_iterator cbegin() const noexcept { return children_.begin(); } /** @brief returns mutating iterator to position one past the last element */ iterator end() noexcept { return children_.end(); } /** @brief returns non-mutating iterator to position one past the last element */ const_iterator end() const noexcept { return children_.end(); } /** @brief returns non-mutating iterator to position one past the last element */ const_iterator cend() const noexcept { return children_.end(); } //--------------------------------------------------------------- /** @brief returns augmented iterator for depth first searches * @details traverser knows end of iteration and can skip over children */ depth_first_traverser begin_dfs() const noexcept { return depth_first_traverser{*this}; } //--------------------------------------------------------------- /** @brief returns recursive parameter count */ size_type param_count() const { size_type c = 0; for(const auto& n : children_) { c += n.param_count(); } return c; } //--------------------------------------------------------------- /** @brief returns range of all flags (recursive) */ arg_list all_flags() const { std::vector all; gather_flags(children_, all); return all; } /** @brief returns true, if no flag occurs as true * prefix of any other flag (identical flags will be ignored) */ bool flags_are_prefix_free() const { const auto fs = all_flags(); using std::begin; using std::end; for(auto i = begin(fs), e = end(fs); i != e; ++i) { if(!i->empty()) { for(auto j = i+1; j != e; ++j) { if(!j->empty() && *i != *j) { if(i->find(*j) == 0) return false; if(j->find(*i) == 0) return false; } } } } return true; } //--------------------------------------------------------------- /** @brief returns longest common prefix of all flags */ arg_string common_flag_prefix() const { arg_list prefixes; gather_prefixes(children_, prefixes); return str::longest_common_prefix(prefixes); } private: //--------------------------------------------------------------- static void gather_flags(const children_store& nodes, arg_list& all) { for(const auto& p : nodes) { if(p.is_group()) { gather_flags(p.as_group().children_, all); } else { const auto& pf = p.as_param().flags(); using std::begin; using std::end; if(!pf.empty()) all.insert(end(all), begin(pf), end(pf)); } } } //--------------------------------------------------------------- static void gather_prefixes(const children_store& nodes, arg_list& all) { for(const auto& p : nodes) { if(p.is_group()) { gather_prefixes(p.as_group().children_, all); } else if(!p.as_param().flags().empty()) { auto pfx = str::longest_common_prefix(p.as_param().flags()); if(!pfx.empty()) all.push_back(std::move(pfx)); } } } //--------------------------------------------------------------- children_store children_; bool exclusive_ = false; bool joinable_ = false; bool scoped_ = false; }; /*************************************************************************//** * * @brief group or parameter * *****************************************************************************/ using pattern = group::child; /*************************************************************************//** * * @brief apply an action to all parameters in a group * *****************************************************************************/ template void for_all_params(group& g, Action&& action) { for(auto& p : g) { if(p.is_group()) { for_all_params(p.as_group(), action); } else { action(p.as_param()); } } } template void for_all_params(const group& g, Action&& action) { for(auto& p : g) { if(p.is_group()) { for_all_params(p.as_group(), action); } else { action(p.as_param()); } } } /*************************************************************************//** * * @brief makes a group of parameters and/or groups * *****************************************************************************/ inline group operator , (parameter a, parameter b) { return group{std::move(a), std::move(b)}.scoped(false); } //--------------------------------------------------------- inline group operator , (parameter a, group b) { return !b.scoped() && !b.blocking() && !b.exclusive() && !b.repeatable() && !b.joinable() && (b.doc().empty() || b.doc() == a.doc()) ? b.push_front(std::move(a)) : group{std::move(a), std::move(b)}.scoped(false); } //--------------------------------------------------------- inline group operator , (group a, parameter b) { return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable() && !a.joinable() && (a.doc().empty() || a.doc() == b.doc()) ? a.push_back(std::move(b)) : group{std::move(a), std::move(b)}.scoped(false); } //--------------------------------------------------------- inline group operator , (group a, group b) { return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable() && !a.joinable() && (a.doc().empty() || a.doc() == b.doc()) ? a.push_back(std::move(b)) : group{std::move(a), std::move(b)}.scoped(false); } /*************************************************************************//** * * @brief makes a group of alternative parameters or groups * *****************************************************************************/ template inline group one_of(Param param, Params... params) { return group{std::move(param), std::move(params)...}.exclusive(true); } /*************************************************************************//** * * @brief makes a group of alternative parameters or groups * *****************************************************************************/ inline group operator | (parameter a, parameter b) { return group{std::move(a), std::move(b)}.scoped(false).exclusive(true); } //------------------------------------------------------------------- inline group operator | (parameter a, group b) { return !b.scoped() && !b.blocking() && b.exclusive() && !b.repeatable() && !b.joinable() && (b.doc().empty() || b.doc() == a.doc()) ? b.push_front(std::move(a)) : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); } //------------------------------------------------------------------- inline group operator | (group a, parameter b) { return !a.scoped() && a.exclusive() && !a.repeatable() && !a.joinable() && a.blocking() == b.blocking() && (a.doc().empty() || a.doc() == b.doc()) ? a.push_back(std::move(b)) : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); } inline group operator | (group a, group b) { return !a.scoped() && a.exclusive() &&!a.repeatable() && !a.joinable() && a.blocking() == b.blocking() && (a.doc().empty() || a.doc() == b.doc()) ? a.push_back(std::move(b)) : group{std::move(a), std::move(b)}.scoped(false).exclusive(true); } /*************************************************************************//** * * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!) * no interface guarantees; might be changed or removed in the future * *****************************************************************************/ namespace detail { inline void set_blocking(bool) {} template void set_blocking(bool yes, P& p, Ps&... ps) { p.blocking(yes); set_blocking(yes, ps...); } } // namespace detail /*************************************************************************//** * * @brief makes a parameter/group sequence by making all input objects blocking * *****************************************************************************/ template inline group in_sequence(Param param, Params... params) { detail::set_blocking(true, param, params...); return group{std::move(param), std::move(params)...}.scoped(true); } /*************************************************************************//** * * @brief makes a parameter/group sequence by making all input objects blocking * *****************************************************************************/ inline group operator & (parameter a, parameter b) { a.blocking(true); b.blocking(true); return group{std::move(a), std::move(b)}.scoped(true); } //--------------------------------------------------------- inline group operator & (parameter a, group b) { a.blocking(true); return group{std::move(a), std::move(b)}.scoped(true); } //--------------------------------------------------------- inline group operator & (group a, parameter b) { b.blocking(true); if(a.all_blocking() && !a.exclusive() && !a.repeatable() && !a.joinable() && (a.doc().empty() || a.doc() == b.doc())) { return a.push_back(std::move(b)); } else { if(!a.all_blocking()) a.blocking(true); return group{std::move(a), std::move(b)}.scoped(true); } } inline group operator & (group a, group b) { if(!b.all_blocking()) b.blocking(true); if(a.all_blocking() && !a.exclusive() && !a.repeatable() && !a.joinable() && (a.doc().empty() || a.doc() == b.doc())) { return a.push_back(std::move(b)); } else { if(!a.all_blocking()) a.blocking(true); return group{std::move(a), std::move(b)}.scoped(true); } } /*************************************************************************//** * * @brief makes a group of parameters and/or groups * where all single char flag params ("-a", "b", ...) are joinable * *****************************************************************************/ inline group joinable(group g) { return g.joinable(true); } //------------------------------------------------------------------- template inline group joinable(parameter param, Params... params) { return group{std::move(param), std::move(params)...}.joinable(true); } template inline group joinable(group p1, P2 p2, Ps... ps) { return group{std::move(p1), std::move(p2), std::move(ps)...}.joinable(true); } template inline group joinable(doc_string docstr, Param param, Params... params) { return group{std::move(param), std::move(params)...} .joinable(true).doc(std::move(docstr)); } /*************************************************************************//** * * @brief makes a repeatable copy of a parameter * *****************************************************************************/ inline parameter repeatable(parameter p) { return p.repeatable(true); } /*************************************************************************//** * * @brief makes a repeatable copy of a group * *****************************************************************************/ inline group repeatable(group g) { return g.repeatable(true); } /*************************************************************************//** * * @brief makes a group of parameters and/or groups * that is repeatable as a whole * Note that a repeatable group consisting entirely of non-blocking * children is equivalent to a non-repeatable group of * repeatable children. * *****************************************************************************/ template inline group repeatable(parameter p1, P2 p2, Ps... ps) { return group{std::move(p1), std::move(p2), std::move(ps)...}.repeatable(true); } template inline group repeatable(group p1, P2 p2, Ps... ps) { return group{std::move(p1), std::move(p2), std::move(ps)...}.repeatable(true); } /*************************************************************************//** * * @brief makes a parameter greedy (match with top priority) * *****************************************************************************/ inline parameter greedy(parameter p) { return p.greedy(true); } inline parameter operator ! (parameter p) { return greedy(p); } /*************************************************************************//** * * @brief recursively prepends a prefix to all flags * *****************************************************************************/ inline parameter&& with_prefix(const arg_string& prefix, parameter&& p) { return std::move(with_prefix(prefix, p)); } //------------------------------------------------------------------- inline group& with_prefix(const arg_string& prefix, group& g) { for(auto& p : g) { if(p.is_group()) { with_prefix(prefix, p.as_group()); } else { with_prefix(prefix, p.as_param()); } } return g; } inline group&& with_prefix(const arg_string& prefix, group&& params) { return std::move(with_prefix(prefix, params)); } template inline group with_prefix(arg_string prefix, Param&& param, Params&&... params) { return with_prefix(prefix, group{std::forward(param), std::forward(params)...}); } /*************************************************************************//** * * @brief recursively prepends a prefix to all flags * * @param shortpfx : used for single-letter flags * @param longpfx : used for flags with length > 1 * *****************************************************************************/ inline parameter&& with_prefixes_short_long(const arg_string& shortpfx, const arg_string& longpfx, parameter&& p) { return std::move(with_prefixes_short_long(shortpfx, longpfx, p)); } //------------------------------------------------------------------- inline group& with_prefixes_short_long(const arg_string& shortFlagPrefix, const arg_string& longFlagPrefix, group& g) { for(auto& p : g) { if(p.is_group()) { with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_group()); } else { with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_param()); } } return g; } inline group&& with_prefixes_short_long(const arg_string& shortFlagPrefix, const arg_string& longFlagPrefix, group&& params) { return std::move(with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, params)); } template inline group with_prefixes_short_long(const arg_string& shortFlagPrefix, const arg_string& longFlagPrefix, Param&& param, Params&&... params) { return with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, group{std::forward(param), std::forward(params)...}); } /*************************************************************************//** * * @brief recursively prepends a suffix to all flags * *****************************************************************************/ inline parameter&& with_suffix(const arg_string& suffix, parameter&& p) { return std::move(with_suffix(suffix, p)); } //------------------------------------------------------------------- inline group& with_suffix(const arg_string& suffix, group& g) { for(auto& p : g) { if(p.is_group()) { with_suffix(suffix, p.as_group()); } else { with_suffix(suffix, p.as_param()); } } return g; } inline group&& with_suffix(const arg_string& suffix, group&& params) { return std::move(with_suffix(suffix, params)); } template inline group with_suffix(arg_string suffix, Param&& param, Params&&... params) { return with_suffix(suffix, group{std::forward(param), std::forward(params)...}); } /*************************************************************************//** * * @brief recursively prepends a suffix to all flags * * @param shortsfx : used for single-letter flags * @param longsfx : used for flags with length > 1 * *****************************************************************************/ inline parameter&& with_suffixes_short_long(const arg_string& shortsfx, const arg_string& longsfx, parameter&& p) { return std::move(with_suffixes_short_long(shortsfx, longsfx, p)); } //------------------------------------------------------------------- inline group& with_suffixes_short_long(const arg_string& shortFlagSuffix, const arg_string& longFlagSuffix, group& g) { for(auto& p : g) { if(p.is_group()) { with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, p.as_group()); } else { with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, p.as_param()); } } return g; } inline group&& with_suffixes_short_long(const arg_string& shortFlagSuffix, const arg_string& longFlagSuffix, group&& params) { return std::move(with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, params)); } template inline group with_suffixes_short_long(const arg_string& shortFlagSuffix, const arg_string& longFlagSuffix, Param&& param, Params&&... params) { return with_suffixes_short_long(shortFlagSuffix, longFlagSuffix, group{std::forward(param), std::forward(params)...}); } /*************************************************************************//** * * @brief parsing implementation details * *****************************************************************************/ namespace detail { /*************************************************************************//** * * @brief DFS traverser that keeps track of 'scopes' * scope = all parameters that are either bounded by * two blocking parameters on the same depth level * or the beginning/end of the outermost group * *****************************************************************************/ class scoped_dfs_traverser { public: using dfs_traverser = group::depth_first_traverser; scoped_dfs_traverser() = default; explicit scoped_dfs_traverser(const group& g): pos_{g}, lastMatch_{}, posAfterLastMatch_{}, scopes_{}, ignoreBlocks_{false}, repeatGroupStarted_{false}, repeatGroupContinues_{false} {} const dfs_traverser& base() const noexcept { return pos_; } const dfs_traverser& last_match() const noexcept { return lastMatch_; } const group& parent() const noexcept { return pos_.parent(); } const group* innermost_repeat_group() const noexcept { return pos_.innermost_repeat_group(); } const group* outermost_join_group() const noexcept { return pos_.outermost_join_group(); } const group* innermost_blocking_group() const noexcept { return pos_.innermost_blocking_group(); } const group* innermost_exclusive_group() const noexcept { return pos_.innermost_exclusive_group(); } const pattern* operator ->() const noexcept { return pos_.operator->(); } const pattern& operator *() const noexcept { return *pos_; } const pattern* ptr() const noexcept { return pos_.operator->(); } explicit operator bool() const noexcept { return bool(pos_); } bool joinable() const noexcept { return pos_.joinable(); } arg_string common_flag_prefix() const { return pos_.common_flag_prefix(); } void ignore_blocking(bool yes) { ignoreBlocks_ = yes; } void invalidate() { pos_.invalidate(); } bool matched() const noexcept { return (pos_ == lastMatch_); } bool start_of_repeat_group() const noexcept { return repeatGroupStarted_; } //----------------------------------------------------- scoped_dfs_traverser& next_sibling() { pos_.next_sibling(); return *this; } scoped_dfs_traverser& next_after_siblings() { pos_.next_after_siblings(); return *this; } //----------------------------------------------------- scoped_dfs_traverser& operator ++ () { if(!pos_) return *this; if(pos_.is_last_in_path()) { return_to_outermost_scope(); return *this; } //current pattern can block if it didn't match already if(ignoreBlocks_ || matched()) { ++pos_; } else if(!pos_->is_group()) { //current group can block if we didn't have any match in it const group* g = pos_.outermost_blocking_group_fully_explored(); //no match in 'g' before -> skip to after its siblings if(g && !lastMatch_.is_inside(g)) { pos_.back_to_ancestor(g).next_after_siblings(); if(!pos_) return_to_outermost_scope(); } else if(pos_->blocking()) { if(pos_.parent().exclusive()) { pos_.next_sibling(); } else { //no match => skip siblings of blocking param pos_.next_after_siblings(); } if(!pos_) return_to_outermost_scope(); } else { ++pos_; } } else { ++pos_; } check_if_left_scope(); return *this; } //----------------------------------------------------- void next_after_match(scoped_dfs_traverser match) { if(!match || ignoreBlocks_) return; check_repeat_group_start(match); lastMatch_ = match.base(); // if there is a blocking ancestor -> go back to it if(!match->blocking()) { match.pos_.back_to_ancestor(match.innermost_blocking_group()); } //if match is not in current position & current position is blocking //=> current position has to be advanced by one so that it is //no longer reachable within current scope //(can happen for repeatable, blocking parameters) if(match.base() != pos_ && pos_->blocking()) pos_.next_sibling(); if(match->blocking()) { if(match.pos_.is_alternative()) { //discard other alternatives match.pos_.skip_alternatives(); } if(is_last_in_current_scope(match.pos_)) { //if current param is not repeatable -> back to previous scope if(!match->repeatable() && !match->is_group()) { pos_ = std::move(match.pos_); if(!scopes_.empty()) pos_.undo(scopes_.top()); } else { //stay at match position pos_ = std::move(match.pos_); } } else { //not last in current group //if current param is not repeatable, go directly to next if(!match->repeatable() && !match->is_group()) { ++match.pos_; } if(match.pos_.level() > pos_.level()) { scopes_.push(pos_.undo_point()); pos_ = std::move(match.pos_); } else if(match.pos_.level() < pos_.level()) { return_to_level(match.pos_.level()); } else { pos_ = std::move(match.pos_); } } posAfterLastMatch_ = pos_; } else { if(match.pos_.level() < pos_.level()) { return_to_level(match.pos_.level()); } posAfterLastMatch_ = pos_; } repeatGroupContinues_ = repeat_group_continues(); } private: //----------------------------------------------------- bool is_last_in_current_scope(const dfs_traverser& pos) const { if(scopes_.empty()) return pos.is_last_in_path(); //check if we would leave the current scope on ++ auto p = pos; ++p; return p.level() < scopes_.top().level(); } //----------------------------------------------------- void check_repeat_group_start(const scoped_dfs_traverser& newMatch) { const auto newrg = newMatch.innermost_repeat_group(); if(!newrg) { repeatGroupStarted_ = false; } else if(lastMatch_.innermost_repeat_group() != newrg) { repeatGroupStarted_ = true; } else if(!repeatGroupContinues_ || !newMatch.repeatGroupContinues_) { repeatGroupStarted_ = true; } else { //special case: repeat group is outermost group //=> we can never really 'leave' and 'reenter' it //but if the current scope is the first element, then we are //conceptually at a position 'before' the group repeatGroupStarted_ = scopes_.empty() || ( newrg == pos_.root() && scopes_.top().param() == &(*pos_.root()->begin()) ); } repeatGroupContinues_ = repeatGroupStarted_; } //----------------------------------------------------- bool repeat_group_continues() const { if(!repeatGroupContinues_) return false; const auto curRepGroup = pos_.innermost_repeat_group(); if(!curRepGroup) return false; if(curRepGroup != lastMatch_.innermost_repeat_group()) return false; if(!posAfterLastMatch_) return false; return true; } //----------------------------------------------------- void check_if_left_scope() { if(posAfterLastMatch_) { if(pos_.level() < posAfterLastMatch_.level()) { while(!scopes_.empty() && scopes_.top().level() >= pos_.level()) { pos_.undo(scopes_.top()); scopes_.pop(); } posAfterLastMatch_.invalidate(); } } while(!scopes_.empty() && scopes_.top().level() > pos_.level()) { pos_.undo(scopes_.top()); scopes_.pop(); } repeatGroupContinues_ = repeat_group_continues(); } //----------------------------------------------------- void return_to_outermost_scope() { posAfterLastMatch_.invalidate(); if(scopes_.empty()) { pos_.invalidate(); repeatGroupContinues_ = false; return; } while(!scopes_.empty() && (!pos_ || pos_.level() >= 1)) { pos_.undo(scopes_.top()); scopes_.pop(); } while(!scopes_.empty()) scopes_.pop(); repeatGroupContinues_ = repeat_group_continues(); } //----------------------------------------------------- void return_to_level(int level) { if(pos_.level() <= level) return; while(!scopes_.empty() && pos_.level() > level) { pos_.undo(scopes_.top()); scopes_.pop(); } }; dfs_traverser pos_; dfs_traverser lastMatch_; dfs_traverser posAfterLastMatch_; std::stack scopes_; bool ignoreBlocks_ = false; bool repeatGroupStarted_ = false; bool repeatGroupContinues_ = false; }; /***************************************************************************** * * some parameter property predicates * *****************************************************************************/ struct select_all { bool operator () (const parameter&) const noexcept { return true; } }; struct select_flags { bool operator () (const parameter& p) const noexcept { return !p.flags().empty(); } }; struct select_values { bool operator () (const parameter& p) const noexcept { return p.flags().empty(); } }; /*************************************************************************//** * * @brief result of a matching operation * *****************************************************************************/ class match_t { public: using size_type = arg_string::size_type; match_t() = default; match_t(arg_string s, scoped_dfs_traverser p): str_{std::move(s)}, pos_{std::move(p)} {} size_type length() const noexcept { return str_.size(); } const arg_string& str() const noexcept { return str_; } const scoped_dfs_traverser& pos() const noexcept { return pos_; } explicit operator bool() const noexcept { return bool(pos_); } private: arg_string str_; scoped_dfs_traverser pos_; }; /*************************************************************************//** * * @brief finds the first parameter that matches a given string; * candidate parameters are traversed using a scoped DFS traverser * *****************************************************************************/ template match_t full_match(scoped_dfs_traverser pos, const arg_string& arg, const ParamSelector& select) { while(pos) { if(pos->is_param()) { const auto& param = pos->as_param(); if(select(param)) { const auto match = param.match(arg); if(match && match.length() == arg.size()) { return match_t{arg, std::move(pos)}; } } } ++pos; } return match_t{}; } /*************************************************************************//** * * @brief finds the first parameter that matches any (non-empty) prefix * of a given string; * candidate parameters are traversed using a scoped DFS traverser * *****************************************************************************/ template match_t longest_prefix_match(scoped_dfs_traverser pos, const arg_string& arg, const ParamSelector& select) { match_t longest; while(pos) { if(pos->is_param()) { const auto& param = pos->as_param(); if(select(param)) { auto match = param.match(arg); if(match.prefix()) { if(match.length() == arg.size()) { return match_t{arg, std::move(pos)}; } else if(match.length() > longest.length()) { longest = match_t{arg.substr(match.at(), match.length()), pos}; } } } } ++pos; } return longest; } /*************************************************************************//** * * @brief finds the first parameter that partially matches a given string; * candidate parameters are traversed using a scoped DFS traverser * *****************************************************************************/ template match_t partial_match(scoped_dfs_traverser pos, const arg_string& arg, const ParamSelector& select) { while(pos) { if(pos->is_param()) { const auto& param = pos->as_param(); if(select(param)) { const auto match = param.match(arg); if(match) { return match_t{arg.substr(match.at(), match.length()), std::move(pos)}; } } } ++pos; } return match_t{}; } } //namespace detail /***************************************************************//** * * @brief default command line arguments parser * *******************************************************************/ class parser { public: using dfs_traverser = group::depth_first_traverser; using scoped_dfs_traverser = detail::scoped_dfs_traverser; /*****************************************************//** * @brief arg -> parameter mapping *********************************************************/ class arg_mapping { public: friend class parser; explicit arg_mapping(arg_index idx, arg_string s, const dfs_traverser& match) : index_{idx}, arg_{std::move(s)}, match_{match}, repeat_{0}, startsRepeatGroup_{false}, blocked_{false}, conflict_{false} {} explicit arg_mapping(arg_index idx, arg_string s) : index_{idx}, arg_{std::move(s)}, match_{}, repeat_{0}, startsRepeatGroup_{false}, blocked_{false}, conflict_{false} {} arg_index index() const noexcept { return index_; } const arg_string& arg() const noexcept { return arg_; } const parameter* param() const noexcept { return match_ && match_->is_param() ? &(match_->as_param()) : nullptr; } std::size_t repeat() const noexcept { return repeat_; } bool blocked() const noexcept { return blocked_; } bool conflict() const noexcept { return conflict_; } bool bad_repeat() const noexcept { if(!param()) return false; return repeat_ > 0 && !param()->repeatable() && !match_.innermost_repeat_group(); } bool any_error() const noexcept { return !match_ || blocked() || conflict() || bad_repeat(); } private: arg_index index_; arg_string arg_; dfs_traverser match_; std::size_t repeat_; bool startsRepeatGroup_; bool blocked_; bool conflict_; }; /*****************************************************//** * @brief references a non-matched, required parameter *********************************************************/ class missing_event { public: explicit missing_event(const parameter* p, arg_index after): param_{p}, aftIndex_{after} {} const parameter* param() const noexcept { return param_; } arg_index after_index() const noexcept { return aftIndex_; } private: const parameter* param_; arg_index aftIndex_; }; //----------------------------------------------------- using missing_events = std::vector; using arg_mappings = std::vector; private: struct miss_candidate { miss_candidate(dfs_traverser p, arg_index idx, bool firstInRepeatGroup = false): pos{std::move(p)}, index{idx}, startsRepeatGroup{firstInRepeatGroup} {} dfs_traverser pos; arg_index index; bool startsRepeatGroup; }; using miss_candidates = std::vector; public: //--------------------------------------------------------------- /** @brief initializes parser with a command line interface * @param offset = argument index offset used for reports * */ explicit parser(const group& root, arg_index offset = 0): root_{&root}, pos_{root}, index_{offset-1}, eaten_{0}, args_{}, missCand_{}, blocked_{false} { for_each_potential_miss(dfs_traverser{root}, [this](const dfs_traverser& p){ missCand_.emplace_back(p, index_); }); } //--------------------------------------------------------------- /** @brief processes one command line argument */ bool operator() (const arg_string& arg) { ++eaten_; ++index_; if(!valid()) return false; if(!blocked_ && try_match(arg)) return true; if(try_match_blocked(arg)) return false; //skipping of blocking & required patterns is not allowed if(!blocked_ && !pos_.matched() && pos_->required() && pos_->blocking()) { blocked_ = true; } add_nomatch(arg); return false; } //--------------------------------------------------------------- /** @brief returns range of argument -> parameter mappings */ const arg_mappings& args() const { return args_; } /** @brief returns list of missing events */ missing_events missed() const { missing_events misses; misses.reserve(missCand_.size()); for(auto i = missCand_.begin(); i != missCand_.end(); ++i) { misses.emplace_back(&(i->pos->as_param()), i->index); } return misses; } /** @brief returns number of processed command line arguments */ arg_index parse_count() const noexcept { return eaten_; } /** @brief returns false if previously processed command line arguments * lead to an invalid / inconsistent parsing result */ bool valid() const noexcept { return bool(pos_); } /** @brief returns false if previously processed command line arguments * lead to an invalid / inconsistent parsing result */ explicit operator bool() const noexcept { return valid(); } private: //--------------------------------------------------------------- using match_t = detail::match_t; //--------------------------------------------------------------- /** @brief try to match argument with unreachable parameter */ bool try_match_blocked(const arg_string& arg) { //try to match ahead (using temporary parser) if(pos_) { auto ahead = *this; if(try_match_blocked(std::move(ahead), arg)) return true; } //try to match from the beginning (using temporary parser) if(root_) { parser all{*root_, index_+1}; if(try_match_blocked(std::move(all), arg)) return true; } return false; } //--------------------------------------------------------------- bool try_match_blocked(parser&& parse, const arg_string& arg) { const auto nold = int(parse.args_.size()); parse.pos_.ignore_blocking(true); if(!parse.try_match(arg)) return false; for(auto i = parse.args_.begin() + nold; i != parse.args_.end(); ++i) { args_.push_back(*i); args_.back().blocked_ = true; } return true; } //--------------------------------------------------------------- /** @brief try to find a parameter/pattern that matches 'arg' */ bool try_match(const arg_string& arg) { //match greedy parameters before everything else if(pos_->is_param() && pos_->blocking() && pos_->as_param().greedy()) { const auto match = pos_->as_param().match(arg); if(match && match.length() == arg.size()) { add_match(detail::match_t{arg,pos_}); return true; } } //try flags first (alone, joinable or strict sequence) if(try_match_full(arg, detail::select_flags{})) return true; if(try_match_joined_flags(arg)) return true; if(try_match_joined_sequence(arg, detail::select_flags{})) return true; //try value params (alone or strict sequence) if(try_match_full(arg, detail::select_values{})) return true; if(try_match_joined_sequence(arg, detail::select_all{})) return true; //try joinable params + values in any order if(try_match_joined_params(arg)) return true; return false; } //--------------------------------------------------------------- /** * @brief try to match full argument * @param select : predicate that candidate parameters must satisfy */ template bool try_match_full(const arg_string& arg, const ParamSelector& select) { auto match = detail::full_match(pos_, arg, select); if(!match) return false; add_match(match); return true; } //--------------------------------------------------------------- /** * @brief try to match argument as blocking sequence of parameters * @param select : predicate that a parameter matching the prefix of * 'arg' must satisfy */ template bool try_match_joined_sequence(arg_string arg, const ParamSelector& acceptFirst) { auto fstMatch = detail::longest_prefix_match(pos_, arg, acceptFirst); if(!fstMatch) return false; if(fstMatch.str().size() == arg.size()) { add_match(fstMatch); return true; } if(!fstMatch.pos()->blocking()) return false; auto pos = fstMatch.pos(); pos.ignore_blocking(true); const auto parent = &pos.parent(); if(!pos->repeatable()) ++pos; arg.erase(0, fstMatch.str().size()); std::vector matches { std::move(fstMatch) }; while(!arg.empty() && pos && pos->blocking() && pos->is_param() && (&pos.parent() == parent)) { auto match = pos->as_param().match(arg); if(match.prefix()) { matches.emplace_back(arg.substr(0,match.length()), pos); arg.erase(0, match.length()); if(!pos->repeatable()) ++pos; } else { if(!pos->repeatable()) return false; ++pos; } } //if arg not fully covered => discard temporary matches if(!arg.empty() || matches.empty()) return false; for(const auto& m : matches) add_match(m); return true; } //----------------------------------------------------- /** @brief try to match 'arg' as a concatenation of joinable flags */ bool try_match_joined_flags(const arg_string& arg) { return find_join_group(pos_, [&](const group& g) { return try_match_joined(g, arg, detail::select_flags{}, g.common_flag_prefix()); }); } //--------------------------------------------------------------- /** @brief try to match 'arg' as a concatenation of joinable parameters */ bool try_match_joined_params(const arg_string& arg) { return find_join_group(pos_, [&](const group& g) { return try_match_joined(g, arg, detail::select_all{}); }); } //----------------------------------------------------- /** @brief try to match 'arg' as concatenation of joinable parameters * that are all contained within one group */ template bool try_match_joined(const group& joinGroup, arg_string arg, const ParamSelector& select, const arg_string& prefix = "") { //temporary parser with 'joinGroup' as top-level group parser parse {joinGroup}; //records temporary matches std::vector matches; while(!arg.empty()) { auto match = detail::longest_prefix_match(parse.pos_, arg, select); if(!match) return false; arg.erase(0, match.str().size()); //make sure prefix is always present after the first match //so that, e.g., flags "-a" and "-b" will be found in "-ab" if(!arg.empty() && !prefix.empty() && arg.find(prefix) != 0 && prefix != match.str()) { arg.insert(0,prefix); } parse.add_match(match); matches.push_back(std::move(match)); } if(!arg.empty() || matches.empty()) return false; if(!parse.missCand_.empty()) return false; for(const auto& a : parse.args_) if(a.any_error()) return false; //replay matches onto *this for(const auto& m : matches) add_match(m); return true; } //----------------------------------------------------- template bool find_join_group(const scoped_dfs_traverser& start, const GroupSelector& accept) const { if(start && start.parent().joinable()) { const auto& g = start.parent(); if(accept(g)) return true; return false; } auto pos = start; while(pos) { if(pos->is_group() && pos->as_group().joinable()) { const auto& g = pos->as_group(); if(accept(g)) return true; pos.next_sibling(); } else { ++pos; } } return false; } //--------------------------------------------------------------- void add_nomatch(const arg_string& arg) { args_.emplace_back(index_, arg); } //--------------------------------------------------------------- void add_match(const match_t& match) { const auto& pos = match.pos(); if(!pos || !pos->is_param()) return; pos_.next_after_match(pos); arg_mapping newArg{index_, match.str(), pos.base()}; newArg.repeat_ = occurrences_of(&pos->as_param()); newArg.conflict_ = check_conflicts(pos.base()); newArg.startsRepeatGroup_ = pos_.start_of_repeat_group(); args_.push_back(std::move(newArg)); add_miss_candidates_after(pos); clean_miss_candidates_for(pos.base()); discard_alternative_miss_candidates(pos.base()); } //----------------------------------------------------- bool check_conflicts(const dfs_traverser& match) { if(pos_.start_of_repeat_group()) return false; bool conflict = false; for(const auto& m : match.stack()) { if(m.parent->exclusive()) { for(auto i = args_.rbegin(); i != args_.rend(); ++i) { if(!i->blocked()) { for(const auto& c : i->match_.stack()) { //sibling within same exclusive group => conflict if(c.parent == m.parent && c.cur != m.cur) { conflict = true; i->conflict_ = true; } } } //check for conflicts only within current repeat cycle if(i->startsRepeatGroup_) break; } } } return conflict; } //----------------------------------------------------- void clean_miss_candidates_for(const dfs_traverser& match) { auto i = std::find_if(missCand_.rbegin(), missCand_.rend(), [&](const miss_candidate& m) { return &(*m.pos) == &(*match); }); if(i != missCand_.rend()) { missCand_.erase(prev(i.base())); } } //----------------------------------------------------- void discard_alternative_miss_candidates(const dfs_traverser& match) { if(missCand_.empty()) return; //find out, if miss candidate is sibling of one of the same //alternative groups that the current match is a member of //if so, we can discard the miss //go through all exclusive groups of matching pattern for(const auto& m : match.stack()) { if(m.parent->exclusive()) { for(auto i = int(missCand_.size())-1; i >= 0; --i) { bool removed = false; for(const auto& c : missCand_[i].pos.stack()) { //sibling within same exclusive group => discard if(c.parent == m.parent && c.cur != m.cur) { missCand_.erase(missCand_.begin() + i); if(missCand_.empty()) return; removed = true; break; } } //remove miss candidates only within current repeat cycle if(i > 0 && removed) { if(missCand_[i-1].startsRepeatGroup) break; } else { if(missCand_[i].startsRepeatGroup) break; } } } } } //----------------------------------------------------- void add_miss_candidates_after(const scoped_dfs_traverser& match) { auto npos = match.base(); if(npos.is_alternative()) npos.skip_alternatives(); ++npos; //need to add potential misses if: //either new repeat group was started const auto newRepGroup = match.innermost_repeat_group(); if(newRepGroup) { if(pos_.start_of_repeat_group()) { for_each_potential_miss(std::move(npos), [&,this](const dfs_traverser& pos) { //only add candidates within repeat group if(newRepGroup == pos.innermost_repeat_group()) { missCand_.emplace_back(pos, index_, true); } }); } } //... or an optional blocking param was hit else if(match->blocking() && !match->required() && npos.level() >= match.base().level()) { for_each_potential_miss(std::move(npos), [&,this](const dfs_traverser& pos) { //only add new candidates if(std::find_if(missCand_.begin(), missCand_.end(), [&](const miss_candidate& c){ return &(*c.pos) == &(*pos); }) == missCand_.end()) { missCand_.emplace_back(pos, index_); } }); } } //----------------------------------------------------- template static void for_each_potential_miss(dfs_traverser pos, Action&& action) { const auto level = pos.level(); while(pos && pos.level() >= level) { if(pos->is_group() ) { const auto& g = pos->as_group(); if(g.all_optional() || (g.exclusive() && g.any_optional())) { pos.next_sibling(); } else { ++pos; } } else { //param if(pos->required()) { action(pos); ++pos; } else if(pos->blocking()) { //optional + blocking pos.next_after_siblings(); } else { ++pos; } } } } //--------------------------------------------------------------- std::size_t occurrences_of(const parameter* p) const { if(!p) return 0; auto i = std::find_if(args_.rbegin(), args_.rend(), [p](const arg_mapping& a){ return a.param() == p; }); if(i != args_.rend()) return i->repeat() + 1; return 0; } //--------------------------------------------------------------- const group* root_; scoped_dfs_traverser pos_; arg_index index_; arg_index eaten_; arg_mappings args_; miss_candidates missCand_; bool blocked_; }; /*************************************************************************//** * * @brief contains argument -> parameter mappings * and missing parameters * *****************************************************************************/ class parsing_result { public: using arg_mapping = parser::arg_mapping; using arg_mappings = parser::arg_mappings; using missing_event = parser::missing_event; using missing_events = parser::missing_events; using iterator = arg_mappings::const_iterator; //----------------------------------------------------- /** @brief default: empty result */ parsing_result() = default; parsing_result(arg_mappings arg2param, missing_events misses): arg2param_{std::move(arg2param)}, missing_{std::move(misses)} {} //----------------------------------------------------- /** @brief returns number of arguments that could not be mapped to * a parameter */ arg_mappings::size_type unmapped_args_count() const noexcept { return std::count_if(arg2param_.begin(), arg2param_.end(), [](const arg_mapping& a){ return !a.param(); }); } /** @brief returns if any argument could only be matched by an * unreachable parameter */ bool any_blocked() const noexcept { return std::any_of(arg2param_.begin(), arg2param_.end(), [](const arg_mapping& a){ return a.blocked(); }); } /** @brief returns if any argument matched more than one parameter * that were mutually exclusive */ bool any_conflict() const noexcept { return std::any_of(arg2param_.begin(), arg2param_.end(), [](const arg_mapping& a){ return a.conflict(); }); } /** @brief returns if any parameter matched repeatedly although * it was not allowed to */ bool any_bad_repeat() const noexcept { return std::any_of(arg2param_.begin(), arg2param_.end(), [](const arg_mapping& a){ return a.bad_repeat(); }); } /** @brief returns true if any parsing error / violation of the * command line interface definition occurred */ bool any_error() const noexcept { return unmapped_args_count() > 0 || !missing().empty() || any_blocked() || any_conflict() || any_bad_repeat(); } /** @brief returns true if no parsing error / violation of the * command line interface definition occurred */ explicit operator bool() const noexcept { return !any_error(); } /** @brief access to range of missing parameter match events */ const missing_events& missing() const noexcept { return missing_; } /** @brief returns non-mutating iterator to position of * first argument -> parameter mapping */ iterator begin() const noexcept { return arg2param_.begin(); } /** @brief returns non-mutating iterator to position one past the * last argument -> parameter mapping */ iterator end() const noexcept { return arg2param_.end(); } private: //----------------------------------------------------- arg_mappings arg2param_; missing_events missing_; }; namespace detail { namespace { /*************************************************************************//** * * @brief correct some common problems * does not - and MUST NOT - change the number of arguments * (no insertions or deletions allowed) * *****************************************************************************/ void sanitize_args(arg_list& args) { //e.g. {"-o12", ".34"} -> {"-o", "12.34"} if(args.empty()) return; for(auto i = begin(args)+1; i != end(args); ++i) { if(i != begin(args) && i->size() > 1 && i->find('.') == 0 && std::isdigit((*i)[1]) ) { //find trailing digits in previous arg using std::prev; auto& prv = *prev(i); auto fstDigit = std::find_if_not(prv.rbegin(), prv.rend(), [](arg_string::value_type c){ return std::isdigit(c); }).base(); //handle leading sign if(fstDigit > prv.begin() && (*prev(fstDigit) == '+' || *prev(fstDigit) == '-')) { --fstDigit; } //prepend digits from previous arg i->insert(begin(*i), fstDigit, end(prv)); //erase digits in previous arg prv.erase(fstDigit, end(prv)); } } } /*************************************************************************//** * * @brief executes actions based on a parsing result * *****************************************************************************/ void execute_actions(const parsing_result& res) { for(const auto& m : res) { if(m.param()) { const auto& param = *(m.param()); if(m.repeat() > 0) param.notify_repeated(m.index()); if(m.blocked()) param.notify_blocked(m.index()); if(m.conflict()) param.notify_conflict(m.index()); //main action if(!m.any_error()) param.execute_actions(m.arg()); } } for(auto m : res.missing()) { if(m.param()) m.param()->notify_missing(m.after_index()); } } /*************************************************************************//** * * @brief parses input args * *****************************************************************************/ static parsing_result parse_args(const arg_list& args, const group& cli, arg_index offset = 0) { //parse args and store unrecognized arg indices parser parse{cli, offset}; for(const auto& arg : args) { parse(arg); if(!parse.valid()) break; } return parsing_result{parse.args(), parse.missed()}; } /*************************************************************************//** * * @brief parses input args & executes actions * *****************************************************************************/ static parsing_result parse_and_execute(const arg_list& args, const group& cli, arg_index offset = 0) { auto result = parse_args(args, cli, offset); execute_actions(result); return result; } } //anonymous namespace } // namespace detail /*************************************************************************//** * * @brief parses vector of arg strings and executes actions * *****************************************************************************/ inline parsing_result parse(arg_list args, const group& cli) { detail::sanitize_args(args); return detail::parse_and_execute(args, cli); } /*************************************************************************//** * * @brief parses initializer_list of C-style arg strings and executes actions * *****************************************************************************/ inline parsing_result parse(std::initializer_list arglist, const group& cli) { arg_list args; args.reserve(arglist.size()); for(auto a : arglist) { args.push_back(a); } return parse(std::move(args), cli); } /*************************************************************************//** * * @brief parses range of arg strings and executes actions * *****************************************************************************/ template inline parsing_result parse(InputIterator first, InputIterator last, const group& cli) { return parse(arg_list(first,last), cli); } /*************************************************************************//** * * @brief parses the standard array of command line arguments; omits argv[0] * *****************************************************************************/ inline parsing_result parse(const int argc, char* argv[], const group& cli, arg_index offset = 1) { arg_list args; if(offset < argc) args.assign(argv+offset, argv+argc); detail::sanitize_args(args); return detail::parse_and_execute(args, cli, offset); } /*************************************************************************//** * * @brief filter predicate for parameters and groups; * Can be used to limit documentation generation to parameter subsets. * *****************************************************************************/ class param_filter { public: /** @brief only allow parameters with given prefix */ param_filter& prefix(const arg_string& p) noexcept { prefix_ = p; return *this; } /** @brief only allow parameters with given prefix */ param_filter& prefix(arg_string&& p) noexcept { prefix_ = std::move(p); return *this; } const arg_string& prefix() const noexcept { return prefix_; } /** @brief only allow parameters with given requirement status */ param_filter& required(tri t) noexcept { required_ = t; return *this; } tri required() const noexcept { return required_; } /** @brief only allow parameters with given blocking status */ param_filter& blocking(tri t) noexcept { blocking_ = t; return *this; } tri blocking() const noexcept { return blocking_; } /** @brief only allow parameters with given repeatable status */ param_filter& repeatable(tri t) noexcept { repeatable_ = t; return *this; } tri repeatable() const noexcept { return repeatable_; } /** @brief only allow parameters with given docstring status */ param_filter& has_doc(tri t) noexcept { hasDoc_ = t; return *this; } tri has_doc() const noexcept { return hasDoc_; } /** @brief returns true, if parameter satisfies all filters */ bool operator() (const parameter& p) const noexcept { if(!prefix_.empty()) { if(!std::any_of(p.flags().begin(), p.flags().end(), [&](const arg_string& flag){ return str::has_prefix(flag, prefix_); })) return false; } if(required() != p.required()) return false; if(blocking() != p.blocking()) return false; if(repeatable() != p.repeatable()) return false; if(has_doc() != !p.doc().empty()) return false; return true; } private: arg_string prefix_; tri required_ = tri::either; tri blocking_ = tri::either; tri repeatable_ = tri::either; tri hasDoc_ = tri::yes; }; /*************************************************************************//** * * @brief documentation formatting options * *****************************************************************************/ class doc_formatting { public: using string = doc_string; /** @brief same as 'first_column' */ #if __cplusplus >= 201402L [[deprecated]] #endif doc_formatting& start_column(int col) { return first_column(col); } #if __cplusplus >= 201402L [[deprecated]] #endif int start_column() const noexcept { return first_column(); } /** @brief determines column where documentation printing starts */ doc_formatting& first_column(int col) { //limit to [0,last_column] but push doc_column to the right if necessary if(col < 0) col = 0; else if(col > last_column()) col = last_column(); if(col > doc_column()) doc_column(first_column()); firstCol_ = col; return *this; } int first_column() const noexcept { return firstCol_; } /** @brief determines column where docstrings start */ doc_formatting& doc_column(int col) { //limit to [first_column,last_column] if(col < 0) col = 0; else if(col < first_column()) col = first_column(); else if(col > last_column()) col = last_column(); docCol_ = col; return *this; } int doc_column() const noexcept { return docCol_; } /** @brief determines column that no documentation text must exceed; * (text should be wrapped appropriately after this column) */ doc_formatting& last_column(int col) { //limit to [first_column,oo] but push doc_column to the left if necessary if(col < first_column()) col = first_column(); if(col < doc_column()) doc_column(col); lastCol_ = col; return *this; } int last_column() const noexcept { return lastCol_; } /** @brief determines indent of documentation lines * for children of a documented group */ doc_formatting& indent_size(int indent) { indentSize_ = indent; return *this; } int indent_size() const noexcept { return indentSize_; } /** @brief determines string to be used * if a parameter has no flags and no label */ doc_formatting& empty_label(const string& label) { emptyLabel_ = label; return *this; } const string& empty_label() const noexcept { return emptyLabel_; } /** @brief determines string for separating parameters */ doc_formatting& param_separator(const string& sep) { paramSep_ = sep; return *this; } const string& param_separator() const noexcept { return paramSep_; } /** @brief determines string for separating groups (in usage lines) */ doc_formatting& group_separator(const string& sep) { groupSep_ = sep; return *this; } const string& group_separator() const noexcept { return groupSep_; } /** @brief determines string for separating alternative parameters */ doc_formatting& alternative_param_separator(const string& sep) { altParamSep_ = sep; return *this; } const string& alternative_param_separator() const noexcept { return altParamSep_; } /** @brief determines string for separating alternative groups */ doc_formatting& alternative_group_separator(const string& sep) { altGroupSep_ = sep; return *this; } const string& alternative_group_separator() const noexcept { return altGroupSep_; } /** @brief determines string for separating flags of the same parameter */ doc_formatting& flag_separator(const string& sep) { flagSep_ = sep; return *this; } const string& flag_separator() const noexcept { return flagSep_; } /** @brief determines strings surrounding parameter labels */ doc_formatting& surround_labels(const string& prefix, const string& postfix) { labelPre_ = prefix; labelPst_ = postfix; return *this; } const string& label_prefix() const noexcept { return labelPre_; } const string& label_postfix() const noexcept { return labelPst_; } /** @brief determines strings surrounding optional parameters/groups */ doc_formatting& surround_optional(const string& prefix, const string& postfix) { optionPre_ = prefix; optionPst_ = postfix; return *this; } const string& optional_prefix() const noexcept { return optionPre_; } const string& optional_postfix() const noexcept { return optionPst_; } /** @brief determines strings surrounding repeatable parameters/groups */ doc_formatting& surround_repeat(const string& prefix, const string& postfix) { repeatPre_ = prefix; repeatPst_ = postfix; return *this; } const string& repeat_prefix() const noexcept { return repeatPre_; } const string& repeat_postfix() const noexcept { return repeatPst_; } /** @brief determines strings surrounding exclusive groups */ doc_formatting& surround_alternatives(const string& prefix, const string& postfix) { alternPre_ = prefix; alternPst_ = postfix; return *this; } const string& alternatives_prefix() const noexcept { return alternPre_; } const string& alternatives_postfix() const noexcept { return alternPst_; } /** @brief determines strings surrounding alternative flags */ doc_formatting& surround_alternative_flags(const string& prefix, const string& postfix) { alternFlagPre_ = prefix; alternFlagPst_ = postfix; return *this; } const string& alternative_flags_prefix() const noexcept { return alternFlagPre_; } const string& alternative_flags_postfix() const noexcept { return alternFlagPst_; } /** @brief determines strings surrounding non-exclusive groups */ doc_formatting& surround_group(const string& prefix, const string& postfix) { groupPre_ = prefix; groupPst_ = postfix; return *this; } const string& group_prefix() const noexcept { return groupPre_; } const string& group_postfix() const noexcept { return groupPst_; } /** @brief determines strings surrounding joinable groups */ doc_formatting& surround_joinable(const string& prefix, const string& postfix) { joinablePre_ = prefix; joinablePst_ = postfix; return *this; } const string& joinable_prefix() const noexcept { return joinablePre_; } const string& joinable_postfix() const noexcept { return joinablePst_; } /** @brief determines maximum number of flags per parameter to be printed * in detailed parameter documentation lines */ doc_formatting& max_flags_per_param_in_doc(int max) { maxAltInDocs_ = max > 0 ? max : 0; return *this; } int max_flags_per_param_in_doc() const noexcept { return maxAltInDocs_; } /** @brief determines maximum number of flags per parameter to be printed * in usage lines */ doc_formatting& max_flags_per_param_in_usage(int max) { maxAltInUsage_ = max > 0 ? max : 0; return *this; } int max_flags_per_param_in_usage() const noexcept { return maxAltInUsage_; } /** @brief determines number of empty rows after one single-line * documentation entry */ doc_formatting& line_spacing(int lines) { lineSpc_ = lines > 0 ? lines : 0; return *this; } int line_spacing() const noexcept { return lineSpc_; } /** @brief determines number of empty rows before and after a paragraph; * a paragraph is defined by a documented group or if * a parameter documentation entry used more than one line */ doc_formatting& paragraph_spacing(int lines) { paragraphSpc_ = lines > 0 ? lines : 0; return *this; } int paragraph_spacing() const noexcept { return paragraphSpc_; } /** @brief determines if alternative flags with a common prefix should * be printed in a merged fashion */ doc_formatting& merge_alternative_flags_with_common_prefix(bool yes = true) { mergeAltCommonPfx_ = yes; return *this; } bool merge_alternative_flags_with_common_prefix() const noexcept { return mergeAltCommonPfx_; } /** @brief determines if joinable flags with a common prefix should * be printed in a merged fashion */ doc_formatting& merge_joinable_with_common_prefix(bool yes = true) { mergeJoinableCommonPfx_ = yes; return *this; } bool merge_joinable_with_common_prefix() const noexcept { return mergeJoinableCommonPfx_; } /** @brief determines if children of exclusive groups should be printed * on individual lines if the exceed 'alternatives_min_split_size' */ doc_formatting& split_alternatives(bool yes = true) { splitTopAlt_ = yes; return *this; } bool split_alternatives() const noexcept { return splitTopAlt_; } /** @brief determines how many children exclusive groups can have before * their children are printed on individual usage lines */ doc_formatting& alternatives_min_split_size(int size) { groupSplitSize_ = size > 0 ? size : 0; return *this; } int alternatives_min_split_size() const noexcept { return groupSplitSize_; } /** @brief determines whether to ignore new line characters in docstrings */ doc_formatting& ignore_newline_chars(bool yes = true) { ignoreNewlines_ = yes; return *this; } bool ignore_newline_chars() const noexcept { return ignoreNewlines_; } private: string paramSep_ = string(" "); string groupSep_ = string(" "); string altParamSep_ = string("|"); string altGroupSep_ = string(" | "); string flagSep_ = string(", "); string labelPre_ = string("<"); string labelPst_ = string(">"); string optionPre_ = string("["); string optionPst_ = string("]"); string repeatPre_ = string(""); string repeatPst_ = string("..."); string groupPre_ = string("("); string groupPst_ = string(")"); string alternPre_ = string("("); string alternPst_ = string(")"); string alternFlagPre_ = string(""); string alternFlagPst_ = string(""); string joinablePre_ = string("("); string joinablePst_ = string(")"); string emptyLabel_ = string(""); int firstCol_ = 8; int docCol_ = 20; int lastCol_ = 100; int indentSize_ = 4; int maxAltInUsage_ = 1; int maxAltInDocs_ = 32; int lineSpc_ = 0; int paragraphSpc_ = 1; int groupSplitSize_ = 3; bool splitTopAlt_ = true; bool mergeAltCommonPfx_ = false; bool mergeJoinableCommonPfx_ = true; bool ignoreNewlines_ = false; }; namespace detail { /*************************************************************************//** * * @brief stream decorator * that applies formatting like line wrapping * *****************************************************************************/ template class formatting_ostream { public: using string_type = StringT; using size_type = typename string_type::size_type; using char_type = typename string_type::value_type; formatting_ostream(OStream& os): os_(os), curCol_{0}, firstCol_{0}, lastCol_{100}, hangingIndent_{0}, paragraphSpacing_{0}, paragraphSpacingThreshold_{2}, curBlankLines_{0}, curParagraphLines_{1}, totalNonBlankLines_{0}, ignoreInputNls_{false} {} //--------------------------------------------------------------- const OStream& base() const noexcept { return os_; } OStream& base() noexcept { return os_; } bool good() const { return os_.good(); } //--------------------------------------------------------------- /** @brief determines the leftmost border of the text body */ formatting_ostream& first_column(int c) { firstCol_ = c < 0 ? 0 : c; return *this; } int first_column() const noexcept { return firstCol_; } /** @brief determines the rightmost border of the text body */ formatting_ostream& last_column(int c) { lastCol_ = c < 0 ? 0 : c; return *this; } int last_column() const noexcept { return lastCol_; } int text_width() const noexcept { return lastCol_ - firstCol_; } /** @brief additional indentation for the 2nd, 3rd, ... line of a paragraph (sequence of soft-wrapped lines) */ formatting_ostream& hanging_indent(int amount) { hangingIndent_ = amount; return *this; } int hanging_indent() const noexcept { return hangingIndent_; } /** @brief amount of blank lines between paragraphs */ formatting_ostream& paragraph_spacing(int lines) { paragraphSpacing_ = lines; return *this; } int paragraph_spacing() const noexcept { return paragraphSpacing_; } /** @brief insert paragraph spacing if paragraph is at least 'lines' lines long */ formatting_ostream& min_paragraph_lines_for_spacing(int lines) { paragraphSpacingThreshold_ = lines; return *this; } int min_paragraph_lines_for_spacing() const noexcept { return paragraphSpacingThreshold_; } /** @brief if set to true, newline characters will be ignored */ formatting_ostream& ignore_newline_chars(bool yes) { ignoreInputNls_ = yes; return *this; } bool ignore_newline_chars() const noexcept { return ignoreInputNls_; } //--------------------------------------------------------------- /* @brief insert 'n' spaces */ void write_spaces(int n) { if(n < 1) return; os_ << string_type(size_type(n), ' '); curCol_ += n; } /* @brief go to new line, but continue current paragraph */ void wrap_soft(int times = 1) { if(times < 1) return; if(times > 1) { os_ << string_type(size_type(times), '\n'); } else { os_ << '\n'; } curCol_ = 0; ++curParagraphLines_; } /* @brief go to new line, and start a new paragraph */ void wrap_hard(int times = 1) { if(times < 1) return; if(paragraph_spacing() > 0 && paragraph_lines() >= min_paragraph_lines_for_spacing()) { times = paragraph_spacing() + 1; } if(times > 1) { os_ << string_type(size_type(times), '\n'); curBlankLines_ += times - 1; } else { os_ << '\n'; } if(at_begin_of_line()) { ++curBlankLines_; } curCol_ = 0; curParagraphLines_ = 1; } //--------------------------------------------------------------- bool at_begin_of_line() const noexcept { return curCol_ <= current_line_begin(); } int current_line_begin() const noexcept { return in_hanging_part_of_paragraph() ? firstCol_ + hangingIndent_ : firstCol_; } int current_column() const noexcept { return curCol_; } int total_non_blank_lines() const noexcept { return totalNonBlankLines_; } int paragraph_lines() const noexcept { return curParagraphLines_; } int blank_lines_before_paragraph() const noexcept { return curBlankLines_; } //--------------------------------------------------------------- template friend formatting_ostream& operator << (formatting_ostream& os, const T& x) { os.write(x); return os; } void flush() { os_.flush(); } private: bool in_hanging_part_of_paragraph() const noexcept { return hanging_indent() > 0 && paragraph_lines() > 1; } bool current_line_empty() const noexcept { return curCol_ < 1; } bool left_of_text_area() const noexcept { return curCol_ < current_line_begin(); } bool right_of_text_area() const noexcept { return curCol_ > lastCol_; } int columns_left_in_line() const noexcept { return lastCol_ - std::max(current_line_begin(), curCol_); } void fix_indent() { if(left_of_text_area()) { const auto fst = current_line_begin(); write_spaces(fst - curCol_); curCol_ = fst; } } template bool only_whitespace(Iter first, Iter last) const { return last == std::find_if_not(first, last, [](char_type c) { return std::isspace(c); }); } /** @brief write any object */ template void write(const T& x) { std::ostringstream ss; ss << x; write(std::move(ss).str()); } /** @brief write a stringstream */ void write(const std::ostringstream& s) { write(s.str()); } /** @brief write a string */ void write(const string_type& s) { write(s.begin(), s.end()); } /** @brief partition output into lines */ template void write(Iter first, Iter last) { if(first == last) return; if(*first == '\n') { if(!ignore_newline_chars()) wrap_hard(); ++first; if(first == last) return; } auto i = std::find(first, last, '\n'); if(i != last) { if(ignore_newline_chars()) ++i; if(i != last) { write_line(first, i); write(i, last); } } else { write_line(first, last); } } /** @brief handle line wrapping due to column constraints */ template void write_line(Iter first, Iter last) { if(first == last) return; if(only_whitespace(first, last)) return; if(right_of_text_area()) wrap_soft(); if(at_begin_of_line()) { //discard whitespace, it we start a new line first = std::find_if(first, last, [](char_type c) { return !std::isspace(c); }); if(first == last) return; } const auto n = int(std::distance(first,last)); const auto m = columns_left_in_line(); //if text to be printed is too long for one line -> wrap if(n > m) { //break before word, if break is mid-word auto breakat = first + m; while(breakat > first && !std::isspace(*breakat)) --breakat; //could not find whitespace before word -> try after the word if(!std::isspace(*breakat) && breakat == first) { breakat = std::find_if(first+m, last, [](char_type c) { return std::isspace(c); }); } if(breakat > first) { if(curCol_ < 1) ++totalNonBlankLines_; fix_indent(); std::copy(first, breakat, std::ostream_iterator(os_)); curBlankLines_ = 0; } if(breakat < last) { wrap_soft(); write_line(breakat, last); } } else { if(curCol_ < 1) ++totalNonBlankLines_; fix_indent(); std::copy(first, last, std::ostream_iterator(os_)); curCol_ += n; curBlankLines_ = 0; } } /** @brief write a single character */ void write(char_type c) { if(c == '\n') { if(!ignore_newline_chars()) wrap_hard(); } else { if(at_begin_of_line()) ++totalNonBlankLines_; fix_indent(); os_ << c; ++curCol_; } } OStream& os_; int curCol_; int firstCol_; int lastCol_; int hangingIndent_; int paragraphSpacing_; int paragraphSpacingThreshold_; int curBlankLines_; int curParagraphLines_; int totalNonBlankLines_; bool ignoreInputNls_; }; } /*************************************************************************//** * * @brief generates usage lines * * @details lazily evaluated * *****************************************************************************/ class usage_lines { public: using string = doc_string; usage_lines(const group& cli, string prefix = "", const doc_formatting& fmt = doc_formatting{}) : cli_(cli), fmt_(fmt), prefix_(std::move(prefix)) { if(!prefix_.empty()) prefix_ += ' '; } usage_lines(const group& cli, const doc_formatting& fmt): usage_lines(cli, "", fmt) {} usage_lines& ommit_outermost_group_surrounders(bool yes) { ommitOutermostSurrounders_ = yes; return *this; } bool ommit_outermost_group_surrounders() const { return ommitOutermostSurrounders_; } template inline friend OStream& operator << (OStream& os, const usage_lines& p) { p.write(os); return os; } string str() const { std::ostringstream os; os << *this; return os.str(); } private: using stream_t = detail::formatting_ostream<>; const group& cli_; doc_formatting fmt_; string prefix_; bool ommitOutermostSurrounders_ = false; //----------------------------------------------------- struct context { group::depth_first_traverser pos; std::stack separators; std::stack postfixes; int level = 0; const group* outermost = nullptr; bool linestart = false; bool useOutermost = true; int line = 0; bool is_singleton() const noexcept { return linestart && pos.is_last_in_path(); } bool is_alternative() const noexcept { return pos.parent().exclusive(); } }; /***************************************************************//** * * @brief writes usage text for command line parameters * *******************************************************************/ template void write(OStream& os) const { detail::formatting_ostream fos(os); fos.first_column(fmt_.first_column()); fos.last_column(fmt_.last_column()); auto hindent = int(prefix_.size()); if(fos.first_column() + hindent >= int(0.4 * fos.text_width())) { hindent = fmt_.indent_size(); } fos.hanging_indent(hindent); fos.paragraph_spacing(fmt_.paragraph_spacing()); fos.min_paragraph_lines_for_spacing(2); fos.ignore_newline_chars(fmt_.ignore_newline_chars()); context cur; cur.pos = cli_.begin_dfs(); cur.linestart = true; cur.level = cur.pos.level(); cur.outermost = &cli_; write(fos, cur, prefix_); } /***************************************************************//** * * @brief writes usage text for command line parameters * * @param prefix all that goes in front of current things to print * *******************************************************************/ template void write(OStream& os, context cur, string prefix) const { if(!cur.pos) return; std::ostringstream buf; if(cur.linestart) buf << prefix; const auto initPos = buf.tellp(); cur.level = cur.pos.level(); if(cur.useOutermost) { //we cannot start outside of the outermost group //so we have to treat it separately start_group(buf, cur.pos.parent(), cur); if(!cur.pos) { os << buf.str(); return; } } else { //don't visit siblings of starter node cur.pos.skip_siblings(); } check_end_group(buf, cur); do { if(buf.tellp() > initPos) cur.linestart = false; if(!cur.linestart && !cur.pos.is_first_in_parent()) { buf << cur.separators.top(); } if(cur.pos->is_group()) { start_group(buf, cur.pos->as_group(), cur); if(!cur.pos) { os << buf.str(); return; } } else { buf << param_label(cur.pos->as_param(), cur); ++cur.pos; } check_end_group(buf, cur); } while(cur.pos); os << buf.str(); } /***************************************************************//** * * @brief handles pattern group surrounders and separators * and alternative splitting * *******************************************************************/ void start_group(std::ostringstream& os, const group& group, context& cur) const { //does cur.pos already point to a member or to group itself? //needed for special treatment of outermost group const bool alreadyInside = &(cur.pos.parent()) == &group; auto lbl = joined_label(group, cur); if(!lbl.empty()) { os << lbl; cur.linestart = false; //skip over entire group as its label has already been created if(alreadyInside) { cur.pos.next_after_siblings(); } else { cur.pos.next_sibling(); } } else { const bool splitAlternatives = group.exclusive() && fmt_.split_alternatives() && std::any_of(group.begin(), group.end(), [this](const pattern& p) { return int(p.param_count()) >= fmt_.alternatives_min_split_size(); }); if(splitAlternatives) { cur.postfixes.push(""); cur.separators.push(""); //recursively print alternative paths in decision-DAG //enter group? if(!alreadyInside) ++cur.pos; cur.linestart = true; cur.useOutermost = false; auto pfx = os.str(); os.str(""); //print paths in DAG starting at each group member for(std::size_t i = 0; i < group.size(); ++i) { std::stringstream buf; cur.outermost = cur.pos->is_group() ? &(cur.pos->as_group()) : nullptr; write(buf, cur, pfx); if(buf.tellp() > int(pfx.size())) { os << buf.str(); if(i < group.size()-1) { if(cur.line > 0) { os << string(fmt_.line_spacing(), '\n'); } ++cur.line; os << '\n'; } } cur.pos.next_sibling(); //do not descend into members } cur.pos.invalidate(); //signal end-of-path return; } else { //pre & postfixes, separators auto surround = group_surrounders(group, cur); os << surround.first; cur.postfixes.push(std::move(surround.second)); cur.separators.push(group_separator(group, fmt_)); //descend into group? if(!alreadyInside) ++cur.pos; } } cur.level = cur.pos.level(); } /***************************************************************//** * *******************************************************************/ void check_end_group(std::ostringstream& os, context& cur) const { for(; cur.level > cur.pos.level(); --cur.level) { os << cur.postfixes.top(); cur.postfixes.pop(); cur.separators.pop(); } cur.level = cur.pos.level(); } /***************************************************************//** * * @brief makes usage label for one command line parameter * *******************************************************************/ string param_label(const parameter& p, const context& cur) const { const auto& parent = cur.pos.parent(); const bool startsOptionalSequence = parent.size() > 1 && p.blocking() && cur.pos.is_first_in_parent(); const bool outermost = ommitOutermostSurrounders_ && cur.outermost == &parent; const bool showopt = !cur.is_alternative() && !p.required() && !startsOptionalSequence && !outermost; const bool showrep = p.repeatable() && !outermost; string lbl; if(showrep) lbl += fmt_.repeat_prefix(); if(showopt) lbl += fmt_.optional_prefix(); const auto& flags = p.flags(); if(!flags.empty()) { const int n = std::min(fmt_.max_flags_per_param_in_usage(), int(flags.size())); const bool surrAlt = n > 1 && !showopt && !cur.is_singleton(); if(surrAlt) lbl += fmt_.alternative_flags_prefix(); bool sep = false; for(int i = 0; i < n; ++i) { if(sep) { if(cur.is_singleton()) lbl += fmt_.alternative_group_separator(); else lbl += fmt_.flag_separator(); } lbl += flags[i]; sep = true; } if(surrAlt) lbl += fmt_.alternative_flags_postfix(); } else { if(!p.label().empty()) { lbl += fmt_.label_prefix() + p.label() + fmt_.label_postfix(); } else if(!fmt_.empty_label().empty()) { lbl += fmt_.label_prefix() + fmt_.empty_label() + fmt_.label_postfix(); } else { return ""; } } if(showopt) lbl += fmt_.optional_postfix(); if(showrep) lbl += fmt_.repeat_postfix(); return lbl; } /***************************************************************//** * * @brief prints flags in one group in a merged fashion * *******************************************************************/ string joined_label(const group& g, const context& cur) const { if(!fmt_.merge_alternative_flags_with_common_prefix() && !fmt_.merge_joinable_with_common_prefix()) return ""; const bool flagsonly = std::all_of(g.begin(), g.end(), [](const pattern& p){ return p.is_param() && !p.as_param().flags().empty(); }); if(!flagsonly) return ""; const bool showOpt = g.all_optional() && !(ommitOutermostSurrounders_ && cur.outermost == &g); auto pfx = g.common_flag_prefix(); if(pfx.empty()) return ""; const auto n = pfx.size(); if(g.exclusive() && fmt_.merge_alternative_flags_with_common_prefix()) { string lbl; if(showOpt) lbl += fmt_.optional_prefix(); lbl += pfx + fmt_.alternatives_prefix(); bool first = true; for(const auto& p : g) { if(p.is_param()) { if(first) first = false; else lbl += fmt_.alternative_param_separator(); lbl += p.as_param().flags().front().substr(n); } } lbl += fmt_.alternatives_postfix(); if(showOpt) lbl += fmt_.optional_postfix(); return lbl; } //no alternatives, but joinable flags else if(g.joinable() && fmt_.merge_joinable_with_common_prefix()) { const bool allSingleChar = std::all_of(g.begin(), g.end(), [&](const pattern& p){ return p.is_param() && p.as_param().flags().front().substr(n).size() == 1; }); if(allSingleChar) { string lbl; if(showOpt) lbl += fmt_.optional_prefix(); lbl += pfx; for(const auto& p : g) { if(p.is_param()) lbl += p.as_param().flags().front().substr(n); } if(showOpt) lbl += fmt_.optional_postfix(); return lbl; } } return ""; } /***************************************************************//** * * @return symbols with which to surround a group * *******************************************************************/ std::pair group_surrounders(const group& group, const context& cur) const { string prefix; string postfix; const bool isOutermost = &group == cur.outermost; if(isOutermost && ommitOutermostSurrounders_) return {string{}, string{}}; if(group.exclusive()) { if(group.all_optional()) { prefix = fmt_.optional_prefix(); postfix = fmt_.optional_postfix(); if(group.all_flagless()) { prefix += fmt_.label_prefix(); postfix = fmt_.label_prefix() + postfix; } } else if(group.all_flagless()) { prefix = fmt_.label_prefix(); postfix = fmt_.label_postfix(); } else if(!cur.is_singleton() || !isOutermost) { prefix = fmt_.alternatives_prefix(); postfix = fmt_.alternatives_postfix(); } } else if(group.size() > 1 && group.front().blocking() && !group.front().required()) { prefix = fmt_.optional_prefix(); postfix = fmt_.optional_postfix(); } else if(group.size() > 1 && cur.is_alternative() && &group != cur.outermost) { prefix = fmt_.group_prefix(); postfix = fmt_.group_postfix(); } else if(!group.exclusive() && group.joinable() && !cur.linestart) { prefix = fmt_.joinable_prefix(); postfix = fmt_.joinable_postfix(); } if(group.repeatable()) { if(prefix.empty()) prefix = fmt_.group_prefix(); prefix = fmt_.repeat_prefix() + prefix; if(postfix.empty()) postfix = fmt_.group_postfix(); postfix += fmt_.repeat_postfix(); } return {std::move(prefix), std::move(postfix)}; } /***************************************************************//** * * @return symbol that separates members of a group * *******************************************************************/ static string group_separator(const group& group, const doc_formatting& fmt) { const bool only1ParamPerMember = std::all_of(group.begin(), group.end(), [](const pattern& p) { return p.param_count() < 2; }); if(only1ParamPerMember) { if(group.exclusive()) { return fmt.alternative_param_separator(); } else { return fmt.param_separator(); } } else { //there is at least one large group inside if(group.exclusive()) { return fmt.alternative_group_separator(); } else { return fmt.group_separator(); } } } }; /*************************************************************************//** * * @brief generates parameter and group documentation from docstrings * * @details lazily evaluated * *****************************************************************************/ class documentation { public: using string = doc_string; using filter_function = std::function; documentation(const group& cli, const doc_formatting& fmt = doc_formatting{}, filter_function filter = param_filter{}) : cli_(cli), fmt_{fmt}, usgFmt_{fmt}, filter_{std::move(filter)} { //necessary, because we re-use "usage_lines" to generate //labels for documented groups usgFmt_.max_flags_per_param_in_usage( usgFmt_.max_flags_per_param_in_doc()); } documentation(const group& cli, filter_function filter) : documentation{cli, doc_formatting{}, std::move(filter)} {} documentation(const group& cli, const param_filter& filter) : documentation{cli, doc_formatting{}, [filter](const parameter& p) { return filter(p); }} {} template inline friend OStream& operator << (OStream& os, const documentation& p) { p.write(os); return os; } string str() const { std::ostringstream os; write(os); return os.str(); } private: using dfs_traverser = group::depth_first_traverser; const group& cli_; doc_formatting fmt_; doc_formatting usgFmt_; filter_function filter_; enum class paragraph { param, group }; /***************************************************************//** * * @brief writes documentation to output stream * *******************************************************************/ template void write(OStream& os) const { detail::formatting_ostream fos(os); fos.first_column(fmt_.first_column()); fos.last_column(fmt_.last_column()); fos.hanging_indent(0); fos.paragraph_spacing(0); fos.ignore_newline_chars(fmt_.ignore_newline_chars()); print_doc(fos, cli_); } /***************************************************************//** * * @brief writes full documentation text for command line parameters * *******************************************************************/ template void print_doc(detail::formatting_ostream& os, const group& cli, int indentLvl = 0) const { if(cli.empty()) return; //if group itself doesn't have docstring if(cli.doc().empty()) { for(const auto& p : cli) { print_doc(os, p, indentLvl); } } else { //group itself does have docstring bool anyDocInside = std::any_of(cli.begin(), cli.end(), [](const pattern& p){ return !p.doc().empty(); }); if(anyDocInside) { //group docstring as title, then child entries handle_spacing(os, paragraph::group, indentLvl); os << cli.doc(); for(const auto& p : cli) { print_doc(os, p, indentLvl + 1); } } else { //group label first then group docstring auto lbl = usage_lines(cli, usgFmt_) .ommit_outermost_group_surrounders(true).str(); str::trim(lbl); handle_spacing(os, paragraph::param, indentLvl); print_entry(os, lbl, cli.doc()); } } } /***************************************************************//** * * @brief writes documentation text for one group or parameter * *******************************************************************/ template void print_doc(detail::formatting_ostream& os, const pattern& ptrn, int indentLvl) const { if(ptrn.is_group()) { print_doc(os, ptrn.as_group(), indentLvl); } else { const auto& p = ptrn.as_param(); if(!filter_(p)) return; handle_spacing(os, paragraph::param, indentLvl); print_entry(os, param_label(p, fmt_), p.doc()); } } /***************************************************************//** * * @brief handles line and paragraph spacings * *******************************************************************/ template void handle_spacing(detail::formatting_ostream& os, paragraph p, int indentLvl) const { const auto oldIndent = os.first_column(); const auto indent = fmt_.first_column() + indentLvl * fmt_.indent_size(); if(os.total_non_blank_lines() < 1) { os.first_column(indent); return; } if(os.paragraph_lines() > 1 || indent < oldIndent) { os.wrap_hard(fmt_.paragraph_spacing() + 1); } else { os.wrap_hard(); } if(p == paragraph::group) { if(os.blank_lines_before_paragraph() < fmt_.paragraph_spacing()) { os.wrap_hard(fmt_.paragraph_spacing() - os.blank_lines_before_paragraph()); } } else if(os.blank_lines_before_paragraph() < fmt_.line_spacing()) { os.wrap_hard(fmt_.line_spacing() - os.blank_lines_before_paragraph()); } os.first_column(indent); } /*********************************************************************//** * * @brief prints one entry = label + docstring * ************************************************************************/ template void print_entry(detail::formatting_ostream& os, const string& label, const string& docstr) const { if(label.empty()) return; os << label; if(!docstr.empty()) { if(os.current_column() >= fmt_.doc_column()) os.wrap_soft(); const auto oldcol = os.first_column(); os.first_column(fmt_.doc_column()); os << docstr; os.first_column(oldcol); } } /*********************************************************************//** * * @brief makes label for one parameter * ************************************************************************/ static doc_string param_label(const parameter& param, const doc_formatting& fmt) { doc_string lbl; if(param.repeatable()) lbl += fmt.repeat_prefix(); const auto& flags = param.flags(); if(!flags.empty()) { lbl += flags[0]; const int n = std::min(fmt.max_flags_per_param_in_doc(), int(flags.size())); for(int i = 1; i < n; ++i) { lbl += fmt.flag_separator() + flags[i]; } } else if(!param.label().empty() || !fmt.empty_label().empty()) { lbl += fmt.label_prefix(); if(!param.label().empty()) { lbl += param.label(); } else { lbl += fmt.empty_label(); } lbl += fmt.label_postfix(); } if(param.repeatable()) lbl += fmt.repeat_postfix(); return lbl; } }; /*************************************************************************//** * * @brief stores strings for man page sections * *****************************************************************************/ class man_page { public: //--------------------------------------------------------------- using string = doc_string; //--------------------------------------------------------------- /** @brief man page section */ class section { public: using string = doc_string; section(string stitle, string scontent): title_{std::move(stitle)}, content_{std::move(scontent)} {} const string& title() const noexcept { return title_; } const string& content() const noexcept { return content_; } private: string title_; string content_; }; private: using section_store = std::vector
; public: //--------------------------------------------------------------- using value_type = section; using const_iterator = section_store::const_iterator; using size_type = section_store::size_type; //--------------------------------------------------------------- man_page& append_section(string title, string content) { sections_.emplace_back(std::move(title), std::move(content)); return *this; } //----------------------------------------------------- man_page& prepend_section(string title, string content) { sections_.emplace(sections_.begin(), std::move(title), std::move(content)); return *this; } //--------------------------------------------------------------- const section& operator [] (size_type index) const noexcept { return sections_[index]; } //--------------------------------------------------------------- size_type size() const noexcept { return sections_.size(); } bool empty() const noexcept { return sections_.empty(); } //--------------------------------------------------------------- const_iterator begin() const noexcept { return sections_.begin(); } const_iterator end() const noexcept { return sections_.end(); } //--------------------------------------------------------------- man_page& program_name(const string& n) { progName_ = n; return *this; } man_page& program_name(string&& n) { progName_ = std::move(n); return *this; } const string& program_name() const noexcept { return progName_; } //--------------------------------------------------------------- man_page& section_row_spacing(int rows) { sectionSpc_ = rows > 0 ? rows : 0; return *this; } int section_row_spacing() const noexcept { return sectionSpc_; } private: int sectionSpc_ = 1; section_store sections_; string progName_; }; /*************************************************************************//** * * @brief generates man sections from command line parameters * with sections "synopsis" and "options" * *****************************************************************************/ inline man_page make_man_page(const group& cli, doc_string progname = "", const doc_formatting& fmt = doc_formatting{}) { man_page man; man.append_section("SYNOPSIS", usage_lines(cli,progname,fmt).str()); man.append_section("OPTIONS", documentation(cli,fmt).str()); return man; } /*************************************************************************//** * * @brief generates man page based on command line parameters * *****************************************************************************/ template OStream& operator << (OStream& os, const man_page& man) { bool first = true; const auto secSpc = doc_string(man.section_row_spacing() + 1, '\n'); for(const auto& section : man) { if(!section.content().empty()) { if(first) first = false; else os << secSpc; if(!section.title().empty()) os << section.title() << '\n'; os << section.content(); } } os << '\n'; return os; } /*************************************************************************//** * * @brief printing methods for debugging command line interfaces * *****************************************************************************/ namespace debug { /*************************************************************************//** * * @brief prints first flag or value label of a parameter * *****************************************************************************/ inline doc_string doc_label(const parameter& p) { if(!p.flags().empty()) return p.flags().front(); if(!p.label().empty()) return p.label(); return doc_string{""}; } inline doc_string doc_label(const group&) { return ""; } inline doc_string doc_label(const pattern& p) { return p.is_group() ? doc_label(p.as_group()) : doc_label(p.as_param()); } /*************************************************************************//** * * @brief prints parsing result * *****************************************************************************/ template void print(OStream& os, const parsing_result& result) { for(const auto& m : result) { os << "#" << m.index() << " " << m.arg() << " -> "; auto p = m.param(); if(p) { os << doc_label(*p) << " \t"; if(m.repeat() > 0) { os << (m.bad_repeat() ? "[bad repeat " : "[repeat ") << m.repeat() << "]"; } if(m.blocked()) os << " [blocked]"; if(m.conflict()) os << " [conflict]"; os << '\n'; } else { os << " [unmapped]\n"; } } for(const auto& m : result.missing()) { auto p = m.param(); if(p) { os << doc_label(*p) << " \t"; os << " [missing after " << m.after_index() << "]\n"; } } } /*************************************************************************//** * * @brief prints parameter label and some properties * *****************************************************************************/ template void print(OStream& os, const parameter& p) { if(p.greedy()) os << '!'; if(p.blocking()) os << '~'; if(!p.required()) os << '['; os << doc_label(p); if(p.repeatable()) os << "..."; if(!p.required()) os << "]"; } //------------------------------------------------------------------- template void print(OStream& os, const group& g, int level = 0); /*************************************************************************//** * * @brief prints group or parameter; uses indentation * *****************************************************************************/ template void print(OStream& os, const pattern& param, int level = 0) { if(param.is_group()) { print(os, param.as_group(), level); } else { os << doc_string(4*level, ' '); print(os, param.as_param()); } } /*************************************************************************//** * * @brief prints group and its contents; uses indentation * *****************************************************************************/ template void print(OStream& os, const group& g, int level) { auto indent = doc_string(4*level, ' '); os << indent; if(g.blocking()) os << '~'; if(g.joinable()) os << 'J'; os << (g.exclusive() ? "(|\n" : "(\n"); for(const auto& p : g) { print(os, p, level+1); } os << '\n' << indent << (g.exclusive() ? "|)" : ")"); if(g.repeatable()) os << "..."; os << '\n'; } } // namespace debug } //namespace clipp #endif