diff options
| author | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2024-04-01 16:11:30 +0300 |
|---|---|---|
| committer | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2024-04-01 16:11:30 +0300 |
| commit | 8e981e3f469e5d903f0212a95a55cbe559e60382 (patch) | |
| tree | 13e3f5bce196a46db83a2d9b0530c8c6175ca318 /thirdparty | |
| parent | f31684bfa68c80ea03040c704667a2465283743c (diff) | |
thirdparty: use CLI11 v2.4.1
Diffstat (limited to 'thirdparty')
| -rw-r--r-- | thirdparty/CLI11.hpp | 1942 |
1 files changed, 1611 insertions, 331 deletions
diff --git a/thirdparty/CLI11.hpp b/thirdparty/CLI11.hpp index 3913fa9..41027f0 100644 --- a/thirdparty/CLI11.hpp +++ b/thirdparty/CLI11.hpp @@ -1,11 +1,11 @@ -// CLI11: Version 2.3.2 +// CLI11: Version 2.4.1 // Originally designed by Henry Schreiner // https://github.com/CLIUtils/CLI11 // // This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts -// from: v2.3.2 +// from: v2.4.1 // -// CLI11 2.3.2 Copyright (c) 2017-2022 University of Cincinnati, developed by Henry +// CLI11 2.4.1 Copyright (c) 2017-2024 University of Cincinnati, developed by Henry // Schreiner under NSF AWARD 1414736. All rights reserved. // // Redistribution and use in source and binary forms of CLI11, with or without @@ -34,34 +34,40 @@ #pragma once // Standard combined includes: -#include <iomanip> -#include <set> -#include <memory> -#include <vector> -#include <utility> -#include <stdexcept> -#include <locale> +#include <algorithm> +#include <array> +#include <cctype> +#include <clocale> +#include <cmath> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <cwchar> +#include <exception> +#include <fstream> #include <functional> +#include <iomanip> #include <iostream> #include <iterator> -#include <exception> +#include <limits> +#include <locale> +#include <map> +#include <memory> #include <numeric> -#include <fstream> +#include <set> +#include <sstream> +#include <stdexcept> #include <string> -#include <type_traits> #include <tuple> -#include <map> -#include <algorithm> -#include <cstdint> -#include <sstream> -#include <cmath> -#include <limits> +#include <type_traits> +#include <utility> +#include <vector> #define CLI11_VERSION_MAJOR 2 -#define CLI11_VERSION_MINOR 3 -#define CLI11_VERSION_PATCH 2 -#define CLI11_VERSION "2.3.2" +#define CLI11_VERSION_MINOR 4 +#define CLI11_VERSION_PATCH 1 +#define CLI11_VERSION "2.4.1" @@ -124,17 +130,7 @@ #endif #endif -/** Inline macro **/ -#ifdef CLI11_COMPILE -#define CLI11_INLINE -#else -#define CLI11_INLINE inline -#endif - - - -// C standard library -// Only needed for existence checking +/** <filesystem> availability */ #if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM #if __has_include(<filesystem>) // Filesystem cannot be used if targeting macOS < 10.15 @@ -161,6 +157,44 @@ #endif #endif +/** <codecvt> availability */ +#if defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 5 +#define CLI11_HAS_CODECVT 0 +#else +#define CLI11_HAS_CODECVT 1 +#include <codecvt> +#endif + +/** disable deprecations */ +#if defined(__GNUC__) // GCC or clang +#define CLI11_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") +#define CLI11_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + +#elif defined(_MSC_VER) +#define CLI11_DIAGNOSTIC_PUSH __pragma(warning(push)) +#define CLI11_DIAGNOSTIC_POP __pragma(warning(pop)) + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED __pragma(warning(disable : 4996)) + +#else +#define CLI11_DIAGNOSTIC_PUSH +#define CLI11_DIAGNOSTIC_POP + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +#endif + +/** Inline macro **/ +#ifdef CLI11_COMPILE +#define CLI11_INLINE +#else +#define CLI11_INLINE inline +#endif + + + #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 #include <filesystem> // NOLINT(build/include) #else @@ -170,9 +204,244 @@ + +#ifdef CLI11_CPP17 +#include <string_view> +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include <filesystem> +#include <string_view> // NOLINT(build/include) +#endif // CLI11_HAS_FILESYSTEM + + + +#if defined(_WIN32) +#if !(defined(_AMD64_) || defined(_X86_) || defined(_ARM_)) +#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || \ + defined(_M_AMD64) +#define _AMD64_ +#elif defined(i386) || defined(__i386) || defined(__i386__) || defined(__i386__) || defined(_M_IX86) +#define _X86_ +#elif defined(__arm__) || defined(_M_ARM) || defined(_M_ARMT) +#define _ARM_ +#elif defined(__aarch64__) || defined(_M_ARM64) +#define _ARM64_ +#elif defined(_M_ARM64EC) +#define _ARM64EC_ +#endif +#endif + +// first +#ifndef NOMINMAX +// if NOMINMAX is already defined we don't want to mess with that either way +#define NOMINMAX +#include <windef.h> +#undef NOMINMAX +#else +#include <windef.h> +#endif + +// second +#include <winbase.h> +// third +#include <processthreadsapi.h> +#include <shellapi.h> +#endif + + namespace CLI { +/// Convert a wide string to a narrow string. +CLI11_INLINE std::string narrow(const std::wstring &str); +CLI11_INLINE std::string narrow(const wchar_t *str); +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t size); + +/// Convert a narrow string to a wide string. +CLI11_INLINE std::wstring widen(const std::string &str); +CLI11_INLINE std::wstring widen(const char *str); +CLI11_INLINE std::wstring widen(const char *str, std::size_t size); + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str); +CLI11_INLINE std::wstring widen(std::string_view str); +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +/// Convert a char-string to a native path correctly. +CLI11_INLINE std::filesystem::path to_path(std::string_view str); +#endif // CLI11_HAS_FILESYSTEM + + + + +namespace detail { + +#if !CLI11_HAS_CODECVT +/// Attempt to set one of the acceptable unicode locales for conversion +CLI11_INLINE void set_unicode_locale() { + static const std::array<const char *, 3> unicode_locales{{"C.UTF-8", "en_US.UTF-8", ".UTF-8"}}; + + for(const auto &locale_name : unicode_locales) { + if(std::setlocale(LC_ALL, locale_name) != nullptr) { + return; + } + } + throw std::runtime_error("CLI::narrow: could not set locale to C.UTF-8"); +} + +template <typename F> struct scope_guard_t { + F closure; + + explicit scope_guard_t(F closure_) : closure(closure_) {} + ~scope_guard_t() { closure(); } +}; + +template <typename F> CLI11_NODISCARD CLI11_INLINE scope_guard_t<F> scope_guard(F &&closure) { + return scope_guard_t<F>{std::forward<F>(closure)}; +} + +#endif // !CLI11_HAS_CODECVT + +CLI11_DIAGNOSTIC_PUSH +CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +CLI11_INLINE std::string narrow_impl(const wchar_t *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>>().to_bytes(str, str + str_size); + +#else + return std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const wchar_t *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::wcsrtombs(nullptr, &it, 0, &state); + if(new_size == static_cast<std::size_t>(-1)) { + throw std::runtime_error("CLI::narrow: conversion error in std::wcsrtombs at offset " + + std::to_string(it - str)); + } + std::string result(new_size, '\0'); + std::wcsrtombs(const_cast<char *>(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_INLINE std::wstring widen_impl(const char *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>>().from_bytes(str, str + str_size); + +#else + return std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const char *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::mbsrtowcs(nullptr, &it, 0, &state); + if(new_size == static_cast<std::size_t>(-1)) { + throw std::runtime_error("CLI::widen: conversion error in std::mbsrtowcs at offset " + + std::to_string(it - str)); + } + std::wstring result(new_size, L'\0'); + std::mbsrtowcs(const_cast<wchar_t *>(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_DIAGNOSTIC_POP + +} // namespace detail + +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t str_size) { return detail::narrow_impl(str, str_size); } +CLI11_INLINE std::string narrow(const std::wstring &str) { return detail::narrow_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::string narrow(const wchar_t *str) { return detail::narrow_impl(str, std::wcslen(str)); } + +CLI11_INLINE std::wstring widen(const char *str, std::size_t str_size) { return detail::widen_impl(str, str_size); } +CLI11_INLINE std::wstring widen(const std::string &str) { return detail::widen_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::wstring widen(const char *str) { return detail::widen_impl(str, std::strlen(str)); } + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str) { return detail::narrow_impl(str.data(), str.size()); } +CLI11_INLINE std::wstring widen(std::string_view str) { return detail::widen_impl(str.data(), str.size()); } +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +CLI11_INLINE std::filesystem::path to_path(std::string_view str) { + return std::filesystem::path{ +#ifdef _WIN32 + widen(str) +#else + str +#endif // _WIN32 + }; +} +#endif // CLI11_HAS_FILESYSTEM + + + + +namespace detail { +#ifdef _WIN32 +/// Decode and return UTF-8 argv from GetCommandLineW. +CLI11_INLINE std::vector<std::string> compute_win32_argv(); +#endif +} // namespace detail + + + +namespace detail { + +#ifdef _WIN32 +CLI11_INLINE std::vector<std::string> compute_win32_argv() { + std::vector<std::string> result; + int argc = 0; + + auto deleter = [](wchar_t **ptr) { LocalFree(ptr); }; + // NOLINTBEGIN(*-avoid-c-arrays) + auto wargv = std::unique_ptr<wchar_t *[], decltype(deleter)>(CommandLineToArgvW(GetCommandLineW(), &argc), deleter); + // NOLINTEND(*-avoid-c-arrays) + + if(wargv == nullptr) { + throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError())); + } + + result.reserve(static_cast<size_t>(argc)); + for(size_t i = 0; i < static_cast<size_t>(argc); ++i) { + result.push_back(narrow(wargv[i])); + } + + return result; +} +#endif + +} // namespace detail + + + + /// Include the items in this namespace to get free conversion of enums to/from streams. /// (This is available inside CLI as well, so CLI11 will use this without a using statement). namespace enums { @@ -270,6 +539,9 @@ inline std::string trim_copy(const std::string &str) { /// remove quotes at the front and back of a string either '"' or '\'' CLI11_INLINE std::string &remove_quotes(std::string &str); +/// remove quotes from all elements of a string vector and process escaped components +CLI11_INLINE void remove_quotes(std::vector<std::string> &args); + /// Add a leader to the beginning of all new lines (nothing is added /// at the start of the first line). `"; "` would be for ini files /// @@ -290,14 +562,16 @@ CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector<s /// Verify the first character of an option /// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with -template <typename T> bool valid_first_char(T c) { return ((c != '-') && (c != '!') && (c != ' ') && c != '\n'); } +template <typename T> bool valid_first_char(T c) { + return ((c != '-') && (static_cast<unsigned char>(c) > 33)); // space and '!' not allowed +} /// Verify following characters of an option template <typename T> bool valid_later_char(T c) { // = and : are value separators, { has special meaning for option defaults, - // and \n would just be annoying to deal with in many places allowing space here has too much potential for - // inadvertent entry errors and bugs - return ((c != '=') && (c != ':') && (c != '{') && (c != ' ') && c != '\n'); + // and control codes other than tab would just be annoying to deal with in many places allowing space here has too + // much potential for inadvertent entry errors and bugs + return ((c != '=') && (c != ':') && (c != '{') && ((static_cast<unsigned char>(c) > 32) || c == '\t')); } /// Verify an option/subcommand name @@ -360,18 +634,46 @@ template <typename Callable> inline std::string find_and_modify(std::string str, return str; } +/// close a sequence of characters indicated by a closure character. Brackets allows sub sequences +/// recognized bracket sequences include "'`[(<{ other closure characters are assumed to be literal strings +CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char); + /// Split a string '"one two" "three"' into 'one two', 'three' -/// Quote characters can be ` ' or " +/// Quote characters can be ` ' or " or bracket characters [{(< with matching to the matching bracket CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter = '\0'); +/// get the value of an environmental variable or empty string if empty +CLI11_INLINE std::string get_environment_value(const std::string &env_name); + /// This function detects an equal or colon followed by an escaped quote after an argument /// then modifies the string to replace the equality with a space. This is needed /// to allow the split up function to work properly and is intended to be used with the find_and_modify function /// the return value is the offset+1 which is required by the find_and_modify function. CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset); -/// Add quotes if the string contains spaces -CLI11_INLINE std::string &add_quotes_if_needed(std::string &str); +/// @brief detect if a string has escapable characters +/// @param str the string to do the detection on +/// @return true if the string has escapable characters +CLI11_INLINE bool has_escapable_character(const std::string &str); + +/// @brief escape all escapable characters +/// @param str the string to escape +/// @return a string with the escapble characters escaped with '\' +CLI11_INLINE std::string add_escaped_characters(const std::string &str); + +/// @brief replace the escaped characters with their equivalent +CLI11_INLINE std::string remove_escaped_characters(const std::string &str); + +/// generate a string with all non printable characters escaped to hex codes +CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape); + +CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string); + +/// extract an escaped binary_string +CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string); + +/// process a quoted string, remove the quotes and if appropriate handle escaped characters +CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\''); } // namespace detail @@ -421,7 +723,17 @@ CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) { } CLI11_INLINE std::string &remove_quotes(std::string &str) { - if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) { + if(str.length() > 1 && (str.front() == '"' || str.front() == '\'' || str.front() == '`')) { + if(str.front() == str.back()) { + str.pop_back(); + str.erase(str.begin(), str.begin() + 1); + } + } + return str; +} + +CLI11_INLINE std::string &remove_outer(std::string &str, char key) { + if(str.length() > 1 && (str.front() == key)) { if(str.front() == str.back()) { str.pop_back(); str.erase(str.begin(), str.begin() + 1); @@ -541,37 +853,220 @@ find_member(std::string name, const std::vector<std::string> names, bool ignore_ return (it != std::end(names)) ? (it - std::begin(names)) : (-1); } +static const std::string escapedChars("\b\t\n\f\r\"\\"); +static const std::string escapedCharsCode("btnfr\"\\"); +static const std::string bracketChars{"\"'`[(<{"}; +static const std::string matchBracketChars("\"'`])>}"); + +CLI11_INLINE bool has_escapable_character(const std::string &str) { + return (str.find_first_of(escapedChars) != std::string::npos); +} + +CLI11_INLINE std::string add_escaped_characters(const std::string &str) { + std::string out; + out.reserve(str.size() + 4); + for(char s : str) { + auto sloc = escapedChars.find_first_of(s); + if(sloc != std::string::npos) { + out.push_back('\\'); + out.push_back(escapedCharsCode[sloc]); + } else { + out.push_back(s); + } + } + return out; +} + +CLI11_INLINE std::uint32_t hexConvert(char hc) { + int hcode{0}; + if(hc >= '0' && hc <= '9') { + hcode = (hc - '0'); + } else if(hc >= 'A' && hc <= 'F') { + hcode = (hc - 'A' + 10); + } else if(hc >= 'a' && hc <= 'f') { + hcode = (hc - 'a' + 10); + } else { + hcode = -1; + } + return static_cast<uint32_t>(hcode); +} + +CLI11_INLINE char make_char(std::uint32_t code) { return static_cast<char>(static_cast<unsigned char>(code)); } + +CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) { + if(code < 0x80) { // ascii code equivalent + str.push_back(static_cast<char>(code)); + } else if(code < 0x800) { // \u0080 to \u07FF + // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111 + str.push_back(make_char(0xC0 | code >> 6)); + str.push_back(make_char(0x80 | (code & 0x3F))); + } else if(code < 0x10000) { // U+0800...U+FFFF + if(0xD800 <= code && code <= 0xDFFF) { + throw std::invalid_argument("[0xD800, 0xDFFF] are not valid UTF-8."); + } + // 1110yyyy 10yxxxxx 10xxxxxx + str.push_back(make_char(0xE0 | code >> 12)); + str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); + str.push_back(make_char(0x80 | (code & 0x3F))); + } else if(code < 0x110000) { // U+010000 ... U+10FFFF + // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx + str.push_back(make_char(0xF0 | code >> 18)); + str.push_back(make_char(0x80 | (code >> 12 & 0x3F))); + str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); + str.push_back(make_char(0x80 | (code & 0x3F))); + } +} + +CLI11_INLINE std::string remove_escaped_characters(const std::string &str) { + + std::string out; + out.reserve(str.size()); + for(auto loc = str.begin(); loc < str.end(); ++loc) { + if(*loc == '\\') { + if(str.end() - loc < 2) { + throw std::invalid_argument("invalid escape sequence " + str); + } + auto ecloc = escapedCharsCode.find_first_of(*(loc + 1)); + if(ecloc != std::string::npos) { + out.push_back(escapedChars[ecloc]); + ++loc; + } else if(*(loc + 1) == 'u') { + // must have 4 hex characters + if(str.end() - loc < 6) { + throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); + } + std::uint32_t code{0}; + std::uint32_t mplier{16 * 16 * 16}; + for(int ii = 2; ii < 6; ++ii) { + std::uint32_t res = hexConvert(*(loc + ii)); + if(res > 0x0F) { + throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); + } + code += res * mplier; + mplier = mplier / 16; + } + append_codepoint(out, code); + loc += 5; + } else if(*(loc + 1) == 'U') { + // must have 8 hex characters + if(str.end() - loc < 10) { + throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); + } + std::uint32_t code{0}; + std::uint32_t mplier{16 * 16 * 16 * 16 * 16 * 16 * 16}; + for(int ii = 2; ii < 10; ++ii) { + std::uint32_t res = hexConvert(*(loc + ii)); + if(res > 0x0F) { + throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); + } + code += res * mplier; + mplier = mplier / 16; + } + append_codepoint(out, code); + loc += 9; + } else if(*(loc + 1) == '0') { + out.push_back('\0'); + ++loc; + } else { + throw std::invalid_argument(std::string("unrecognized escape sequence \\") + *(loc + 1) + " in " + str); + } + } else { + out.push_back(*loc); + } + } + return out; +} + +CLI11_INLINE std::size_t close_string_quote(const std::string &str, std::size_t start, char closure_char) { + std::size_t loc{0}; + for(loc = start + 1; loc < str.size(); ++loc) { + if(str[loc] == closure_char) { + break; + } + if(str[loc] == '\\') { + // skip the next character for escaped sequences + ++loc; + } + } + return loc; +} + +CLI11_INLINE std::size_t close_literal_quote(const std::string &str, std::size_t start, char closure_char) { + auto loc = str.find_first_of(closure_char, start + 1); + return (loc != std::string::npos ? loc : str.size()); +} + +CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char) { + + auto bracket_loc = matchBracketChars.find(closure_char); + switch(bracket_loc) { + case 0: + return close_string_quote(str, start, closure_char); + case 1: + case 2: + case std::string::npos: + return close_literal_quote(str, start, closure_char); + default: + break; + } + + std::string closures(1, closure_char); + auto loc = start + 1; + + while(loc < str.size()) { + if(str[loc] == closures.back()) { + closures.pop_back(); + if(closures.empty()) { + return loc; + } + } + bracket_loc = bracketChars.find(str[loc]); + if(bracket_loc != std::string::npos) { + switch(bracket_loc) { + case 0: + loc = close_string_quote(str, loc, str[loc]); + break; + case 1: + case 2: + loc = close_literal_quote(str, loc, str[loc]); + break; + default: + closures.push_back(matchBracketChars[bracket_loc]); + break; + } + } + ++loc; + } + if(loc > str.size()) { + loc = str.size(); + } + return loc; +} + CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter) { - const std::string delims("\'\"`"); auto find_ws = [delimiter](char ch) { return (delimiter == '\0') ? std::isspace<char>(ch, std::locale()) : (ch == delimiter); }; trim(str); std::vector<std::string> output; - bool embeddedQuote = false; - char keyChar = ' '; while(!str.empty()) { - if(delims.find_first_of(str[0]) != std::string::npos) { - keyChar = str[0]; - auto end = str.find_first_of(keyChar, 1); - while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes - end = str.find_first_of(keyChar, end + 1); - embeddedQuote = true; - } - if(end != std::string::npos) { - output.push_back(str.substr(1, end - 1)); + if(bracketChars.find_first_of(str[0]) != std::string::npos) { + auto bracketLoc = bracketChars.find_first_of(str[0]); + auto end = close_sequence(str, 0, matchBracketChars[bracketLoc]); + if(end >= str.size()) { + output.push_back(std::move(str)); + str.clear(); + } else { + output.push_back(str.substr(0, end + 1)); if(end + 2 < str.size()) { str = str.substr(end + 2); } else { str.clear(); } - - } else { - output.push_back(str.substr(1)); - str = ""; } + } else { auto it = std::find_if(std::begin(str), std::end(str), find_ws); if(it != std::end(str)) { @@ -580,14 +1075,9 @@ CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter) str = std::string(it + 1, str.end()); } else { output.push_back(str); - str = ""; + str.clear(); } } - // transform any embedded quotes into the regular character - if(embeddedQuote) { - output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); - embeddedQuote = false; - } trim(str); } return output; @@ -605,15 +1095,140 @@ CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) { return offset + 1; } -CLI11_INLINE std::string &add_quotes_if_needed(std::string &str) { - if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) { - char quote = str.find('"') < str.find('\'') ? '\'' : '"'; - if(str.find(' ') != std::string::npos) { - str.insert(0, 1, quote); - str.append(1, quote); +CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape) { + // s is our escaped output string + std::string escaped_string{}; + // loop through all characters + for(char c : string_to_escape) { + // check if a given character is printable + // the cast is necessary to avoid undefined behaviour + if(isprint(static_cast<unsigned char>(c)) == 0) { + std::stringstream stream; + // if the character is not printable + // we'll convert it to a hex string using a stringstream + // note that since char is signed we have to cast it to unsigned first + stream << std::hex << static_cast<unsigned int>(static_cast<unsigned char>(c)); + std::string code = stream.str(); + escaped_string += std::string("\\x") + (code.size() < 2 ? "0" : "") + code; + + } else { + escaped_string.push_back(c); } } - return str; + if(escaped_string != string_to_escape) { + auto sqLoc = escaped_string.find('\''); + while(sqLoc != std::string::npos) { + escaped_string.replace(sqLoc, sqLoc + 1, "\\x27"); + sqLoc = escaped_string.find('\''); + } + escaped_string.insert(0, "'B\"("); + escaped_string.push_back(')'); + escaped_string.push_back('"'); + escaped_string.push_back('\''); + } + return escaped_string; +} + +CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string) { + size_t ssize = escaped_string.size(); + if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { + return true; + } + return (escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0); +} + +CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string) { + std::size_t start{0}; + std::size_t tail{0}; + size_t ssize = escaped_string.size(); + if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { + start = 3; + tail = 2; + } else if(escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0) { + start = 4; + tail = 3; + } + + if(start == 0) { + return escaped_string; + } + std::string outstring; + + outstring.reserve(ssize - start - tail); + std::size_t loc = start; + while(loc < ssize - tail) { + // ssize-2 to skip )" at the end + if(escaped_string[loc] == '\\' && (escaped_string[loc + 1] == 'x' || escaped_string[loc + 1] == 'X')) { + auto c1 = escaped_string[loc + 2]; + auto c2 = escaped_string[loc + 3]; + + std::uint32_t res1 = hexConvert(c1); + std::uint32_t res2 = hexConvert(c2); + if(res1 <= 0x0F && res2 <= 0x0F) { + loc += 4; + outstring.push_back(static_cast<char>(res1 * 16 + res2)); + continue; + } + } + outstring.push_back(escaped_string[loc]); + ++loc; + } + return outstring; +} + +CLI11_INLINE void remove_quotes(std::vector<std::string> &args) { + for(auto &arg : args) { + if(arg.front() == '\"' && arg.back() == '\"') { + remove_quotes(arg); + // only remove escaped for string arguments not literal strings + arg = remove_escaped_characters(arg); + } else { + remove_quotes(arg); + } + } +} + +CLI11_INLINE bool process_quoted_string(std::string &str, char string_char, char literal_char) { + if(str.size() <= 1) { + return false; + } + if(detail::is_binary_escaped_string(str)) { + str = detail::extract_binary_string(str); + return true; + } + if(str.front() == string_char && str.back() == string_char) { + detail::remove_outer(str, string_char); + if(str.find_first_of('\\') != std::string::npos) { + str = detail::remove_escaped_characters(str); + } + return true; + } + if((str.front() == literal_char || str.front() == '`') && str.back() == str.front()) { + detail::remove_outer(str, str.front()); + return true; + } + return false; +} + +std::string get_environment_value(const std::string &env_name) { + char *buffer = nullptr; + std::string ename_string; + +#ifdef _MSC_VER + // Windows version + std::size_t sz = 0; + if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) { + ename_string = std::string(buffer); + free(buffer); + } +#else + // This also works on Windows, but gives a warning + buffer = std::getenv(env_name.c_str()); + if(buffer != nullptr) { + ename_string = std::string(buffer); + } +#endif + return ename_string; } } // namespace detail @@ -722,7 +1337,13 @@ class BadNameString : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, BadNameString) CLI11_ERROR_SIMPLE(BadNameString) static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } + static BadNameString MissingDash(std::string name) { + return BadNameString("Long names strings require 2 dashes " + name); + } static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } + static BadNameString BadPositionalName(std::string name) { + return BadNameString("Invalid positional Name: " + name); + } static BadNameString DashesOnly(std::string name) { return BadNameString("Must have a name, not just dashes: " + name); } @@ -1087,12 +1708,20 @@ template <typename T, typename C> class is_direct_constructible { static auto test(int, std::true_type) -> decltype( // NVCC warns about narrowing conversions here #ifdef __CUDACC__ +#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ +#pragma nv_diag_suppress 2361 +#else #pragma diag_suppress 2361 #endif +#endif TT{std::declval<CC>()} #ifdef __CUDACC__ +#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ +#pragma nv_diag_default 2361 +#else #pragma diag_default 2361 #endif +#endif , std::is_move_assignable<TT>()); @@ -1171,8 +1800,10 @@ struct is_mutable_container< decltype(std::declval<T>().clear()), decltype(std::declval<T>().insert(std::declval<decltype(std::declval<T>().end())>(), std::declval<const typename T::value_type &>()))>, - void>> - : public conditional_t<std::is_constructible<T, std::string>::value, std::false_type, std::true_type> {}; + void>> : public conditional_t<std::is_constructible<T, std::string>::value || + std::is_constructible<T, std::wstring>::value, + std::false_type, + std::true_type> {}; // check to see if an object is a mutable container (fail by default) template <typename T, typename _ = void> struct is_readable_container : std::false_type {}; @@ -1475,6 +2106,8 @@ enum class object_category : int { // string like types string_assignable = 23, string_constructible = 24, + wstring_assignable = 25, + wstring_constructible = 26, other = 45, // special wrapper or container types wrapper_value = 50, @@ -1523,12 +2156,23 @@ template <typename T> struct classify_object<T, typename std::enable_if<is_bool< template <typename T> struct classify_object<T, typename std::enable_if<std::is_floating_point<T>::value>::type> { static constexpr object_category value{object_category::floating_point}; }; +#if defined _MSC_VER +// in MSVC wstring should take precedence if available this isn't as useful on other compilers due to the broader use of +// utf-8 encoding +#define WIDE_STRING_CHECK \ + !std::is_assignable<T &, std::wstring>::value && !std::is_constructible<T, std::wstring>::value +#define STRING_CHECK true +#else +#define WIDE_STRING_CHECK true +#define STRING_CHECK !std::is_assignable<T &, std::string>::value && !std::is_constructible<T, std::string>::value +#endif /// String and similar direct assignment template <typename T> -struct classify_object<T, - typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && - std::is_assignable<T &, std::string>::value>::type> { +struct classify_object< + T, + typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && WIDE_STRING_CHECK && + std::is_assignable<T &, std::string>::value>::type> { static constexpr object_category value{object_category::string_assignable}; }; @@ -1538,10 +2182,27 @@ struct classify_object< T, typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && !std::is_assignable<T &, std::string>::value && (type_count<T>::value == 1) && - std::is_constructible<T, std::string>::value>::type> { + WIDE_STRING_CHECK && std::is_constructible<T, std::string>::value>::type> { static constexpr object_category value{object_category::string_constructible}; }; +/// Wide strings +template <typename T> +struct classify_object<T, + typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && + STRING_CHECK && std::is_assignable<T &, std::wstring>::value>::type> { + static constexpr object_category value{object_category::wstring_assignable}; +}; + +template <typename T> +struct classify_object< + T, + typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && + !std::is_assignable<T &, std::wstring>::value && (type_count<T>::value == 1) && + STRING_CHECK && std::is_constructible<T, std::wstring>::value>::type> { + static constexpr object_category value{object_category::wstring_constructible}; +}; + /// Enumerations template <typename T> struct classify_object<T, typename std::enable_if<std::is_enum<T>::value>::type> { static constexpr object_category value{object_category::enumeration}; @@ -1554,12 +2215,13 @@ template <typename T> struct classify_object<T, typename std::enable_if<is_compl /// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, /// vectors, and enumerations template <typename T> struct uncommon_type { - using type = typename std::conditional<!std::is_floating_point<T>::value && !std::is_integral<T>::value && - !std::is_assignable<T &, std::string>::value && - !std::is_constructible<T, std::string>::value && !is_complex<T>::value && - !is_mutable_container<T>::value && !std::is_enum<T>::value, - std::true_type, - std::false_type>::type; + using type = typename std::conditional< + !std::is_floating_point<T>::value && !std::is_integral<T>::value && + !std::is_assignable<T &, std::string>::value && !std::is_constructible<T, std::string>::value && + !std::is_assignable<T &, std::wstring>::value && !std::is_constructible<T, std::wstring>::value && + !is_complex<T>::value && !is_mutable_container<T>::value && !std::is_enum<T>::value, + std::true_type, + std::false_type>::type; static constexpr bool value = type::value; }; @@ -1748,7 +2410,7 @@ bool integral_conversion(const std::string &input, T &output) noexcept { if(input.empty() || input.front() == '-') { return false; } - char *val = nullptr; + char *val{nullptr}; errno = 0; std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); if(errno == ERANGE) { @@ -1764,6 +2426,33 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = (output_sll < 0) ? static_cast<T>(0) : static_cast<T>(output_sll); return (static_cast<std::int64_t>(output) == output_sll); } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return integral_conversion(nstring, output); + } + if(input.compare(0, 2, "0o") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoull(input.c_str() + 2, &val, 8); + if(errno == ERANGE) { + return false; + } + output = static_cast<T>(output_ll); + return (val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll); + } + if(input.compare(0, 2, "0b") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoull(input.c_str() + 2, &val, 2); + if(errno == ERANGE) { + return false; + } + output = static_cast<T>(output_ll); + return (val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll); + } return false; } @@ -1788,11 +2477,38 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = static_cast<T>(1); return true; } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return integral_conversion(nstring, output); + } + if(input.compare(0, 2, "0o") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoll(input.c_str() + 2, &val, 8); + if(errno == ERANGE) { + return false; + } + output = static_cast<T>(output_ll); + return (val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll); + } + if(input.compare(0, 2, "0b") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoll(input.c_str() + 2, &val, 2); + if(errno == ERANGE) { + return false; + } + output = static_cast<T>(output_ll); + return (val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll); + } return false; } -/// Convert a flag into an integer value typically binary flags -inline std::int64_t to_flag_value(std::string val) { +/// Convert a flag into an integer value typically binary flags sets errno to nonzero if conversion failed +inline std::int64_t to_flag_value(std::string val) noexcept { static const std::string trueString("true"); static const std::string falseString("false"); if(val == trueString) { @@ -1820,7 +2536,8 @@ inline std::int64_t to_flag_value(std::string val) { ret = 1; break; default: - throw std::invalid_argument("unrecognized character"); + errno = EINVAL; + return -1; } return ret; } @@ -1829,7 +2546,11 @@ inline std::int64_t to_flag_value(std::string val) { } else if(val == falseString || val == "off" || val == "no" || val == "disable") { ret = -1; } else { - ret = std::stoll(val); + char *loc_ptr{nullptr}; + ret = std::strtoll(val.c_str(), &loc_ptr, 0); + if(loc_ptr != (val.c_str() + val.size()) && errno == 0) { + errno = EINVAL; + } } return ret; } @@ -1858,18 +2579,16 @@ bool lexical_cast(const std::string &input, T &output) { template <typename T, enable_if_t<classify_object<T>::value == object_category::boolean_value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { - try { - auto out = to_flag_value(input); + errno = 0; + auto out = to_flag_value(input); + if(errno == 0) { output = (out > 0); - return true; - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still - // valid all we care about the sign + } else if(errno == ERANGE) { output = (input[0] != '-'); - return true; + } else { + return false; } + return true; } /// Floats @@ -1882,7 +2601,17 @@ bool lexical_cast(const std::string &input, T &output) { char *val = nullptr; auto output_ld = std::strtold(input.c_str(), &val); output = static_cast<T>(output_ld); - return val == (input.c_str() + input.size()); + if(val == (input.c_str() + input.size())) { + return true; + } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return lexical_cast(nstring, output); + } + return false; } /// complex @@ -1934,6 +2663,23 @@ bool lexical_cast(const std::string &input, T &output) { return true; } +/// Wide strings +template < + typename T, + enable_if_t<classify_object<T>::value == object_category::wstring_assignable, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = widen(input); + return true; +} + +template < + typename T, + enable_if_t<classify_object<T>::value == object_category::wstring_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = T{widen(input)}; + return true; +} + /// Enumerations template <typename T, enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy> @@ -2062,7 +2808,9 @@ template <typename AssignTo, typename ConvertTo, enable_if_t<std::is_same<AssignTo, ConvertTo>::value && (classify_object<AssignTo>::value == object_category::string_assignable || - classify_object<AssignTo>::value == object_category::string_constructible), + classify_object<AssignTo>::value == object_category::string_constructible || + classify_object<AssignTo>::value == object_category::wstring_assignable || + classify_object<AssignTo>::value == object_category::wstring_constructible), detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { return lexical_cast(input, output); @@ -2073,7 +2821,9 @@ template <typename AssignTo, typename ConvertTo, enable_if_t<std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, AssignTo>::value && classify_object<AssignTo>::value != object_category::string_assignable && - classify_object<AssignTo>::value != object_category::string_constructible, + classify_object<AssignTo>::value != object_category::string_constructible && + classify_object<AssignTo>::value != object_category::wstring_assignable && + classify_object<AssignTo>::value != object_category::wstring_constructible, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { if(input.empty()) { @@ -2112,9 +2862,17 @@ bool lexical_assign(const std::string &input, AssignTo &output) { output = 0; return true; } - int val = 0; + int val{0}; if(lexical_cast(input, val)) { +#if defined(__clang__) +/* on some older clang compilers */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" +#endif output = val; +#if defined(__clang__) +#pragma clang diagnostic pop +#endif return true; } return false; @@ -2169,12 +2927,12 @@ template <typename AssignTo, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) { // the remove const is to handle pair types coming from a container - typename std::remove_const<typename std::tuple_element<0, ConvertTo>::type>::type v1; - typename std::tuple_element<1, ConvertTo>::type v2; - bool retval = lexical_assign<decltype(v1), decltype(v1)>(strings[0], v1); - if(strings.size() > 1) { - retval = retval && lexical_assign<decltype(v2), decltype(v2)>(strings[1], v2); - } + using FirstType = typename std::remove_const<typename std::tuple_element<0, ConvertTo>::type>::type; + using SecondType = typename std::tuple_element<1, ConvertTo>::type; + FirstType v1; + SecondType v2; + bool retval = lexical_assign<FirstType, FirstType>(strings[0], v1); + retval = retval && lexical_assign<SecondType, SecondType>((strings.size() > 1) ? strings[1] : std::string{}, v2); if(retval) { output = AssignTo{v1, v2}; } @@ -2189,6 +2947,9 @@ template <class AssignTo, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) { output.erase(output.begin(), output.end()); + if(strings.empty()) { + return true; + } if(strings.size() == 1 && strings[0] == "{}") { return true; } @@ -2333,7 +3094,7 @@ tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) { std::size_t index{subtype_count_min<ConvertTo>::value}; const std::size_t mx_count{subtype_count<ConvertTo>::value}; - const std::size_t mx{(std::max)(mx_count, strings.size())}; + const std::size_t mx{(std::min)(mx_count, strings.size() - 1)}; while(index < mx) { if(is_separator(strings[index])) { @@ -2343,7 +3104,11 @@ tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) { } bool retval = lexical_conversion<AssignTo, ConvertTo>( std::vector<std::string>(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index)), output); - strings.erase(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index) + 1); + if(strings.size() > index) { + strings.erase(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index) + 1); + } else { + strings.clear(); + } return retval; } @@ -2487,12 +3252,13 @@ inline std::string sum_string_vector(const std::vector<std::string> &values) { double tv{0.0}; auto comp = lexical_cast(arg, tv); if(!comp) { - try { - tv = static_cast<double>(detail::to_flag_value(arg)); - } catch(const std::exception &) { - fail = true; + errno = 0; + auto fv = detail::to_flag_value(arg); + fail = (errno != 0); + if(fail) { break; } + tv = static_cast<double>(fv); } val += tv; } @@ -2501,13 +3267,10 @@ inline std::string sum_string_vector(const std::vector<std::string> &values) { output.append(arg); } } else { - if(val <= static_cast<double>((std::numeric_limits<std::int64_t>::min)()) || - val >= static_cast<double>((std::numeric_limits<std::int64_t>::max)()) || - std::ceil(val) == std::floor(val)) { - output = detail::value_string(static_cast<int64_t>(val)); - } else { - output = detail::value_string(val); - } + std::ostringstream out; + out.precision(16); + out << val; + output = out.str(); } return output; } @@ -2553,7 +3316,7 @@ CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std } CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { + if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) { auto loc = current.find_first_of('='); if(loc != std::string::npos) { name = current.substr(2, loc - 2); @@ -2625,7 +3388,6 @@ get_names(const std::vector<std::string> &input) { std::vector<std::string> short_names; std::vector<std::string> long_names; std::string pos_name; - for(std::string name : input) { if(name.length() == 0) { continue; @@ -2633,6 +3395,8 @@ get_names(const std::vector<std::string> &input) { if(name.length() > 1 && name[0] == '-' && name[1] != '-') { if(name.length() == 2 && valid_first_char(name[1])) short_names.emplace_back(1, name[1]); + else if(name.length() > 2) + throw BadNameString::MissingDash(name); else throw BadNameString::OneCharName(name); } else if(name.length() > 2 && name.substr(0, 2) == "--") { @@ -2644,12 +3408,15 @@ get_names(const std::vector<std::string> &input) { } else if(name == "-" || name == "--") { throw BadNameString::DashesOnly(name); } else { - if(pos_name.length() > 0) + if(!pos_name.empty()) throw BadNameString::MultiPositionalNames(name); - pos_name = name; + if(valid_name_string(name)) { + pos_name = name; + } else { + throw BadNameString::BadPositionalName(name); + } } } - return std::make_tuple(short_names, long_names, pos_name); } @@ -2666,7 +3433,6 @@ struct ConfigItem { /// This is the name std::string name{}; - /// Listing of inputs std::vector<std::string> inputs{}; @@ -2729,8 +3495,8 @@ class ConfigBase : public Config { char valueDelimiter = '='; /// the character to use around strings char stringQuote = '"'; - /// the character to use around single characters - char characterQuote = '\''; + /// the character to use around single characters and literal strings + char literalQuote = '\''; /// the maximum number of layers to allow uint8_t maximumLayers{255}; /// the separator used to separator parent layers @@ -2766,10 +3532,10 @@ class ConfigBase : public Config { valueDelimiter = vSep; return this; } - /// Specify the quote characters used around strings and characters - ConfigBase *quoteCharacter(char qString, char qChar) { + /// Specify the quote characters used around strings and literal strings + ConfigBase *quoteCharacter(char qString, char literalChar) { stringQuote = qString; - characterQuote = qChar; + literalQuote = literalChar; return this; } /// Specify the maximum number of parents @@ -3001,6 +3767,11 @@ class IPV4Validator : public Validator { IPV4Validator(); }; +class EscapedStringTransformer : public Validator { + public: + EscapedStringTransformer(); +}; + } // namespace detail // Static is not needed here, because global const implies static. @@ -3020,6 +3791,9 @@ const detail::NonexistentPathValidator NonexistentPath; /// Check for an IP4 address const detail::IPV4Validator ValidIPV4; +/// convert escaped characters into their associated values +const detail::EscapedStringTransformer EscapedString; + /// Validate the input as a particular type template <typename DesiredType> class TypeValidator : public Validator { public: @@ -3772,14 +4546,14 @@ namespace detail { #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 CLI11_INLINE path_type check_path(const char *file) noexcept { std::error_code ec; - auto stat = std::filesystem::status(file, ec); + auto stat = std::filesystem::status(to_path(file), ec); if(ec) { return path_type::nonexistent; } switch(stat.type()) { case std::filesystem::file_type::none: // LCOV_EXCL_LINE case std::filesystem::file_type::not_found: - return path_type::nonexistent; + return path_type::nonexistent; // LCOV_EXCL_LINE case std::filesystem::file_type::directory: return path_type::directory; case std::filesystem::file_type::symlink: @@ -3873,10 +4647,29 @@ CLI11_INLINE IPV4Validator::IPV4Validator() : Validator("IPV4") { return std::string("Each IP number must be between 0 and 255 ") + var; } } - return std::string(); + return std::string{}; }; } +CLI11_INLINE EscapedStringTransformer::EscapedStringTransformer() { + func_ = [](std::string &str) { + try { + if(str.size() > 1 && (str.front() == '\"' || str.front() == '\'' || str.front() == '`') && + str.front() == str.back()) { + process_quoted_string(str); + } else if(str.find_first_of('\\') != std::string::npos) { + if(detail::is_binary_escaped_string(str)) { + str = detail::extract_binary_string(str); + } else { + str = remove_escaped_characters(str); + } + } + return std::string{}; + } catch(const std::invalid_argument &ia) { + return std::string(ia.what()); + } + }; +} } // namespace detail CLI11_INLINE FileOnDefaultPath::FileOnDefaultPath(std::string default_path, bool enableErrorReturn) @@ -4175,7 +4968,8 @@ enum class MultiOptionPolicy : char { TakeFirst, //!< take only the first Expected number of arguments Join, //!< merge all the arguments together into a single string via the delimiter character default('\n') TakeAll, //!< just get all the passed argument regardless - Sum //!< sum all the arguments together if numerical or concatenate directly without delimiter + Sum, //!< sum all the arguments together if numerical or concatenate directly without delimiter + Reverse, //!< take only the last Expected number of arguments in reverse order }; /// This is the CRTP base class for Option and OptionDefaults. It was designed this way @@ -4683,12 +5477,12 @@ class Option : public OptionBase<Option> { if(!lnames_.empty()) { return lnames_[0]; } - if(!pname_.empty()) { - return pname_; - } if(!snames_.empty()) { return snames_[0]; } + if(!pname_.empty()) { + return pname_; + } return envname_; } /// The number of times the option expects to be included @@ -4711,13 +5505,13 @@ class Option : public OptionBase<Option> { CLI11_NODISCARD int get_items_expected() const { return get_items_expected_min(); } /// True if the argument can be given directly - CLI11_NODISCARD bool get_positional() const { return pname_.length() > 0; } + CLI11_NODISCARD bool get_positional() const { return !pname_.empty(); } /// True if option has at least one non-positional name - CLI11_NODISCARD bool nonpositional() const { return (snames_.size() + lnames_.size()) > 0; } + CLI11_NODISCARD bool nonpositional() const { return (!lnames_.empty() || !snames_.empty()); } /// True if option has description - CLI11_NODISCARD bool has_description() const { return description_.length() > 0; } + CLI11_NODISCARD bool has_description() const { return !description_.empty(); } /// Get the description CLI11_NODISCARD const std::string &get_description() const { return description_; } @@ -5226,13 +6020,29 @@ CLI11_INLINE void Option::run_callback() { CLI11_NODISCARD CLI11_INLINE const std::string &Option::matching_name(const Option &other) const { static const std::string estring; - for(const std::string &sname : snames_) + for(const std::string &sname : snames_) { if(other.check_sname(sname)) return sname; - for(const std::string &lname : lnames_) + if(other.check_lname(sname)) + return sname; + } + for(const std::string &lname : lnames_) { if(other.check_lname(lname)) return lname; - + if(lname.size() == 1) { + if(other.check_sname(lname)) { + return lname; + } + } + } + if(snames_.empty() && lnames_.empty() && !pname_.empty()) { + if(other.check_sname(pname_) || other.check_lname(pname_) || pname_ == other.pname_) + return pname_; + } + if(other.snames_.empty() && other.fnames_.empty() && !other.pname_.empty()) { + if(check_sname(other.pname_) || check_lname(other.pname_) || (pname_ == other.pname_)) + return other.pname_; + } if(ignore_case_ || ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore for(const std::string &sname : other.snames_) @@ -5286,6 +6096,9 @@ CLI11_NODISCARD CLI11_INLINE std::string Option::get_flag_value(const std::strin if(default_ind >= 0) { // We can static cast this to std::size_t because it is more than 0 in this block if(default_flag_values_[static_cast<std::size_t>(default_ind)].second != input_value) { + if(input_value == default_str_ && force_callback_) { + return input_value; + } throw(ArgumentMismatch::FlagOverride(name)); } } else { @@ -5306,15 +6119,15 @@ CLI11_NODISCARD CLI11_INLINE std::string Option::get_flag_value(const std::strin return input_value; } if(default_flag_values_[static_cast<std::size_t>(ind)].second == falseString) { - try { - auto val = detail::to_flag_value(input_value); - return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val)); - } catch(const std::invalid_argument &) { + errno = 0; + auto val = detail::to_flag_value(input_value); + if(errno != 0) { + errno = 0; return input_value; } - } else { - return input_value; + return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val)); } + return input_value; } CLI11_INLINE Option *Option::add_result(std::string s) { @@ -5417,7 +6230,8 @@ CLI11_INLINE void Option::_validate_results(results_t &res) const { if(type_size_max_ > 1) { // in this context index refers to the index in the type int index = 0; if(get_items_expected_max() < static_cast<int>(res.size()) && - multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) { + (multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast || + multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) { // create a negative index for the earliest ones index = get_items_expected_max() - static_cast<int>(res.size()); } @@ -5435,7 +6249,8 @@ CLI11_INLINE void Option::_validate_results(results_t &res) const { } else { int index = 0; if(expected_max_ < static_cast<int>(res.size()) && - multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) { + (multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast || + multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) { // create a negative index for the earliest ones index = expected_max_ - static_cast<int>(res.size()); } @@ -5467,6 +6282,15 @@ CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &origi out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end()); } } break; + case MultiOptionPolicy::Reverse: { + // Allow multi-option sizes (including 0) + std::size_t trim_size = std::min<std::size_t>( + static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size()); + if(original.size() != trim_size || trim_size > 1) { + out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end()); + } + std::reverse(out.begin(), out.end()); + } break; case MultiOptionPolicy::TakeFirst: { std::size_t trim_size = std::min<std::size_t>( static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size()); @@ -5496,7 +6320,12 @@ CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &origi throw ArgumentMismatch::AtLeast(get_name(), static_cast<int>(num_min), original.size()); } if(original.size() > num_max) { - throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size()); + if(original.size() == 2 && num_max == 1 && original[1] == "%%" && original[0] == "{}") { + // this condition is a trap for the following empty indicator check on config files + out = original; + } else { + throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size()); + } } break; } @@ -5505,11 +6334,11 @@ CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &origi // {} is the indicator for an empty container if(out.empty()) { if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) { - out.push_back("{}"); - out.push_back("%%"); + out.emplace_back("{}"); + out.emplace_back("%%"); } } else if(out.size() == 1 && out[0] == "{}" && get_items_expected_min() > 0) { - out.push_back("%%"); + out.emplace_back("%%"); } } @@ -5570,9 +6399,9 @@ CLI11_INLINE int Option::_add_result(std::string &&result, std::vector<std::stri #ifndef CLI11_PARSE -#define CLI11_PARSE(app, argc, argv) \ +#define CLI11_PARSE(app, ...) \ try { \ - (app).parse((argc), (argv)); \ + (app).parse(__VA_ARGS__); \ } catch(const CLI::ParseError &e) { \ return (app).exit(e); \ } @@ -5685,6 +6514,12 @@ class App { /// @name Help ///@{ + /// Usage to put after program/subcommand description in the help output INHERITABLE + std::string usage_{}; + + /// This is a function that generates a usage to put after program/subcommand description in help output + std::function<std::string()> usage_callback_{}; + /// Footer to put after all options in the help output INHERITABLE std::string footer_{}; @@ -5819,6 +6654,14 @@ class App { ///@} +#ifdef _WIN32 + /// When normalizing argv to UTF-8 on Windows, this is the storage for normalized args. + std::vector<std::string> normalized_argv_{}; + + /// When normalizing argv to UTF-8 on Windows, this is the `char**` value returned to the user. + std::vector<char *> normalized_argv_view_{}; +#endif + /// Special private constructor for subcommand App(std::string app_description, std::string app_name, App *parent); @@ -5838,6 +6681,9 @@ class App { /// virtual destructor virtual ~App() = default; + /// Convert the contents of argv to UTF-8. Only does something on Windows, does nothing elsewhere. + CLI11_NODISCARD char **ensure_utf8(char **argv); + /// Set a callback for execution when all parsing and processing has completed /// /// Due to a bug in c++11, @@ -6369,12 +7215,18 @@ class App { /// Parses the command line - throws errors. /// This must be called after the options are in but before the rest of the program. void parse(int argc, const char *const *argv); + void parse(int argc, const wchar_t *const *argv); + + private: + template <class CharT> void parse_char_t(int argc, const CharT *const *argv); + public: /// Parse a single string as if it contained command line arguments. /// This function splits the string into arguments then calls parse(std::vector<std::string> &) /// the function takes an optional boolean argument specifying if the programName is included in the string to /// process void parse(std::string commandline, bool program_name_included = false); + void parse(std::wstring commandline, bool program_name_included = false); /// The real work is done here. Expects a reversed vector. /// Changes the vector to the remaining options. @@ -6482,6 +7334,16 @@ class App { /// @name Help ///@{ + /// Set usage. + App *usage(std::string usage_string) { + usage_ = std::move(usage_string); + return this; + } + /// Set usage. + App *usage(std::function<std::string()> usage_function) { + usage_callback_ = std::move(usage_function); + return this; + } /// Set footer. App *footer(std::string footer_string) { footer_ = std::move(footer_string); @@ -6590,6 +7452,11 @@ class App { /// Get the group of this subcommand CLI11_NODISCARD const std::string &get_group() const { return group_; } + /// Generate and return the usage. + CLI11_NODISCARD std::string get_usage() const { + return (usage_callback_) ? usage_callback_() + '\n' + usage_ : usage_; + } + /// Generate and return the footer. CLI11_NODISCARD std::string get_footer() const { return (footer_callback_) ? footer_callback_() + '\n' + footer_ : footer_; @@ -6727,6 +7594,9 @@ class App { /// Read and process a configuration file (main app only) void _process_config_file(); + /// Read and process a particular configuration file + bool _process_config_file(const std::string &config_file, bool throw_error); + /// Get envname options if not yet passed. Runs on *all* subcommands. void _process_env(); @@ -6799,8 +7669,9 @@ class App { bool _parse_subcommand(std::vector<std::string> &args); /// Parse a short (false) or long (true) argument, must be at the top of the list + /// if local_processing_only is set to true then fallthrough is disabled will return false if not found /// return true if the argument was processed or false if nothing was done - bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type); + bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only); /// Trigger the pre_parse callback if needed void _trigger_pre_parse(std::size_t remaining_args); @@ -6955,6 +7826,7 @@ CLI11_INLINE App::App(std::string app_description, std::string app_name, App *pa configurable_ = parent_->configurable_; allow_windows_style_options_ = parent_->allow_windows_style_options_; group_ = parent_->group_; + usage_ = parent_->usage_; footer_ = parent_->footer_; formatter_ = parent_->formatter_; config_formatter_ = parent_->config_formatter_; @@ -6962,10 +7834,32 @@ CLI11_INLINE App::App(std::string app_description, std::string app_name, App *pa } } +CLI11_NODISCARD CLI11_INLINE char **App::ensure_utf8(char **argv) { +#ifdef _WIN32 + (void)argv; + + normalized_argv_ = detail::compute_win32_argv(); + + if(!normalized_argv_view_.empty()) { + normalized_argv_view_.clear(); + } + + normalized_argv_view_.reserve(normalized_argv_.size()); + for(auto &arg : normalized_argv_) { + // using const_cast is well-defined, string is known to not be const. + normalized_argv_view_.push_back(const_cast<char *>(arg.data())); + } + + return normalized_argv_view_.data(); +#else + return argv; +#endif +} + CLI11_INLINE App *App::name(std::string app_name) { if(parent_ != nullptr) { - auto oname = name_; + std::string oname = name_; name_ = app_name; const auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent()); if(!res.empty()) { @@ -7046,6 +7940,32 @@ CLI11_INLINE Option *App::add_option(std::string option_name, if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; }) == std::end(options_)) { + if(myopt.lnames_.empty() && myopt.snames_.empty()) { + // if the option is positional only there is additional potential for ambiguities in config files and needs + // to be checked + std::string test_name = "--" + myopt.get_single_name(); + if(test_name.size() == 3) { + test_name.erase(0, 1); + } + + auto *op = get_option_no_throw(test_name); + if(op != nullptr) { + throw(OptionAlreadyAdded("added option positional name matches existing option: " + test_name)); + } + } else if(parent_ != nullptr) { + for(auto &ln : myopt.lnames_) { + auto *op = parent_->get_option_no_throw(ln); + if(op != nullptr) { + throw(OptionAlreadyAdded("added option matches existing positional option: " + ln)); + } + } + for(auto &sn : myopt.snames_) { + auto *op = parent_->get_option_no_throw(sn); + if(op != nullptr) { + throw(OptionAlreadyAdded("added option matches existing positional option: " + sn)); + } + } + } options_.emplace_back(); Option_p &option = options_.back(); option.reset(new Option(option_name, option_description, option_callback, this)); @@ -7220,8 +8140,11 @@ CLI11_INLINE Option *App::set_config(std::string option_name, } if(!default_filename.empty()) { config_ptr_->default_str(std::move(default_filename)); + config_ptr_->force_callback_ = true; } config_ptr_->configurable(false); + // set the option to take the last value and reverse given by default + config_ptr_->multi_option_policy(MultiOptionPolicy::Reverse); } return config_ptr_; @@ -7251,13 +8174,14 @@ CLI11_INLINE bool App::remove_option(Option *opt) { CLI11_INLINE App *App::add_subcommand(std::string subcommand_name, std::string subcommand_description) { if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) { if(!detail::valid_first_char(subcommand_name[0])) { - throw IncorrectConstruction("Subcommand name starts with invalid character, '!' and '-' are not allowed"); + throw IncorrectConstruction( + "Subcommand name starts with invalid character, '!' and '-' and control characters"); } for(auto c : subcommand_name) { if(!detail::valid_later_char(c)) { throw IncorrectConstruction(std::string("Subcommand name contains invalid character ('") + c + "'), all characters are allowed except" - "'=',':','{','}', and ' '"); + "'=',':','{','}', ' ', and control characters"); } } } @@ -7382,17 +8306,29 @@ CLI11_INLINE void App::clear() { } } -CLI11_INLINE void App::parse(int argc, const char *const *argv) { +CLI11_INLINE void App::parse(int argc, const char *const *argv) { parse_char_t(argc, argv); } +CLI11_INLINE void App::parse(int argc, const wchar_t *const *argv) { parse_char_t(argc, argv); } + +namespace detail { + +// Do nothing or perform narrowing +CLI11_INLINE const char *maybe_narrow(const char *str) { return str; } +CLI11_INLINE std::string maybe_narrow(const wchar_t *str) { return narrow(str); } + +} // namespace detail + +template <class CharT> CLI11_INLINE void App::parse_char_t(int argc, const CharT *const *argv) { // If the name is not set, read from command line if(name_.empty() || has_automatic_name_) { has_automatic_name_ = true; - name_ = argv[0]; + name_ = detail::maybe_narrow(argv[0]); } std::vector<std::string> args; args.reserve(static_cast<std::size_t>(argc) - 1U); for(auto i = static_cast<std::size_t>(argc) - 1U; i > 0U; --i) - args.emplace_back(argv[i]); + args.emplace_back(detail::maybe_narrow(argv[i])); + parse(std::move(args)); } @@ -7418,11 +8354,19 @@ CLI11_INLINE void App::parse(std::string commandline, bool program_name_included auto args = detail::split_up(std::move(commandline)); // remove all empty strings args.erase(std::remove(args.begin(), args.end(), std::string{}), args.end()); + try { + detail::remove_quotes(args); + } catch(const std::invalid_argument &arg) { + throw CLI::ParseError(arg.what(), CLI::ExitCodes::InvalidError); + } std::reverse(args.begin(), args.end()); - parse(std::move(args)); } +CLI11_INLINE void App::parse(std::wstring commandline, bool program_name_included) { + parse(narrow(commandline), program_name_included); +} + CLI11_INLINE void App::parse(std::vector<std::string> &args) { // Clear if parsed if(parsed_ > 0) @@ -7489,7 +8433,7 @@ CLI11_INLINE int App::exit(const Error &e, std::ostream &out, std::ostream &err) } if(e.get_name() == "CallForVersion") { - out << e.what() << std::endl; + out << e.what() << '\n'; return e.get_exit_code(); } @@ -7585,7 +8529,8 @@ CLI11_NODISCARD CLI11_INLINE std::string App::help(std::string prev, AppFormatMo CLI11_NODISCARD CLI11_INLINE std::string App::version() const { std::string val; if(version_ptr_ != nullptr) { - auto rv = version_ptr_->results(); + // copy the results for reuse later + results_t rv = version_ptr_->results(); version_ptr_->clear(); version_ptr_->add_result("true"); try { @@ -7694,7 +8639,7 @@ CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) con if(local_name == name_to_check) { return true; } - for(auto les : aliases_) { // NOLINT(performance-for-range-copy) + for(std::string les : aliases_) { // NOLINT(performance-for-range-copy) if(ignore_underscore_) { les = detail::remove_underscore(les); } @@ -7884,70 +8829,96 @@ CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::strin return detail::Classifier::WINDOWS_STYLE; if((current == "++") && !name_.empty() && parent_ != nullptr) return detail::Classifier::SUBCOMMAND_TERMINATOR; + auto dotloc = current.find_first_of('.'); + if(dotloc != std::string::npos) { + auto *cm = _find_subcommand(current.substr(0, dotloc), true, ignore_used_subcommands); + if(cm != nullptr) { + auto res = cm->_recognize(current.substr(dotloc + 1), ignore_used_subcommands); + if(res == detail::Classifier::SUBCOMMAND) { + return res; + } + } + } return detail::Classifier::NONE; } +CLI11_INLINE bool App::_process_config_file(const std::string &config_file, bool throw_error) { + auto path_result = detail::check_path(config_file.c_str()); + if(path_result == detail::path_type::file) { + try { + std::vector<ConfigItem> values = config_formatter_->from_file(config_file); + _parse_config(values); + return true; + } catch(const FileError &) { + if(throw_error) { + throw; + } + return false; + } + } else if(throw_error) { + throw FileError::Missing(config_file); + } else { + return false; + } +} + CLI11_INLINE void App::_process_config_file() { if(config_ptr_ != nullptr) { bool config_required = config_ptr_->get_required(); auto file_given = config_ptr_->count() > 0; + if(!(file_given || config_ptr_->envname_.empty())) { + std::string ename_string = detail::get_environment_value(config_ptr_->envname_); + if(!ename_string.empty()) { + config_ptr_->add_result(ename_string); + } + } + config_ptr_->run_callback(); + auto config_files = config_ptr_->as<std::vector<std::string>>(); + bool files_used{file_given}; if(config_files.empty() || config_files.front().empty()) { if(config_required) { - throw FileError::Missing("no specified config file"); + throw FileError("config file is required but none was given"); } return; } - for(auto rit = config_files.rbegin(); rit != config_files.rend(); ++rit) { - const auto &config_file = *rit; - auto path_result = detail::check_path(config_file.c_str()); - if(path_result == detail::path_type::file) { - try { - std::vector<ConfigItem> values = config_formatter_->from_file(config_file); - _parse_config(values); - if(!file_given) { - config_ptr_->add_result(config_file); - } - } catch(const FileError &) { - if(config_required || file_given) - throw; - } - } else if(config_required || file_given) { - throw FileError::Missing(config_file); + for(const auto &config_file : config_files) { + if(_process_config_file(config_file, config_required || file_given)) { + files_used = true; } } + if(!files_used) { + // this is done so the count shows as 0 if no callbacks were processed + config_ptr_->clear(); + bool force = config_ptr_->force_callback_; + config_ptr_->force_callback_ = false; + config_ptr_->run_callback(); + config_ptr_->force_callback_ = force; + } } } CLI11_INLINE void App::_process_env() { for(const Option_p &opt : options_) { if(opt->count() == 0 && !opt->envname_.empty()) { - char *buffer = nullptr; - std::string ename_string; - -#ifdef _MSC_VER - // Windows version - std::size_t sz = 0; - if(_dupenv_s(&buffer, &sz, opt->envname_.c_str()) == 0 && buffer != nullptr) { - ename_string = std::string(buffer); - free(buffer); - } -#else - // This also works on Windows, but gives a warning - buffer = std::getenv(opt->envname_.c_str()); - if(buffer != nullptr) - ename_string = std::string(buffer); -#endif - + std::string ename_string = detail::get_environment_value(opt->envname_); if(!ename_string.empty()) { - opt->add_result(ename_string); + std::string result = ename_string; + result = opt->_validate(result, 0); + if(result.empty()) { + opt->add_result(ename_string); + } } } } for(App_p &sub : subcommands_) { - if(sub->get_name().empty() || !sub->parse_complete_callback_) - sub->_process_env(); + if(sub->get_name().empty() || !sub->parse_complete_callback_) { + if(sub->count_all() > 0) { + // only process environment variables if the callback has actually been triggered already + sub->_process_env(); + } + } } } @@ -8247,12 +9218,11 @@ CLI11_INLINE void App::_parse_config(const std::vector<ConfigItem> &args) { } CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t level) { + if(level < item.parents.size()) { try { auto *subcom = get_subcommand(item.parents.at(level)); - auto result = subcom->_parse_single_config(item, level + 1); - - return result; + return subcom->_parse_single_config(item, level + 1); } catch(const OptionNotFound &) { return false; } @@ -8282,15 +9252,19 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t if(item.name.size() == 1) { op = get_option_no_throw("-" + item.name); } + if(op == nullptr) { + op = get_option_no_throw(item.name); + } } - if(op == nullptr) { - op = get_option_no_throw(item.name); - } + if(op == nullptr) { // If the option was not present if(get_allow_config_extras() == config_extras_mode::capture) // Should we worry about classifying the extras properly? missing_.emplace_back(detail::Classifier::NONE, item.fullname()); + for(const auto &input : item.inputs) { + missing_.emplace_back(detail::Classifier::NONE, input); + } return false; } @@ -8309,29 +9283,54 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t auto res = config_formatter_->to_flag(item); bool converted{false}; if(op->get_disable_flag_override()) { - - try { - auto val = detail::to_flag_value(res); - if(val == 1) { - res = op->get_flag_value(item.name, "{}"); - converted = true; - } - } catch(...) { + auto val = detail::to_flag_value(res); + if(val == 1) { + res = op->get_flag_value(item.name, "{}"); + converted = true; } } if(!converted) { + errno = 0; res = op->get_flag_value(item.name, res); } op->add_result(res); return true; } - if(static_cast<int>(item.inputs.size()) > op->get_items_expected_max()) { + if(static_cast<int>(item.inputs.size()) > op->get_items_expected_max() && + op->get_multi_option_policy() != MultiOptionPolicy::TakeAll) { if(op->get_items_expected_max() > 1) { throw ArgumentMismatch::AtMost(item.fullname(), op->get_items_expected_max(), item.inputs.size()); } - throw ConversionError::TooManyInputsFlag(item.fullname()); + + if(!op->get_disable_flag_override()) { + throw ConversionError::TooManyInputsFlag(item.fullname()); + } + // if the disable flag override is set then we must have the flag values match a known flag value + // this is true regardless of the output value, so an array input is possible and must be accounted for + for(const auto &res : item.inputs) { + bool valid_value{false}; + if(op->default_flag_values_.empty()) { + if(res == "true" || res == "false" || res == "1" || res == "0") { + valid_value = true; + } + } else { + for(const auto &valid_res : op->default_flag_values_) { + if(valid_res.second == res) { + valid_value = true; + break; + } + } + } + + if(valid_value) { + op->add_result(res); + } else { + throw InvalidError("invalid flag argument given"); + } + } + return true; } } op->add_result(item.inputs); @@ -8366,7 +9365,7 @@ CLI11_INLINE bool App::_parse_single(std::vector<std::string> &args, bool &posit case detail::Classifier::SHORT: case detail::Classifier::WINDOWS_STYLE: // If already parsed a subcommand, don't accept options_ - _parse_arg(args, classifier); + retval = _parse_arg(args, classifier, false); break; case detail::Classifier::NONE: // Probably a positional or something for a parent (sub)command @@ -8408,6 +9407,7 @@ CLI11_NODISCARD CLI11_INLINE bool App::_has_remaining_positionals() const { CLI11_INLINE bool App::_parse_positional(std::vector<std::string> &args, bool haltOnSubcommand) { const std::string &positional = args.back(); + Option *posOpt{nullptr}; if(positionals_at_end_) { // deal with the case of required arguments at the end which should take precedence over other arguments @@ -8424,56 +9424,47 @@ CLI11_INLINE bool App::_parse_positional(std::vector<std::string> &args, bool ha continue; } } - - parse_order_.push_back(opt.get()); - /// if we require a separator add it here - if(opt->get_inject_separator()) { - if(!opt->results().empty() && !opt->results().back().empty()) { - opt->add_result(std::string{}); - } - } - if(opt->get_trigger_on_parse() && - opt->current_option_state_ == Option::option_state::callback_run) { - opt->clear(); - } - opt->add_result(positional); - if(opt->get_trigger_on_parse()) { - opt->run_callback(); - } - args.pop_back(); - return true; + posOpt = opt.get(); + break; } } } } } - for(const Option_p &opt : options_) { - // Eat options, one by one, until done - if(opt->get_positional() && - (static_cast<int>(opt->count()) < opt->get_items_expected_min() || opt->get_allow_extra_args())) { - if(validate_positionals_) { - std::string pos = positional; - pos = opt->_validate(pos, 0); - if(!pos.empty()) { - continue; - } - } - if(opt->get_inject_separator()) { - if(!opt->results().empty() && !opt->results().back().empty()) { - opt->add_result(std::string{}); + if(posOpt == nullptr) { + for(const Option_p &opt : options_) { + // Eat options, one by one, until done + if(opt->get_positional() && + (static_cast<int>(opt->count()) < opt->get_items_expected_min() || opt->get_allow_extra_args())) { + if(validate_positionals_) { + std::string pos = positional; + pos = opt->_validate(pos, 0); + if(!pos.empty()) { + continue; + } } + posOpt = opt.get(); + break; } - if(opt->get_trigger_on_parse() && opt->current_option_state_ == Option::option_state::callback_run) { - opt->clear(); - } - opt->add_result(positional); - if(opt->get_trigger_on_parse()) { - opt->run_callback(); + } + } + if(posOpt != nullptr) { + parse_order_.push_back(posOpt); + if(posOpt->get_inject_separator()) { + if(!posOpt->results().empty() && !posOpt->results().back().empty()) { + posOpt->add_result(std::string{}); } - parse_order_.push_back(opt.get()); - args.pop_back(); - return true; } + if(posOpt->get_trigger_on_parse() && posOpt->current_option_state_ == Option::option_state::callback_run) { + posOpt->clear(); + } + posOpt->add_result(positional); + if(posOpt->get_trigger_on_parse()) { + posOpt->run_callback(); + } + + args.pop_back(); + return true; } for(auto &subc : subcommands_) { @@ -8554,6 +9545,17 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector<std::string> &args) { return true; } auto *com = _find_subcommand(args.back(), true, true); + if(com == nullptr) { + // the main way to get here is using .notation + auto dotloc = args.back().find_first_of('.'); + if(dotloc != std::string::npos) { + com = _find_subcommand(args.back().substr(0, dotloc), true, true); + if(com != nullptr) { + args.back() = args.back().substr(dotloc + 1); + args.push_back(com->get_display_name()); + } + } + } if(com != nullptr) { args.pop_back(); if(!com->silent_) { @@ -8576,7 +9578,8 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector<std::string> &args) { return false; } -CLI11_INLINE bool App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type) { +CLI11_INLINE bool +App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only) { std::string current = args.back(); @@ -8618,7 +9621,7 @@ CLI11_INLINE bool App::_parse_arg(std::vector<std::string> &args, detail::Classi if(op_ptr == std::end(options_)) { for(auto &subc : subcommands_) { if(subc->name_.empty() && !subc->disabled_) { - if(subc->_parse_arg(args, current_type)) { + if(subc->_parse_arg(args, current_type, local_processing_only)) { if(!subc->pre_parse_called_) { subc->_trigger_pre_parse(args.size()); } @@ -8632,9 +9635,57 @@ CLI11_INLINE bool App::_parse_arg(std::vector<std::string> &args, detail::Classi return false; } + // now check for '.' notation of subcommands + auto dotloc = arg_name.find_first_of('.', 1); + if(dotloc != std::string::npos) { + // using dot notation is equivalent to single argument subcommand + auto *sub = _find_subcommand(arg_name.substr(0, dotloc), true, false); + if(sub != nullptr) { + auto v = args.back(); + args.pop_back(); + arg_name = arg_name.substr(dotloc + 1); + if(arg_name.size() > 1) { + args.push_back(std::string("--") + v.substr(dotloc + 3)); + current_type = detail::Classifier::LONG; + } else { + auto nval = v.substr(dotloc + 2); + nval.front() = '-'; + if(nval.size() > 2) { + // '=' not allowed in short form arguments + args.push_back(nval.substr(3)); + nval.resize(2); + } + args.push_back(nval); + current_type = detail::Classifier::SHORT; + } + auto val = sub->_parse_arg(args, current_type, true); + if(val) { + if(!sub->silent_) { + parsed_subcommands_.push_back(sub); + } + // deal with preparsing + increment_parsed(); + _trigger_pre_parse(args.size()); + // run the parse complete callback since the subcommand processing is now complete + if(sub->parse_complete_callback_) { + sub->_process_env(); + sub->_process_callbacks(); + sub->_process_help_flags(); + sub->_process_requirements(); + sub->run_callback(false, true); + } + return true; + } + args.pop_back(); + args.push_back(v); + } + } + if(local_processing_only) { + return false; + } // If a subcommand, try the main command if(parent_ != nullptr && fallthrough_) - return _get_fallthrough_parent()->_parse_arg(args, current_type); + return _get_fallthrough_parent()->_parse_arg(args, current_type, false); // Otherwise, add to missing args.pop_back(); @@ -8756,7 +9807,7 @@ CLI11_INLINE void App::_trigger_pre_parse(std::size_t remaining_args) { } else if(immediate_callback_) { if(!name_.empty()) { auto pcnt = parsed_; - auto extras = std::move(missing_); + missing_t extras = std::move(missing_); clear(); parsed_ = pcnt; pre_parse_called_ = true; @@ -8942,12 +9993,12 @@ CLI11_INLINE void retire_option(App *app, Option *opt) { ->allow_extra_args(opt->get_allow_extra_args()); app->remove_option(opt); - auto *opt2 = app->add_option(option_copy->get_name(false, true), "option has been retired and has no effect") - ->type_name("RETIRED") - ->default_str("RETIRED") - ->type_size(option_copy->get_type_size_min(), option_copy->get_type_size_max()) - ->expected(option_copy->get_expected_min(), option_copy->get_expected_max()) - ->allow_extra_args(option_copy->get_allow_extra_args()); + auto *opt2 = app->add_option(option_copy->get_name(false, true), "option has been retired and has no effect"); + opt2->type_name("RETIRED") + ->default_str("RETIRED") + ->type_size(option_copy->get_type_size_min(), option_copy->get_type_size_max()) + ->expected(option_copy->get_expected_min(), option_copy->get_expected_max()) + ->allow_extra_args(option_copy->get_allow_extra_args()); Validator retired_warning{[opt2](std::string &) { std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n"; @@ -9015,7 +10066,10 @@ CLI11_INLINE std::string help(const App *app, const Error &e) { namespace detail { -std::string convert_arg_for_ini(const std::string &arg, char stringQuote = '"', char characterQuote = '\''); +std::string convert_arg_for_ini(const std::string &arg, + char stringQuote = '"', + char literalQuote = '\'', + bool disable_multi_line = false); /// Comma separated join, adds quotes if needed std::string ini_join(const std::vector<std::string> &args, @@ -9023,7 +10077,9 @@ std::string ini_join(const std::vector<std::string> &args, char arrayStart = '[', char arrayEnd = ']', char stringQuote = '"', - char characterQuote = '\''); + char literalQuote = '\''); + +void clean_name_string(std::string &name, const std::string &keyChars); std::vector<std::string> generate_parents(const std::string §ion, std::string &name, char parentSeparator); @@ -9034,9 +10090,19 @@ void checkParentSegments(std::vector<ConfigItem> &output, const std::string &cur +static constexpr auto multiline_literal_quote = R"(''')"; +static constexpr auto multiline_string_quote = R"(""")"; + namespace detail { -CLI11_INLINE std::string convert_arg_for_ini(const std::string &arg, char stringQuote, char characterQuote) { +CLI11_INLINE bool is_printable(const std::string &test_string) { + return std::all_of(test_string.begin(), test_string.end(), [](char x) { + return (isprint(static_cast<unsigned char>(x)) != 0 || x == '\n' || x == '\t'); + }); +} + +CLI11_INLINE std::string +convert_arg_for_ini(const std::string &arg, char stringQuote, char literalQuote, bool disable_multi_line) { if(arg.empty()) { return std::string(2, stringQuote); } @@ -9049,12 +10115,20 @@ CLI11_INLINE std::string convert_arg_for_ini(const std::string &arg, char string using CLI::detail::lexical_cast; double val = 0.0; if(lexical_cast(arg, val)) { - return arg; + if(arg.find_first_not_of("0123456789.-+eE") == std::string::npos) { + return arg; + } } } // just quote a single non numeric character if(arg.size() == 1) { - return std::string(1, characterQuote) + arg + characterQuote; + if(isprint(static_cast<unsigned char>(arg.front())) == 0) { + return binary_escape_string(arg); + } + if(arg == "'") { + return std::string(1, stringQuote) + "'" + stringQuote; + } + return std::string(1, literalQuote) + arg + literalQuote; } // handle hex, binary or octal arguments if(arg.front() == '0') { @@ -9074,10 +10148,16 @@ CLI11_INLINE std::string convert_arg_for_ini(const std::string &arg, char string } } } - if(arg.find_first_of(stringQuote) == std::string::npos) { - return std::string(1, stringQuote) + arg + stringQuote; + if(!is_printable(arg)) { + return binary_escape_string(arg); } - return characterQuote + arg + characterQuote; + if(detail::has_escapable_character(arg)) { + if(arg.size() > 100 && !disable_multi_line) { + return std::string(multiline_literal_quote) + arg + multiline_literal_quote; + } + return std::string(1, stringQuote) + detail::add_escaped_characters(arg) + stringQuote; + } + return std::string(1, stringQuote) + arg + stringQuote; } CLI11_INLINE std::string ini_join(const std::vector<std::string> &args, @@ -9085,10 +10165,12 @@ CLI11_INLINE std::string ini_join(const std::vector<std::string> &args, char arrayStart, char arrayEnd, char stringQuote, - char characterQuote) { + char literalQuote) { + bool disable_multi_line{false}; std::string joined; if(args.size() > 1 && arrayStart != '\0') { joined.push_back(arrayStart); + disable_multi_line = true; } std::size_t start = 0; for(const auto &arg : args) { @@ -9098,7 +10180,7 @@ CLI11_INLINE std::string ini_join(const std::vector<std::string> &args, joined.push_back(' '); } } - joined.append(convert_arg_for_ini(arg, stringQuote, characterQuote)); + joined.append(convert_arg_for_ini(arg, stringQuote, literalQuote, disable_multi_line)); } if(args.size() > 1 && arrayEnd != '\0') { joined.push_back(arrayEnd); @@ -9111,22 +10193,22 @@ generate_parents(const std::string §ion, std::string &name, char parentSepar std::vector<std::string> parents; if(detail::to_lower(section) != "default") { if(section.find(parentSeparator) != std::string::npos) { - parents = detail::split(section, parentSeparator); + parents = detail::split_up(section, parentSeparator); } else { parents = {section}; } } if(name.find(parentSeparator) != std::string::npos) { - std::vector<std::string> plist = detail::split(name, parentSeparator); + std::vector<std::string> plist = detail::split_up(name, parentSeparator); name = plist.back(); - detail::remove_quotes(name); plist.pop_back(); parents.insert(parents.end(), plist.begin(), plist.end()); } - // clean up quotes on the parents - for(auto &parent : parents) { - detail::remove_quotes(parent); + try { + detail::remove_quotes(parents); + } catch(const std::invalid_argument &iarg) { + throw CLI::ParseError(iarg.what(), CLI::ExitCodes::InvalidError); } return parents; } @@ -9179,30 +10261,59 @@ checkParentSegments(std::vector<ConfigItem> &output, const std::string ¤tS output.back().parents = std::move(parents); output.back().name = "++"; } + +/// @brief checks if a string represents a multiline comment +CLI11_INLINE bool hasMLString(std::string const &fullString, char check) { + if(fullString.length() < 3) { + return false; + } + auto it = fullString.rbegin(); + return (*it == check) && (*(it + 1) == check) && (*(it + 2) == check); +} } // namespace detail inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const { std::string line; + std::string buffer; std::string currentSection = "default"; std::string previousSection = "default"; std::vector<ConfigItem> output; bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ','); bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd; bool inSection{false}; + bool inMLineComment{false}; + bool inMLineValue{false}; + char aStart = (isINIArray) ? '[' : arrayStart; char aEnd = (isINIArray) ? ']' : arrayEnd; char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator; int currentSectionIndex{0}; - while(getline(input, line)) { + + std::string line_sep_chars{parentSeparatorChar, commentChar, valueDelimiter}; + while(getline(input, buffer)) { std::vector<std::string> items_buffer; std::string name; - - detail::trim(line); + line = detail::trim_copy(buffer); std::size_t len = line.length(); // lines have to be at least 3 characters to have any meaning to CLI just skip the rest if(len < 3) { continue; } + if(line.compare(0, 3, multiline_string_quote) == 0 || line.compare(0, 3, multiline_literal_quote) == 0) { + inMLineComment = true; + auto cchar = line.front(); + while(inMLineComment) { + if(getline(input, line)) { + detail::trim(line); + } else { + break; + } + if(detail::hasMLString(line, cchar)) { + inMLineComment = false; + } + } + continue; + } if(line.front() == '[' && line.back() == ']') { if(currentSection != "default") { // insert a section end which is just an empty items_buffer @@ -9234,49 +10345,130 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) { continue; } - + std::size_t search_start = 0; + if(line.find_first_of("\"'`") != std::string::npos) { + while(search_start < line.size()) { + auto test_char = line[search_start]; + if(test_char == '\"' || test_char == '\'' || test_char == '`') { + search_start = detail::close_sequence(line, search_start, line[search_start]); + ++search_start; + } else if(test_char == valueDelimiter || test_char == commentChar) { + --search_start; + break; + } else if(test_char == ' ' || test_char == '\t' || test_char == parentSeparatorChar) { + ++search_start; + } else { + search_start = line.find_first_of(line_sep_chars, search_start); + } + } + } // Find = in string, split and recombine - auto pos = line.find(valueDelimiter); - if(pos != std::string::npos) { - name = detail::trim_copy(line.substr(0, pos)); - std::string item = detail::trim_copy(line.substr(pos + 1)); - auto cloc = item.find(commentChar); - if(cloc != std::string::npos) { - item.erase(cloc, std::string::npos); // NOLINT(readability-suspicious-call-argument) - detail::trim(item); - } - if(item.size() > 1 && item.front() == aStart) { + auto delimiter_pos = line.find_first_of(valueDelimiter, search_start + 1); + auto comment_pos = line.find_first_of(commentChar, search_start); + if(comment_pos < delimiter_pos) { + delimiter_pos = std::string::npos; + } + if(delimiter_pos != std::string::npos) { + + name = detail::trim_copy(line.substr(0, delimiter_pos)); + std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, std::string::npos)); + bool mlquote = + (item.compare(0, 3, multiline_literal_quote) == 0 || item.compare(0, 3, multiline_string_quote) == 0); + if(!mlquote && comment_pos != std::string::npos) { + auto citems = detail::split_up(item, commentChar); + item = detail::trim_copy(citems.front()); + } + if(mlquote) { + // mutliline string + auto keyChar = item.front(); + item = buffer.substr(delimiter_pos + 1, std::string::npos); + detail::ltrim(item); + item.erase(0, 3); + inMLineValue = true; + bool lineExtension{false}; + bool firstLine = true; + if(!item.empty() && item.back() == '\\') { + item.pop_back(); + lineExtension = true; + } + while(inMLineValue) { + std::string l2; + if(!std::getline(input, l2)) { + break; + } + line = l2; + detail::rtrim(line); + if(detail::hasMLString(line, keyChar)) { + line.pop_back(); + line.pop_back(); + line.pop_back(); + if(lineExtension) { + detail::ltrim(line); + } else if(!(firstLine && item.empty())) { + item.push_back('\n'); + } + firstLine = false; + item += line; + inMLineValue = false; + if(!item.empty() && item.back() == '\n') { + item.pop_back(); + } + if(keyChar == '\"') { + try { + item = detail::remove_escaped_characters(item); + } catch(const std::invalid_argument &iarg) { + throw CLI::ParseError(iarg.what(), CLI::ExitCodes::InvalidError); + } + } + } else { + if(lineExtension) { + detail::trim(l2); + } else if(!(firstLine && item.empty())) { + item.push_back('\n'); + } + lineExtension = false; + firstLine = false; + if(!l2.empty() && l2.back() == '\\') { + lineExtension = true; + l2.pop_back(); + } + item += l2; + } + } + items_buffer = {item}; + } else if(item.size() > 1 && item.front() == aStart) { for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) { detail::trim(multiline); item += multiline; } - items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep); + if(item.back() == aEnd) { + items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep); + } else { + items_buffer = detail::split_up(item.substr(1, std::string::npos), aSep); + } } else if((isDefaultArray || isINIArray) && item.find_first_of(aSep) != std::string::npos) { items_buffer = detail::split_up(item, aSep); } else if((isDefaultArray || isINIArray) && item.find_first_of(' ') != std::string::npos) { - items_buffer = detail::split_up(item); + items_buffer = detail::split_up(item, '\0'); } else { items_buffer = {item}; } } else { - name = detail::trim_copy(line); - auto cloc = name.find(commentChar); - if(cloc != std::string::npos) { - name.erase(cloc, std::string::npos); // NOLINT(readability-suspicious-call-argument) - detail::trim(name); - } - + name = detail::trim_copy(line.substr(0, comment_pos)); items_buffer = {"true"}; } - if(name.find(parentSeparatorChar) == std::string::npos) { - detail::remove_quotes(name); - } - // clean up quotes on the items - for(auto &it : items_buffer) { - detail::remove_quotes(it); + std::vector<std::string> parents; + try { + parents = detail::generate_parents(currentSection, name, parentSeparatorChar); + detail::process_quoted_string(name); + // clean up quotes on the items and check for escaped strings + for(auto &it : items_buffer) { + detail::process_quoted_string(it, stringQuote, literalQuote); + } + } catch(const std::invalid_argument &ia) { + throw CLI::ParseError(ia.what(), CLI::ExitCodes::InvalidError); } - std::vector<std::string> parents = detail::generate_parents(currentSection, name, parentSeparatorChar); if(parents.size() > maximumLayers) { continue; } @@ -9313,6 +10505,23 @@ inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) cons return output; } +CLI11_INLINE std::string &clean_name_string(std::string &name, const std::string &keyChars) { + if(name.find_first_of(keyChars) != std::string::npos || (name.front() == '[' && name.back() == ']') || + (name.find_first_of("'`\"\\") != std::string::npos)) { + if(name.find_first_of('\'') == std::string::npos) { + name.insert(0, 1, '\''); + name.push_back('\''); + } else { + if(detail::has_escapable_character(name)) { + name = detail::add_escaped_characters(name); + } + name.insert(0, 1, '\"'); + name.push_back('\"'); + } + } + return name; +} + CLI11_INLINE std::string ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const { std::stringstream out; @@ -9320,6 +10529,18 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, commentLead.push_back(commentChar); commentLead.push_back(' '); + std::string commentTest = "#;"; + commentTest.push_back(commentChar); + commentTest.push_back(parentSeparatorChar); + + std::string keyChars = commentTest; + keyChars.push_back(literalQuote); + keyChars.push_back(stringQuote); + keyChars.push_back(arrayStart); + keyChars.push_back(arrayEnd); + keyChars.push_back(valueDelimiter); + keyChars.push_back(arraySeparator); + std::vector<std::string> groups = app->get_groups(); bool defaultUsed = false; groups.insert(groups.begin(), std::string("Options")); @@ -9345,13 +10566,17 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, continue; } } - std::string name = prefix + opt->get_single_name(); + std::string single_name = opt->get_single_name(); + if(single_name.empty()) { + continue; + } + std::string value = detail::ini_join( - opt->reduced_results(), arraySeparator, arrayStart, arrayEnd, stringQuote, characterQuote); + opt->reduced_results(), arraySeparator, arrayStart, arrayEnd, stringQuote, literalQuote); if(value.empty() && default_also) { if(!opt->get_default_str().empty()) { - value = detail::convert_arg_for_ini(opt->get_default_str(), stringQuote, characterQuote); + value = detail::convert_arg_for_ini(opt->get_default_str(), stringQuote, literalQuote, false); } else if(opt->get_expected_min() == 0) { value = "false"; } else if(opt->get_run_callback_for_default()) { @@ -9360,13 +10585,35 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, } if(!value.empty()) { + if(!opt->get_fnames().empty()) { - value = opt->get_flag_value(name, value); + try { + value = opt->get_flag_value(single_name, value); + } catch(const CLI::ArgumentMismatch &) { + bool valid{false}; + for(const auto &test_name : opt->get_fnames()) { + try { + value = opt->get_flag_value(test_name, value); + single_name = test_name; + valid = true; + } catch(const CLI::ArgumentMismatch &) { + continue; + } + } + if(!valid) { + value = detail::ini_join( + opt->results(), arraySeparator, arrayStart, arrayEnd, stringQuote, literalQuote); + } + } } if(write_description && opt->has_description()) { out << '\n'; out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n'; } + clean_name_string(single_name, keyChars); + + std::string name = prefix + single_name; + out << name << valueDelimiter << value << '\n'; } } @@ -9375,31 +10622,56 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, auto subcommands = app->get_subcommands({}); for(const App *subcom : subcommands) { if(subcom->get_name().empty()) { + if(!default_also && (subcom->count_all() == 0)) { + continue; + } if(write_description && !subcom->get_group().empty()) { out << '\n' << commentLead << subcom->get_group() << " Options\n"; } + /*if (!prefix.empty() || app->get_parent() == nullptr) { + out << '[' << prefix << "___"<< subcom->get_group() << "]\n"; + } else { + std::string subname = app->get_name() + parentSeparatorChar + "___"+subcom->get_group(); + const auto *p = app->get_parent(); + while(p->get_parent() != nullptr) { + subname = p->get_name() + parentSeparatorChar +subname; + p = p->get_parent(); + } + out << '[' << subname << "]\n"; + } + */ out << to_config(subcom, default_also, write_description, prefix); } } for(const App *subcom : subcommands) { if(!subcom->get_name().empty()) { + if(!default_also && (subcom->count_all() == 0)) { + continue; + } + std::string subname = subcom->get_name(); + clean_name_string(subname, keyChars); + if(subcom->get_configurable() && app->got_subcommand(subcom)) { if(!prefix.empty() || app->get_parent() == nullptr) { - out << '[' << prefix << subcom->get_name() << "]\n"; + + out << '[' << prefix << subname << "]\n"; } else { - std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name(); + std::string appname = app->get_name(); + clean_name_string(appname, keyChars); + subname = appname + parentSeparatorChar + subname; const auto *p = app->get_parent(); while(p->get_parent() != nullptr) { - subname = p->get_name() + parentSeparatorChar + subname; + std::string pname = p->get_name(); + clean_name_string(pname, keyChars); + subname = pname + parentSeparatorChar + subname; p = p->get_parent(); } out << '[' << subname << "]\n"; } out << to_config(subcom, default_also, write_description, ""); } else { - out << to_config( - subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar); + out << to_config(subcom, default_also, write_description, prefix + subname + parentSeparatorChar); } } } @@ -9463,7 +10735,7 @@ CLI11_INLINE std::string Formatter::make_description(const App *app) const { auto min_options = app->get_require_option_min(); auto max_options = app->get_require_option_max(); if(app->get_required()) { - desc += " REQUIRED "; + desc += " " + get_label("REQUIRED") + " "; } if((max_options == min_options) && (min_options > 0)) { if(min_options == 1) { @@ -9485,6 +10757,11 @@ CLI11_INLINE std::string Formatter::make_description(const App *app) const { } CLI11_INLINE std::string Formatter::make_usage(const App *app, std::string name) const { + std::string usage = app->get_usage(); + if(!usage.empty()) { + return usage + "\n"; + } + std::stringstream out; out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name; @@ -9521,7 +10798,7 @@ CLI11_INLINE std::string Formatter::make_usage(const App *app, std::string name) << (app->get_require_subcommand_min() == 0 ? "]" : ""); } - out << std::endl; + out << '\n'; return out.str(); } @@ -9602,7 +10879,10 @@ CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMo CLI11_INLINE std::string Formatter::make_subcommand(const App *sub) const { std::stringstream out; - detail::format_help(out, sub->get_display_name(true), sub->get_description(), column_width_); + detail::format_help(out, + sub->get_display_name(true) + (sub->get_required() ? " " + get_label("REQUIRED") : ""), + sub->get_description(), + column_width_); return out.str(); } |
