diff --git a/.circleci/config.yml b/.circleci/config.yml index dd9c3a9799..206d11dc6c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -103,7 +103,7 @@ jobs: docker: - image: helics/buildenv:sanitizers-14 environment: - CMAKE_FLAGS: '-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_FLAGS="-fsanitize=undefined,address -lc++ -lc++abi -fsanitize-address-use-after-scope -fsanitize-ignorelist=/root/project/.circleci/asan_suppression.txt -Wno-unused-command-line-argument -fno-omit-frame-pointer -g -O1 -fsanitize-blacklist=/root/project/.circleci/asan_blacklist.txt" -DHELICS_BUILD_TESTS=ON -DHELICS_ZMQ_SUBPROJECT=ON -DHELICS_ZMQ_FORCE_SUBPROJECT=ON' + CMAKE_FLAGS: '-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_FLAGS="-fsanitize=undefined,address -lc++ -lc++abi -fsanitize-address-use-after-scope -fsanitize-ignorelist=/root/project/.circleci/asan_suppression.txt -Wno-reserved-macro-identifier -Wno-unused-command-line-argument -fno-omit-frame-pointer -g -O1 -fsanitize-blacklist=/root/project/.circleci/asan_blacklist.txt" -DHELICS_BUILD_TESTS=ON -DHELICS_ZMQ_SUBPROJECT=ON -DHELICS_ZMQ_FORCE_SUBPROJECT=ON' LSAN_OPTIONS: "suppressions=/root/project/.circleci/leak_suppression.txt" UBSAN_OPTIONS: "print_stacktrace=1 suppressions=/root/project/.circleci/ubsan_suppression.txt" ASAN_OPTIONS: "alloc_dealloc_mismatch=0" @@ -116,7 +116,7 @@ jobs: docker: - image: helics/buildenv:sanitizers-14 environment: - CMAKE_FLAGS: '-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_FLAGS="-fsanitize=memory -nostdinc++ -nostdlib++ -L/root/develop/libcxx_msan/lib -lc++ -lc++abi -I/root/develop/libcxx_msan/include -I/root/develop/libcxx_msan/include/c++/v1 -Wno-unused-command-line-argument -fno-omit-frame-pointer -g -O1 -Wl,-rpath,/root/develop/libcxx_msan/lib" -DHELICS_BUILD_TESTS=ON -DHELICS_ZMQ_SUBPROJECT=ON -DHELICS_ZMQ_FORCE_SUBPROJECT=ON' + CMAKE_FLAGS: '-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_FLAGS="-fsanitize=memory -nostdinc++ -nostdlib++ -Wno-reserved-macro-identifier -L/root/develop/libcxx_msan/lib -lc++ -lc++abi -I/root/develop/libcxx_msan/include -I/root/develop/libcxx_msan/include/c++/v1 -Wno-unused-command-line-argument -fno-omit-frame-pointer -g -O1 -Wl,-rpath,/root/develop/libcxx_msan/lib" -DHELICS_BUILD_TESTS=ON -DHELICS_ZMQ_SUBPROJECT=ON -DHELICS_ZMQ_FORCE_SUBPROJECT=ON' steps: - checkout diff --git a/.github/workflows/swig-interface-gen.yml b/.github/workflows/swig-interface-gen.yml index 7176e6bd09..7d5e0603dd 100644 --- a/.github/workflows/swig-interface-gen.yml +++ b/.github/workflows/swig-interface-gen.yml @@ -36,7 +36,7 @@ jobs: # Run clang-format on helics.h and helics_api.h - name: clang-format - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 # Failure is expected, since we know it will be fixing up the generated helics header files continue-on-error: true with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adefeb257c..1d5582acf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: dockerfile_lint args: [--rulefile, ./config/Docker/docker_rules.yml, --dockerfile] - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.3.0 hooks: - id: black args: ["--line-length=100"] @@ -18,7 +18,7 @@ repos: - id: blacken-docs additional_dependencies: [black==22.12] - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.4 + rev: v1.5.5 hooks: - id: remove-tabs - repo: https://github.com/pre-commit/pre-commit-hooks @@ -52,7 +52,7 @@ repos: exclude: "mac.md" - id: script-must-have-extension - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.9.0.6 + rev: v0.10.0.1 hooks: - id: shellcheck args: [-x] @@ -74,7 +74,7 @@ repos: "--exclude-file=./config/spelling_ignorelines.txt", ] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v17.0.6 + rev: v18.1.1 hooks: - id: clang-format types: diff --git a/CHANGELOG.md b/CHANGELOG.md index b78f6e11cc..9268c13809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm A note on future revisions. Everything within a major version number should be code compatible (with the exception of experimental interfaces). The most notable example of an experimental interface is the support for multiple source inputs. The APIs to deal with this will change in future minor releases. Everything within a single minor release should be network compatible with other federates on the same minor release number. Compatibility across minor release numbers may be possible in some situations but we are not going to guarantee this as those components are subject to performance improvements and may need to be modified at some point. Patch releases will be limited to bug fixes and other improvements not impacting the public API or network compatibility. Check the [Public API](./docs/Public_API.md) for details on what is included and excluded from the public API and version stability. +## [3.5.1][] - 2024-03-19 + +Patch release including beta version of reentrant federates and support for "potential_interfaces" section in config files and automatic handling of potential interface generation in the federate class for operation with the connector app. + +### Fixed + +- Fixed an issue related to disconnection of federates with endpoints while still executing, that could potentially have resulted in an infinite loop +- Fixed an issue with aliases potentially causing a seg fault or memory condition, now aliases have reciprocity so order in the given alias call doesn't matter + +### Changed + +- Updated 3rd party libraries including cli11, fmt, spdlog, and asio + +### Added + +- Automatic handling of potential interface sequence from Federate class +- Support for potential_interface templates +- Support for reentrant federates: federates with the same name that can come and go, and support for reconnecting interfaces to the reentrant federate +- Support for a "helics" section in the configuration json files + ## [3.5.0][] - 2024-02-05 Major release including connector app capability, a refresh of the of the dataBuffer interface, and other bug fixes @@ -414,3 +434,5 @@ HELICS 3.0 is a major update to HELICS. The major features that have been added [3.3.1]: https://github.com/GMLC-TDC/HELICS/releases/tag/v3.3.1 [3.3.2]: https://github.com/GMLC-TDC/HELICS/releases/tag/v3.3.2 [3.4.0]: https://github.com/GMLC-TDC/HELICS/releases/tag/v3.4.0 +[3.5.0]: https://github.com/GMLC-TDC/HELICS/releases/tag/v3.5.0 +[3.5.1]: https://github.com/GMLC-TDC/HELICS/releases/tag/v3.5.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index d48f3d5569..1370de59ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,14 +17,14 @@ if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE AND NOT HELICS_D set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") endif() -project(HELICS VERSION 3.5.0) +project(HELICS VERSION 3.5.1) # ----------------------------------------------------------------------------- # HELICS Version number # ----------------------------------------------------------------------------- set(HELICS_VERSION_BUILD) # use ISO date YYYY-MM-DD -set(HELICS_DATE "2024-02-06") +set(HELICS_DATE "2024-03-19") set(HELICS_VERSION_UNDERSCORE "${HELICS_VERSION_MAJOR}_${HELICS_VERSION_MINOR}_${HELICS_VERSION_PATCH}" @@ -687,7 +687,7 @@ set(LICENSE_LIST "cppzmq" "${PROJECT_SOURCE_DIR}/ThirdParty/cppzmq/LICENSE" "fmt" - "${PROJECT_SOURCE_DIR}/ThirdParty/fmtlib/LICENSE.rst" + "${PROJECT_SOURCE_DIR}/ThirdParty/fmtlib/LICENSE" "units" "${PROJECT_SOURCE_DIR}/ThirdParty/units/LICENSE" "JsonCpp" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4e129ad354..4da2590bc3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,7 +11,7 @@ If you would like to contribute to the HELICS project see [CONTRIBUTING](CONTRIB - [Andy Fisher](https://github.com/afisher1) - [Jason Fuller](https://github.com/jcfuller1) - [Shwetha Niddodi](https://github.com/shwethanidd) -- [Trevor Hardy](https://energyenvironment.pnnl.gov/staff/staff_info.asp?staff_num=2704) +- [Trevor Hardy](https://www.pnnl.gov/people/trevor-hardy) - [Monish Mukherjee](https://github.com/MuMonish) - [Jacob Hansen](https://github.com/Jacobhansens) - [Marc Eberlein](https://github.com/eberleim) diff --git a/README.md b/README.md index 315c478db3..d615039da0 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,26 @@ A multi-language, cross-platform library that enables different simulators to ea ## Table of contents +- [Table of contents](#table-of-contents) - [Introduction](#introduction) - - [Philosophy](#philosophy-of-helics) + - [Philosophy of HELICS](#philosophy-of-helics) - [Getting Started](#getting-started) - [Language Bindings](#language-bindings) - [Documentation](#documentation) - - [Changelog](#changelog) - - [RoadMap](#roadmap) + - [Documentation downloads](#documentation-downloads) + - [CHANGELOG](#changelog) + - [ROADMAP](#roadmap) - [Installation](#installation) - [Quick links](#quick-links) + - [Docker](#docker) - [Tools with HELICS support](#tools-with-helics-support) + - [HELICS helper Apps](#helics-helper-apps) - [Contributing](#contributing) - [Build Status](#build-status) - [Publications](#publications) - [In the News](#in-the-news) - [History and Motivation](#history-and-motivation) +- [Source Repo](#source-repo) - [Release](#release) ## Introduction @@ -172,6 +177,10 @@ Contributors are welcome, see the [Contributing](CONTRIBUTING.md) guidelines for If you use HELICS in your research, please cite: +T. Hardy, B. Palmintier, P. Top, D. Krishnamurthy and J. Fuller, "HELICS: A Co-Simulation Framework for Scalable Multi-Domain Modeling and Analysis," in IEEE Access, doi: 10.1109/ACCESS.2024.3363615, available at [https://ieeexplore.ieee.org/document/10424422](https://ieeexplore.ieee.org/document/10424422) + +Older citation + \[1\] B. Palmintier, D. Krishnamurthy, P. Top, S. Smith, J. Daily, and J. Fuller, “Design of the HELICS High-Performance Transmission-Distribution-Communication-Market Co-Simulation Framework,” in _Proc. of the 2017 Workshop on Modeling and Simulation of Cyber-Physical Energy Systems_, Pittsburgh, PA, 2017. [pre-print](https://www.nrel.gov/docs/fy17osti/67928.pdf) | [published](https://ieeexplore.ieee.org/document/8064542/) ## In the News diff --git a/ThirdParty/asio b/ThirdParty/asio index 1f8d154829..814f67e730 160000 --- a/ThirdParty/asio +++ b/ThirdParty/asio @@ -1 +1 @@ -Subproject commit 1f8d154829b902dbc45a651587c6c6df948358e8 +Subproject commit 814f67e730e154547aea3f4d99f709cbdf1ea4a0 diff --git a/ThirdParty/fmtlib b/ThirdParty/fmtlib index f5e54359df..e69e5f977d 160000 --- a/ThirdParty/fmtlib +++ b/ThirdParty/fmtlib @@ -1 +1 @@ -Subproject commit f5e54359df4c26b6230fc61d38aa294581393084 +Subproject commit e69e5f977d458f2650bb346dadf2ad30c5320281 diff --git a/ThirdParty/helics/external/CLI11/CLI11.hpp b/ThirdParty/helics/external/CLI11/CLI11.hpp index e5b4e823c8..ce1875e3a7 100644 --- a/ThirdParty/helics/external/CLI11/CLI11.hpp +++ b/ThirdParty/helics/external/CLI11/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-8-ga88dd9b +// from: v2.4.0-14-g5f9141d // -// CLI11 2.3.2 Copyright (c) 2017-2023 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 @@ -65,9 +65,9 @@ #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" @@ -225,20 +225,28 @@ #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 -//first #include -//second +#undef NOMINMAX +#else +#include +#endif + +// second #include -//third +// third #include #include - -#undef NOMINMAX -#elif defined(__APPLE__) -#include #endif @@ -395,138 +403,42 @@ CLI11_INLINE std::filesystem::path to_path(std::string_view str) { -/// argc as passed in to this executable. -CLI11_INLINE int argc(); - -/// argv as passed in to this executable, converted to utf-8 on Windows. -CLI11_INLINE const char *const *argv(); - - - - namespace detail { - -#ifdef __APPLE__ -// Copy argc and argv as early as possible to avoid modification -static const std::vector static_args = [] { - static const std::vector static_args_as_strings = [] { - std::vector args_as_strings; - int argc = *_NSGetArgc(); - char **argv = *_NSGetArgv(); - - args_as_strings.reserve(static_cast(argc)); - for(size_t i = 0; i < static_cast(argc); i++) { - args_as_strings.push_back(argv[i]); - } - - return args_as_strings; - }(); - - std::vector static_args_result; - static_args_result.reserve(static_args_as_strings.size()); - - for(const auto &arg : static_args_as_strings) { - static_args_result.push_back(arg.data()); - } - - return static_args_result; -}(); -#endif - -/// Command-line arguments, as passed in to this executable, converted to utf-8 on Windows. -CLI11_INLINE const std::vector &args() { - // This function uses initialization via lambdas extensively to take advantage of the thread safety of static - // variable initialization [stmt.dcl.3] - #ifdef _WIN32 - static const std::vector static_args = [] { - static const std::vector static_args_as_strings = [] { - // On Windows, take arguments from GetCommandLineW and convert them to utf-8. - std::vector args_as_strings; - int argc = 0; - - auto deleter = [](wchar_t **ptr) { LocalFree(ptr); }; - // NOLINTBEGIN(*-avoid-c-arrays) - auto wargv = - std::unique_ptr(CommandLineToArgvW(GetCommandLineW(), &argc), deleter); - // NOLINTEND(*-avoid-c-arrays) - - if(wargv == nullptr) { - throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError())); - } - - args_as_strings.reserve(static_cast(argc)); - for(size_t i = 0; i < static_cast(argc); ++i) { - args_as_strings.push_back(narrow(wargv[i])); - } - - return args_as_strings; - }(); - - std::vector static_args_result; - static_args_result.reserve(static_args_as_strings.size()); - - for(const auto &arg : static_args_as_strings) { - static_args_result.push_back(arg.data()); - } - - return static_args_result; - }(); - - return static_args; - -#elif defined(__APPLE__) - - return static_args; +/// Decode and return UTF-8 argv from GetCommandLineW. +CLI11_INLINE std::vector compute_win32_argv(); +#endif +} // namespace detail -#else - static const std::vector static_args = [] { - static const std::vector static_cmdline = [] { - // On posix, retrieve arguments from /proc/self/cmdline, separated by null terminators. - std::vector cmdline; - auto deleter = [](FILE *f) { std::fclose(f); }; - std::unique_ptr fp_unique(std::fopen("/proc/self/cmdline", "r"), deleter); - FILE *fp = fp_unique.get(); - if(!fp) { - throw std::runtime_error("could not open /proc/self/cmdline for reading"); // LCOV_EXCL_LINE - } - size_t size = 0; - while(std::feof(fp) == 0) { - cmdline.resize(size + 128); - size += std::fread(cmdline.data() + size, 1, 128, fp); - - if(std::ferror(fp) != 0) { - throw std::runtime_error("error during reading /proc/self/cmdline"); // LCOV_EXCL_LINE - } - } - cmdline.resize(size); +namespace detail { - return cmdline; - }(); +#ifdef _WIN32 +CLI11_INLINE std::vector compute_win32_argv() { + std::vector result; + int argc = 0; - std::size_t argc = static_cast(std::count(static_cmdline.begin(), static_cmdline.end(), '\0')); - std::vector static_args_result; - static_args_result.reserve(argc); + auto deleter = [](wchar_t **ptr) { LocalFree(ptr); }; + // NOLINTBEGIN(*-avoid-c-arrays) + auto wargv = std::unique_ptr(CommandLineToArgvW(GetCommandLineW(), &argc), deleter); + // NOLINTEND(*-avoid-c-arrays) - for(auto it = static_cmdline.begin(); it != static_cmdline.end(); - it = std::find(it, static_cmdline.end(), '\0') + 1) { - static_args_result.push_back(static_cmdline.data() + (it - static_cmdline.begin())); - } + if(wargv == nullptr) { + throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError())); + } - return static_args_result; - }(); + result.reserve(static_cast(argc)); + for(size_t i = 0; i < static_cast(argc); ++i) { + result.push_back(narrow(wargv[i])); + } - return static_args; -#endif + return result; } +#endif } // namespace detail -CLI11_INLINE const char *const *argv() { return detail::args().data(); } -CLI11_INLINE int argc() { return static_cast(detail::args().size()); } - @@ -627,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 &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 /// @@ -647,14 +562,16 @@ CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector bool valid_first_char(T c) { return ((c != '-') && (c != '!') && (c != ' ') && c != '\n'); } +template bool valid_first_char(T c) { + return ((c != '-') && (static_cast(c) > 33)); // space and '!' not allowed +} /// Verify following characters of an option template 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(c) > 32) || c == '\t')); } /// Verify an option/subcommand name @@ -717,18 +634,46 @@ template 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 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 @@ -778,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); @@ -898,37 +853,220 @@ find_member(std::string name, const std::vector 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(hcode); +} + +CLI11_INLINE char make_char(std::uint32_t code) { return static_cast(static_cast(code)); } + +CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) { + if(code < 0x80) { // ascii code equivalent + str.push_back(static_cast(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 split_up(std::string str, char delimiter) { - const std::string delims("\'\"`"); auto find_ws = [delimiter](char ch) { return (delimiter == '\0') ? std::isspace(ch, std::locale()) : (ch == delimiter); }; trim(str); std::vector 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)) { @@ -937,14 +1075,9 @@ CLI11_INLINE std::vector 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; @@ -962,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(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(static_cast(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(res1 * 16 + res2)); + continue; + } + } + outstring.push_back(escaped_string[loc]); + ++loc; + } + return outstring; +} + +CLI11_INLINE void remove_quotes(std::vector &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 @@ -1079,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); } @@ -1444,11 +1708,19 @@ template 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()} #ifdef __CUDACC__ +#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ +#pragma nv_diag_default 2361 +#else #pragma diag_default 2361 +#endif #endif , std::is_move_assignable()); @@ -1884,12 +2156,23 @@ template struct classify_object struct classify_object::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::value && !std::is_constructible::value +#define STRING_CHECK true +#else +#define WIDE_STRING_CHECK true +#define STRING_CHECK !std::is_assignable::value && !std::is_constructible::value +#endif /// String and similar direct assignment template -struct classify_object::value && !std::is_integral::value && - std::is_assignable::value>::type> { +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && WIDE_STRING_CHECK && + std::is_assignable::value>::type> { static constexpr object_category value{object_category::string_assignable}; }; @@ -1899,7 +2182,7 @@ struct classify_object< T, typename std::enable_if::value && !std::is_integral::value && !std::is_assignable::value && (type_count::value == 1) && - std::is_constructible::value>::type> { + WIDE_STRING_CHECK && std::is_constructible::value>::type> { static constexpr object_category value{object_category::string_constructible}; }; @@ -1907,9 +2190,7 @@ struct classify_object< template struct classify_object::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && - std::is_assignable::value>::type> { + STRING_CHECK && std::is_assignable::value>::type> { static constexpr object_category value{object_category::wstring_assignable}; }; @@ -1917,10 +2198,8 @@ template struct classify_object< T, typename std::enable_if::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !std::is_assignable::value && (type_count::value == 1) && - std::is_constructible::value>::type> { + STRING_CHECK && std::is_constructible::value>::type> { static constexpr object_category value{object_category::wstring_constructible}; }; @@ -2131,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) { @@ -2147,6 +2426,33 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = (output_sll < 0) ? static_cast(0) : static_cast(output_sll); return (static_cast(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(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(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(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + } return false; } @@ -2171,11 +2477,38 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = static_cast(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(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(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(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(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) { @@ -2203,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; } @@ -2212,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; } @@ -2241,18 +2579,16 @@ bool lexical_cast(const std::string &input, T &output) { template ::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 @@ -2265,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(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 @@ -2516,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; @@ -2573,12 +2927,12 @@ template = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { // the remove const is to handle pair types coming from a container - typename std::remove_const::type>::type v1; - typename std::tuple_element<1, ConvertTo>::type v2; - bool retval = lexical_assign(strings[0], v1); - if(strings.size() > 1) { - retval = retval && lexical_assign(strings[1], v2); - } + using FirstType = typename std::remove_const::type>::type; + using SecondType = typename std::tuple_element<1, ConvertTo>::type; + FirstType v1; + SecondType v2; + bool retval = lexical_assign(strings[0], v1); + retval = retval && lexical_assign((strings.size() > 1) ? strings[1] : std::string{}, v2); if(retval) { output = AssignTo{v1, v2}; } @@ -2593,6 +2947,9 @@ template = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { output.erase(output.begin(), output.end()); + if(strings.empty()) { + return true; + } if(strings.size() == 1 && strings[0] == "{}") { return true; } @@ -2737,7 +3094,7 @@ tuple_type_conversion(std::vector &strings, AssignTo &output) { std::size_t index{subtype_count_min::value}; const std::size_t mx_count{subtype_count::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])) { @@ -2747,7 +3104,11 @@ tuple_type_conversion(std::vector &strings, AssignTo &output) { } bool retval = lexical_conversion( std::vector(strings.begin(), strings.begin() + static_cast(index)), output); - strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); + if(strings.size() > index) { + strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); + } else { + strings.clear(); + } return retval; } @@ -2891,12 +3252,13 @@ inline std::string sum_string_vector(const std::vector &values) { double tv{0.0}; auto comp = lexical_cast(arg, tv); if(!comp) { - try { - tv = static_cast(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(fv); } val += tv; } @@ -2905,13 +3267,10 @@ inline std::string sum_string_vector(const std::vector &values) { output.append(arg); } } else { - if(val <= static_cast((std::numeric_limits::min)()) || - val >= static_cast((std::numeric_limits::max)()) || - std::ceil(val) == std::floor(val)) { - output = detail::value_string(static_cast(val)); - } else { - output = detail::value_string(val); - } + std::ostringstream out; + out.precision(16); + out << val; + output = out.str(); } return output; } @@ -3029,7 +3388,6 @@ get_names(const std::vector &input) { std::vector short_names; std::vector long_names; std::string pos_name; - for(std::string name : input) { if(name.length() == 0) { continue; @@ -3037,6 +3395,8 @@ get_names(const std::vector &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) == "--") { @@ -3048,12 +3408,15 @@ get_names(const std::vector &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); } @@ -3070,7 +3433,6 @@ struct ConfigItem { /// This is the name std::string name{}; - /// Listing of inputs std::vector inputs{}; @@ -3133,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 @@ -3170,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 @@ -3405,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. @@ -3424,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 class TypeValidator : public Validator { public: @@ -4183,7 +4553,7 @@ CLI11_INLINE path_type check_path(const char *file) noexcept { 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: @@ -4277,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) @@ -4579,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 @@ -5087,12 +5477,12 @@ class Option : public OptionBase