diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e465ab..291787a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,25 +25,19 @@ cmake_minimum_required(VERSION 3.16) # set the project name -project(mcfp VERSION 1.3.0 LANGUAGES CXX) - -list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) +project(libmcfp VERSION 1.2.4 LANGUAGES CXX) include(GNUInstallDirs) include(CMakePackageConfigHelpers) -include(CTest) +include(Dart) set(CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 17 CACHE STRING "The minimum version of C++ required for this library") set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(BUILD_DOCUMENTATION "Build the documentation" OFF) +option(ENABLE_TESTING "Build the unit test applications" OFF) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - if("${CMAKE_CXX_COMPILER_VERSION}" LESS 9.4) - message(FATAL_ERROR "Your GNU g++ is too old, need at least 9.4") - endif() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") elseif(MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") @@ -70,46 +64,78 @@ if(MSVC) add_definitions(-DNOMINMAX) endif() -add_library(mcfp INTERFACE) -add_library(mcfp::mcfp ALIAS mcfp) +add_library(libmcfp INTERFACE) +add_library(libmcfp::libmcfp ALIAS libmcfp) -target_compile_features(mcfp INTERFACE cxx_std_20) +target_include_directories(libmcfp INTERFACE + $ + $ +) # adding header sources just helps IDEs -target_sources(mcfp PUBLIC - FILE_SET mcfp_headers TYPE HEADERS - BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include - FILES - include/mcfp/detail/charconv.hpp - include/mcfp/detail/options.hpp - include/mcfp/error.hpp - include/mcfp/mcfp.hpp - include/mcfp/text.hpp - include/mcfp/utilities.hpp +target_sources(libmcfp INTERFACE + $$/mcfp/mcfp.hpp ) +set_target_properties(libmcfp PROPERTIES PUBLIC_HEADER include/mcfp/mcfp.hpp) + # installation -install(TARGETS mcfp - EXPORT mcfp - FILE_SET mcfp_headers - DESTINATION include/) +set(version_config "${CMAKE_CURRENT_BINARY_DIR}/libmcfpConfigVersion.cmake") -install(EXPORT mcfp - NAMESPACE mcfp:: - DESTINATION lib/cmake/mcfp - FILE "mcfp-targets.cmake") +set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}) -configure_file(${PROJECT_SOURCE_DIR}/cmake/mcfp-config.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/mcfp-config.cmake @ONLY) +write_basic_package_version_file("${version_config}" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/mcfp-config.cmake" - DESTINATION "lib/cmake/mcfp") +install(TARGETS libmcfp + EXPORT libmcfpConfig + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -if(BUILD_TESTING) - add_subdirectory(test) -endif() +install( + DIRECTORY include/mcfp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + COMPONENT Devel +) + +export(TARGETS libmcfp NAMESPACE libmcfp:: FILE libmcfpTargets.cmake) -if(BUILD_DOCUMENTATION) - add_subdirectory(docs) +if(WIN32 AND NOT CYGWIN) + set(CONFIG_LOC CMake) +else() + set(CONFIG_LOC "${CMAKE_INSTALL_LIBDIR}/cmake/libmcfp") endif() +configure_package_config_file( + ${PROJECT_SOURCE_DIR}/cmake/libmcfpConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/libmcfpConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libmcfp + PATH_VARS INCLUDE_INSTALL_DIR +) + +install(EXPORT libmcfpConfig + FILE libmcfpTargets.cmake + NAMESPACE libmcfp:: + DESTINATION ${CONFIG_LOC}) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/libmcfpConfig.cmake "${version_config}" + DESTINATION ${CONFIG_LOC}) + +if(ENABLE_TESTING) + enable_testing() + + find_package(Boost REQUIRED) + + add_executable(libmcfp-unit-test ${PROJECT_SOURCE_DIR}/test/unit-test.cpp) + + target_link_libraries(libmcfp-unit-test libmcfp::libmcfp Boost::boost) + + if(MSVC) + # Specify unwind semantics so that MSVC knowns how to handle exceptions + target_compile_options(libmcfp-unit-test PRIVATE /EHsc) + endif() + + add_test(NAME libmcfp-unit-test + COMMAND $ -- ${PROJECT_SOURCE_DIR}/test) +endif() diff --git a/README.md b/README.md index 9cf77e2..50176f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -[![github CI](https://github.com/mhekkel/libmcfp/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/mhekkel/libmcfp/actions) -[![github CI](https://github.com/mhekkel/libmcfp/actions/workflows/build-documentation.yml/badge.svg)](https://github.com/mhekkel/libmcfp/actions) - # libmcfp A library for parsing command line arguments and configuration files and making them available throughout a program. @@ -132,3 +129,4 @@ cmake .. cmake --build . cmake --install . ``` + diff --git a/changelog b/changelog index 1d9c7d0..3f9a787 100644 --- a/changelog +++ b/changelog @@ -1,10 +1,3 @@ -Version 1.3.0 -- Config file syntax improvements (better conforming to ini file syntax) -- Better cmake file - -Version 1.2.5 -- Replace test for _MSC_VER with more generic _WIN32 - Version 1.2.4 - Simpler get (added a version without template arguments) diff --git a/cmake/libmcfpConfig.cmake.in b/cmake/libmcfpConfig.cmake.in new file mode 100644 index 0000000..f37b546 --- /dev/null +++ b/cmake/libmcfpConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +INCLUDE("${CMAKE_CURRENT_LIST_DIR}/libmcfpTargets.cmake") + +check_required_components(libmcfp) diff --git a/cmake/mcfp-config.cmake.in b/cmake/mcfp-config.cmake.in deleted file mode 100644 index e1288ca..0000000 --- a/cmake/mcfp-config.cmake.in +++ /dev/null @@ -1,5 +0,0 @@ -@PACKAGE_INIT@ - -INCLUDE("${CMAKE_CURRENT_LIST_DIR}/mcfp-targets.cmake") - -check_required_components(mcfp) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ba3c04a..f8e2ddf 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,7 +6,7 @@ set(CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 17 CACHE STRING "The minimum version of C++ required for this library") set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(mcfp REQUIRED) +find_package(libmcfp REQUIRED) add_executable(example example.cpp) -target_link_libraries(example mcfp::mcfp) +target_link_libraries(example libmcfp::libmcfp) diff --git a/include/mcfp/mcfp.hpp b/include/mcfp/mcfp.hpp index ad63bad..5dec155 100644 --- a/include/mcfp/mcfp.hpp +++ b/include/mcfp/mcfp.hpp @@ -26,9 +26,9 @@ #pragma once -/// \file +/// \file mcfp.hpp /// This header-only library contains code to parse argc/argv and store the -/// values provided into a singleton object. +/// values contained therein into a singleton object. #include #include @@ -43,67 +43,406 @@ #include #include -#include #include #include -#include namespace mcfp { +// we use the new system_error stuff. + +enum class config_error +{ + unknown_option = 1, + option_does_not_accept_argument, + missing_argument_for_option, + option_not_specified, + invalid_config_file, + wrong_type_cast, + config_file_not_found +}; + +class config_category_impl : public std::error_category +{ + public: + const char *name() const noexcept override + { + return "configuration"; + } + + std::string message(int ev) const override + { + switch (static_cast(ev)) + { + case config_error::unknown_option: + return "unknown option"; + case config_error::option_does_not_accept_argument: + return "option does not accept argument"; + case config_error::missing_argument_for_option: + return "missing argument for option"; + case config_error::option_not_specified: + return "option was not specified"; + case config_error::invalid_config_file: + return "config file contains a syntax error"; + case config_error::wrong_type_cast: + return "the implementation contains a type cast error"; + case config_error::config_file_not_found: + return "the specified config file was not found"; + default: + assert(false); + return "unknown error code"; + } + } + + bool equivalent(const std::error_code &/*code*/, int /*condition*/) const noexcept override + { + return false; + } +}; + +inline std::error_category &config_category() +{ + static config_category_impl instance; + return instance; +} + +inline std::error_code make_error_code(config_error e) +{ + return std::error_code(static_cast(e), config_category()); +} + +inline std::error_condition make_error_condition(config_error e) +{ + return std::error_condition(static_cast(e), config_category()); +} + // -------------------------------------------------------------------- -/** - * @brief A singleton class. Use @ref mcfp::config::instance to create and/or - * retrieve the single instance - * - */ +// Some template wizardry to detect containers, needed to have special +// handling of options that can be repeated. -class config +template +using iterator_t = typename T::iterator; + +template +using value_type_t = typename T::value_type; + +template +using std_string_npos_t = decltype(T::npos); + +template +struct is_container_type : std::false_type { - using option_base = detail::option_base; +}; + +template +struct is_container_type and + is_detected_v and + not is_detected_v>> : std::true_type +{ +}; + +template +inline constexpr bool is_container_type_v = is_container_type::value; + +// -------------------------------------------------------------------- +// The options classes + +namespace detail +{ + // The option traits classes are used to convert from the string-based + // command line argument to the type that should be stored. + // In fact, here is where the command line arguments are checked for + // proper formatting. + template + struct option_traits; + + template + struct option_traits>> + { + using value_type = T; + + static value_type set_value(std::string_view argument, std::error_code &ec) + { + value_type value{}; + auto r = charconv::from_chars(argument.data(), argument.data() + argument.length(), value); + if (r.ec != std::errc()) + ec = std::make_error_code(r.ec); + return value; + } + + static std::string to_string(const T &value) + { + char b[32]; + auto r = charconv::to_chars(b, b + sizeof(b), value); + if (r.ec != std::errc()) + throw std::system_error(std::make_error_code(r.ec)); + return { b, r.ptr }; + } + }; + + template <> + struct option_traits + { + using value_type = std::filesystem::path; + + static value_type set_value(std::string_view argument, std::error_code &/*ec*/) + { + return value_type{ argument }; + } + + static std::string to_string(const std::filesystem::path &value) + { + return value.string(); + } + }; + + template + struct option_traits and std::is_assignable_v>> + { + using value_type = std::string; + + static value_type set_value(std::string_view argument, std::error_code &/*ec*/) + { + return value_type{ argument }; + } + + static std::string to_string(const T &value) + { + return { value }; + } + }; + + // The Options. The reason to have this weird constructing of + // polymorphic options based on templates is to have a very + // simple interface. The disadvantage is that the options have + // to be copied during the construction of the config object. + + struct option_base + { + std::string m_name; ///< The long argument name + std::string m_desc; ///< The description of the argument + char m_short_name; ///< The single character name of the argument, can be zero + bool m_is_flag = true, ///< When true, this option does not allow arguments + m_has_default = false, ///< When true, this option has a default value. + m_multi = false, ///< When true, this option allows mulitple values. + m_hidden; ///< When true, this option is hidden from the help text + int m_seen = 0; ///< How often the option was seen on the command line + + option_base(const option_base &rhs) = default; + + option_base(std::string_view name, std::string_view desc, bool hidden) + : m_name(name) + , m_desc(desc) + , m_short_name(0) + , m_hidden(hidden) + { + if (m_name.length() == 1) + m_short_name = m_name.front(); + else if (m_name.length() > 2 and m_name[m_name.length() - 2] == ',') + { + m_short_name = m_name.back(); + m_name.erase(m_name.end() - 2, m_name.end()); + } + } + + virtual ~option_base() = default; + + virtual void set_value(std::string_view /*value*/, std::error_code &/*ec*/) + { + assert(false); + } + + virtual std::any get_value() const + { + return {}; + } + + virtual std::string get_default_value() const + { + return {}; + } + + size_t width() const + { + size_t result = m_name.length(); + if (result <= 1) + result = 2; + else if (m_short_name != 0) + result += 7; + if (not m_is_flag) + { + result += 4; + if (m_has_default) + result += 4 + get_default_value().length(); + } + return result + 6; + } + + void write(std::ostream &os, size_t width) const + { + if (m_hidden) // quick exit + return; + size_t w2 = 2; + os << " "; + if (m_short_name) + { + os << '-' << m_short_name; + w2 += 2; + if (m_name.length() > 1) + { + os << " [ --" << m_name << " ]"; + w2 += 7 + m_name.length(); + } + } + else + { + os << "--" << m_name; + w2 += 2 + m_name.length(); + } + + if (not m_is_flag) + { + os << " arg"; + w2 += 4; + + if (m_has_default) + { + auto default_value = get_default_value(); + os << " (=" << default_value << ')'; + w2 += 4 + default_value.length(); + } + } + + auto leading_spaces = width; + if (w2 + 2 > width) + os << std::endl; + else + leading_spaces = width - w2; + + word_wrapper ww(m_desc, get_terminal_width() - width); + for (auto line : ww) + { + os << std::string(leading_spaces, ' ') << line << std::endl; + leading_spaces = width; + } + } + }; + + template + struct option : public option_base + { + using traits_type = option_traits; + using value_type = typename option_traits::value_type; + + std::optional m_value; + + option(const option &rhs) = default; + + option(std::string_view name, std::string_view desc, bool hidden) + : option_base(name, desc, hidden) + { + m_is_flag = false; + } + + option(std::string_view name, const value_type &default_value, std::string_view desc, bool hidden) + : option(name, desc, hidden) + { + m_has_default = true; + m_value = default_value; + } + + void set_value(std::string_view argument, std::error_code &ec) override + { + m_value = traits_type::set_value(argument, ec); + } + + std::any get_value() const override + { + std::any result; + if (m_value) + result = *m_value; + return result; + } + + std::string get_default_value() const override + { + if constexpr (std::is_same_v) + return *m_value; + else + return traits_type::to_string(*m_value); + } + }; + + template + struct multiple_option : public option_base + { + using value_type = typename T::value_type; + using traits_type = option_traits; + + std::vector m_values; + + multiple_option(const multiple_option &rhs) = default; + + multiple_option(std::string_view name, std::string_view desc, bool hidden) + : option_base(name, desc, hidden) + { + m_is_flag = false; + m_multi = true; + } + + void set_value(std::string_view argument, std::error_code &ec) override + { + m_values.emplace_back(traits_type::set_value(argument, ec)); + } + + std::any get_value() const override + { + return { m_values }; + } + }; + + template <> + struct option : public option_base + { + option(const option &rhs) = default; + + option(std::string_view name, std::string_view desc, bool hidden) + : option_base(name, desc, hidden) + { + } + }; + +} // namespace detail + +// -------------------------------------------------------------------- +/// \brief A singleton class. Use config::instance to create an instance + +class config +{ public: + using option_base = detail::option_base; - /** - * @brief Set the 'usage' string - * - * @param usage The usage message - */ void set_usage(std::string_view usage) { m_usage = usage; } - /** - * @brief Initialise a config instance with a \a usage message and a set of \a options - * - * @param usage The usage message - * @param options Variadic list of options recognised by this config object, use mcfp::make_option and variants to create these - */ + /// \brief Initialise a config instance with a \a usage message and a set of \a options template void init(std::string_view usage, Options... options) { m_usage = usage; - m_ignore_unknown = false; m_impl.reset(new config_impl(std::forward(options)...)); } - /** - * @brief Set the ignore unknown flag - * - * @param ignore_unknown When true, unknown options are simply ignored instead of - * throwing an error - */ void set_ignore_unknown(bool ignore_unknown) { m_ignore_unknown = ignore_unknown; } - /** - * @brief Use this to retrieve the single instance of this class - * - * @return config& The singleton instance - */ static config &instance() { static std::unique_ptr s_instance; @@ -112,39 +451,18 @@ class config return *s_instance; } - /** - * @brief Simply return true if the option with \a name has a value assigned - * - * @param name The name of the option - * @return bool Returns true when the option has a value - */ bool has(std::string_view name) const { auto opt = m_impl->get_option(name); return opt != nullptr and (opt->m_seen > 0 or opt->m_has_default); } - /** - * @brief Return how often an option with the name \a name was seen. - * Use e.g. to increase verbosity level - * - * @param name The name of the option to check - * @return int The count for the named option - */ int count(std::string_view name) const { auto opt = m_impl->get_option(name); return opt ? opt->m_seen : 0; } - /** - * @brief Returns the value for the option with name \a name. Throws - * an exception if the option has not value assigned - * - * @tparam T The type of the value requested. - * @param name The name of the option requested - * @return auto The value of the named option - */ template auto get(std::string_view name) const { @@ -159,16 +477,6 @@ class config return result; } - /** - * @brief Returns the value for the option with name \a name. If - * the option has no value assigned or is of a wrong type, - * ec is set to an appropriate error - * - * @tparam T The type of the value requested. - * @param name The name of the option requested - * @param ec The error status is returned in this variable - * @return auto The value of the named option - */ template auto get(std::string_view name, std::error_code &ec) const { @@ -201,53 +509,21 @@ class config return result; } - /** - * @brief Return the std::string value of the option with name \a name - * If no value was assigned, or the type of the option cannot be casted - * to a string, an exception is thrown. - * - * @param name The name of the option value requested - * @return std::string The value of the option - */ std::string get(std::string_view name) const { return get(name); } - /** - * @brief Return the std::string value of the option with name \a name - * If no value was assigned, or the type of the option cannot be casted - * to a string, an error is returned in \a ec. - * - * @param name The name of the option value requested - * @param ec The error status is returned in this variable - * @return std::string The value of the option - */ std::string get(std::string_view name, std::error_code &ec) const { return get(name, ec); } - /** - * @brief Return the list of operands. - * - * @return const std::vector& The operand as a vector of strings - */ const std::vector &operands() const { return m_impl->m_operands; } - /** - * @brief Write the configuration to the std::ostream \a os - * This will print the usage string and each of the configured - * options along with their optional default value as well as - * their help string - * - * @param os The std::ostream to write to, usually std::cout or std::cerr - * @param conf The config object to write out - * @return std::ostream& Returns the parameter \a os - */ friend std::ostream &operator<<(std::ostream &os, const config &conf) { size_t terminal_width = get_terminal_width(); @@ -267,13 +543,6 @@ class config // -------------------------------------------------------------------- - /** - * @brief Parse the \a argv vector containing \a argc elements. Throws - * an exception if any error was found - * - * @param argc The number of elements in \a argv - * @param argv The vector of command line arguments - */ void parse(int argc, const char *const argv[]) { std::error_code ec; @@ -282,17 +551,6 @@ class config throw std::system_error(ec); } - /** - * @brief Parse a configuration file called \a config_file_name optionally - * specified on the command line with option \a config_option - * The file is searched for in each of the directories specified in \a search_dirs - * This function throws an exception if an error was found during processing - * - * @param config_option The name of the option used to specify the config file - * @param config_file_name The default name of the option file to use if the config - * option was not specified on the command line - * @param search_dirs The list of directories to search for the config file - */ void parse_config_file(std::string_view config_option, std::string_view config_file_name, std::initializer_list search_dirs) { @@ -302,18 +560,6 @@ class config throw std::system_error(ec); } - /** - * @brief Parse a configuration file called \a config_file_name optionally - * specified on the command line with option \a config_option - * The file is searched for in each of the directories specified in \a search_dirs - * If an error is found it is returned in the variable \a ec - * - * @param config_option The name of the option used to specify the config file - * @param config_file_name The default name of the option file to use if the config - * option was not specified on the command line - * @param search_dirs The list of directories to search for the config file - * @param ec The variable containing the error status - */ void parse_config_file(std::string_view config_option, std::string_view config_file_name, std::initializer_list search_dirs, std::error_code &ec) { @@ -339,13 +585,6 @@ class config ec = make_error_code(config_error::config_file_not_found); } - /** - * @brief Parse a configuration file specified by \a file - * If an error is found it is returned in the variable \a ec - * - * @param file The path to the config file - * @param ec The variable containing the error status - */ void parse_config_file(const std::filesystem::path &file, std::error_code &ec) { std::ifstream is(file); @@ -353,8 +592,6 @@ class config parse_config_file(is, ec); } - private: - static bool is_name_char(int ch) { return std::isalnum(ch) or ch == '_' or ch == '-'; @@ -365,15 +602,6 @@ class config return ch == '\n' or ch == '\r' or ch == std::char_traits::eof(); } - public: - - /** - * @brief Parse the configuration file in \a is - * If an error is found it is returned in the variable \a ec - * - * @param is A std::istream for the contents of a config file - * @param ec The variable containing the error status - */ void parse_config_file(std::istream &is, std::error_code &ec) { auto &buffer = *is.rdbuf(); @@ -403,7 +631,7 @@ class config value.clear(); state = State::NAME; } - else if (ch == '#' or ch == ';') + else if (ch == '#') state = State::COMMENT; else if (ch != ' ' and ch != '\t' and not is_eoln(ch)) ec = make_error_code(config_error::invalid_config_file); @@ -499,14 +727,6 @@ class config } } - /** - * @brief Parse the \a argv vector containing \a argc elements. - * In case of an error, the error is returned in \a ec - * - * @param argc The number of elements in \a argv - * @param argv The vector of command line arguments - * @param ec The variable receiving the error status - */ void parse(int argc, const char *const argv[], std::error_code &ec) { using namespace std::literals; @@ -631,8 +851,6 @@ class config config(const config &) = delete; config &operator=(const config &) = delete; - /// @cond - struct config_impl_base { virtual ~config_impl_base() = default; @@ -712,113 +930,44 @@ class config std::unique_ptr m_impl; bool m_ignore_unknown = false; std::string m_usage; - - /// @endcond }; // -------------------------------------------------------------------- -/** - * @brief Create an option with name \a name and without a default value. - * If \a T is void the option does not expect a value and is in fact a flag. - * - * If the type of \a T is a container (std::vector e.g.) the option can be - * specified multiple times on the command line. - * - * The name \a name may end with a comma and a single character. This last - * character will then be the short version whereas the leading characters - * make up the long version. - * - * @tparam T The type of the option - * @param name The name of the option - * @param description The help text for this option - * @return auto The option object created - */ -template , int> = 0> +template , int> = 0> auto make_option(std::string_view name, std::string_view description) { return detail::option(name, description, false); } -template , int> = 0> -auto make_option(std::string_view name, std::string_view description) +template , int> = 0> +auto make_hidden_option(std::string_view name, std::string_view description) { - return detail::multiple_option(name, description, false); + return detail::option(name, description, true); } -/** - * @brief Create an option with name \a name and with a default value \a v. - * - * If the type of \a T is a container (std::vector e.g.) the option can be - * specified multiple times on the command line. - * - * The name \a name may end with a comma and a single character. This last - * character will then be the short version whereas the leading characters - * make up the long version. - * - * @tparam T The type of the option - * @param name The name of the option - * @param v The default value to use - * @param description The help text for this option - * @return auto The option object created - */ -template , int> = 0> +template , int> = 0> auto make_option(std::string_view name, const T &v, std::string_view description) { return detail::option(name, v, description, false); } -/** - * @brief Create an option with name \a name and without a default value. - * If \a T is void the option does not expect a value and is in fact a flag. - * This option will not be shown in the help / usage output. - * - * If the type of \a T is a container (std::vector e.g.) the option can be - * specified multiple times on the command line. - * - * The name \a name may end with a comma and a single character. This last - * character will then be the short version whereas the leading characters - * make up the long version. - * - * @tparam T The type of the option - * @param name The name of the option - * @param description The help text for this option - * @return auto The option object created - */ -template , int> = 0> -auto make_hidden_option(std::string_view name, std::string_view description) +template , int> = 0> +auto make_hidden_option(std::string_view name, const T &v, std::string_view description) { - return detail::option(name, description, true); + return detail::option(name, v, description, true); } -template , int> = 0> -auto make_hidden_option(std::string_view name, std::string_view description) +template , int> = 0> +auto make_option(std::string_view name, std::string_view description) { - return detail::multiple_option(name, description, true); + return detail::multiple_option(name, description, false); } -/** - * @brief Create an option with name \a name and with default value \a v. - * If \a T is void the option does not expect a value and is in fact a flag. - * This option will not be shown in the help / usage output. - * - * If the type of \a T is a container (std::vector e.g.) the option can be - * specified multiple times on the command line. - * - * The name \a name may end with a comma and a single character. This last - * character will then be the short version whereas the leading characters - * make up the long version. - * - * @tparam T The type of the option - * @param name The name of the option - * @param v The default value to use - * @param description The help text for this option - * @return auto The option object created - */ -template , int> = 0> -auto make_hidden_option(std::string_view name, const T &v, std::string_view description) +template , int> = 0> +auto make_hidden_option(std::string_view name, std::string_view description) { - return detail::option(name, v, description, true); + return detail::option(name, description, true); } } // namespace mcfp diff --git a/include/mcfp/text.hpp b/include/mcfp/text.hpp index 22cee15..3fa62ec 100644 --- a/include/mcfp/text.hpp +++ b/include/mcfp/text.hpp @@ -26,30 +26,245 @@ #pragma once -/** - * @file text.hpp - * This file contains an implementation of charconv and of work wrapping code - */ - -#include +#include +#include +#include +#include +#include + +#if __has_include() +#include +#else +#include +#endif namespace mcfp { -/** Import of the private implementation of charconv which maybe - * resolved to the std::charconv implementation or a private one - * defined in the detail namespace. -*/ +#if (not defined(__cpp_lib_experimental_detect) or (__cpp_lib_experimental_detect < 201505)) and (not defined(_LIBCPP_VERSION) or _LIBCPP_VERSION < 5000) +// This code is copied from: +// https://ld2015.scusa.lsu.edu/cppreference/en/cpp/experimental/is_detected.html -template -using charconv = typename detail::charconv; +template< class... > +using void_t = void; -/** Use the private implementation of is_detected_v which maybe - * resolved to the std::is_detected_v template when available - */ +namespace detail +{ + template class Op, class... Args> + struct detector + { + using value_t = std::false_type; + using type = Default; + }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + // Note that std::void_t is a c++17 feature + using value_t = std::true_type; + using type = Op; + }; +} // namespace detail + +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + void operator=(nonesuch const&) = delete; +}; + +template class Op, class... Args> +using is_detected = typename detail::detector::value_t; + +template class Op, class... Args> +constexpr inline bool is_detected_v = is_detected::value; + +template class Op, class... Args> +using detected_t = typename detail::detector::type; + +template class Op, class... Args> +using detected_or = detail::detector; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +constexpr inline bool is_detected_exact_v = is_detected_exact::value; + +#else template class Op, class... Args> -constexpr inline bool is_detected_v = detail::is_detected_v; +constexpr inline bool is_detected_v = std::experimental::is_detected::value; + +#endif + +template +struct my_charconv +{ + using value_type = T; + + static std::from_chars_result from_chars(const char *first, const char *last, value_type &value) + { + std::from_chars_result result{ first, {} }; + + enum State + { + IntegerSign, + Integer, + Fraction, + ExponentSign, + Exponent + } state = IntegerSign; + int sign = 1; + unsigned long long vi = 0; + long double f = 1; + int exponent_sign = 1; + int exponent = 0; + bool done = false; + + while (not done and result.ec == std::errc()) + { + char ch = result.ptr != last ? *result.ptr : 0; + ++result.ptr; + + switch (state) + { + case IntegerSign: + if (ch == '-') + { + sign = -1; + state = Integer; + } + else if (ch == '+') + state = Integer; + else if (ch >= '0' and ch <= '9') + { + vi = ch - '0'; + state = Integer; + } + else if (ch == '.') + state = Fraction; + else + result.ec = std::errc::invalid_argument; + break; + + case Integer: + if (ch >= '0' and ch <= '9') + vi = 10 * vi + (ch - '0'); + else if (ch == 'e' or ch == 'E') + state = ExponentSign; + else if (ch == '.') + state = Fraction; + else + { + done = true; + --result.ptr; + } + break; + + case Fraction: + if (ch >= '0' and ch <= '9') + { + vi = 10 * vi + (ch - '0'); + f /= 10; + } + else if (ch == 'e' or ch == 'E') + state = ExponentSign; + else + { + done = true; + --result.ptr; + } + break; + + case ExponentSign: + if (ch == '-') + { + exponent_sign = -1; + state = Exponent; + } + else if (ch == '+') + state = Exponent; + else if (ch >= '0' and ch <= '9') + { + exponent = ch - '0'; + state = Exponent; + } + else + result.ec = std::errc::invalid_argument; + break; + + case Exponent: + if (ch >= '0' and ch <= '9') + exponent = 10 * exponent + (ch - '0'); + else + { + done = true; + --result.ptr; + } + break; + } + } + + if (result.ec == std::errc()) + { + long double v = f * vi * sign; + if (exponent != 0) + v *= std::pow(10, exponent * exponent_sign); + + if (std::isnan(v)) + result.ec = std::errc::invalid_argument; + else if (std::abs(v) > std::numeric_limits::max()) + result.ec = std::errc::result_out_of_range; + + value = static_cast(v); + } + + return result; + } + + template , int> = 0> + static std::to_chars_result to_chars(Iterator first, Iterator last, const T &value) + { + int size = last - first; + int r; + + if constexpr (std::is_same_v) + r = snprintf(first, last - first, "%lg", value); + else + r = snprintf(first, last - first, "%g", value); + + std::to_chars_result result; + if (r < 0 or r >= size) + result = { first, std::errc::value_too_large }; + else + result = { first + r, std::errc() }; + + return result; + } +}; + +template +struct std_charconv +{ + static std::from_chars_result from_chars(const char *a, const char *b, T &d) + { + return std::from_chars(a, b, d); + } + + template + static std::to_chars_result to_chars(Iterator a, Iterator b, const T &value) + { + return std::to_chars(a, b, value); + } +}; + +template +using from_chars_function = decltype(std::from_chars(std::declval(), std::declval(), std::declval())); + +template +using charconv = typename std::conditional_t, std_charconv, my_charconv>; // -------------------------------------------------------------------- /// Simplified line breaking code taken from a decent text editor. diff --git a/include/mcfp/utilities.hpp b/include/mcfp/utilities.hpp index 19d8fde..7ee6b0d 100644 --- a/include/mcfp/utilities.hpp +++ b/include/mcfp/utilities.hpp @@ -33,14 +33,14 @@ #include #include #include -#elif defined(_WIN32) +#elif defined(_MSC_VER) #include #endif namespace mcfp { -#if defined(_WIN32) +#if defined(_MSC_VER) /// @brief Get the width in columns of the current terminal /// @return number of columns of the terminal inline uint32_t get_terminal_width() diff --git a/test/unit-test.conf b/test/unit-test.conf index fe5a7a0..d28ac93 100644 --- a/test/unit-test.conf +++ b/test/unit-test.conf @@ -1,4 +1,3 @@ # A simple test config file aap = 2 -noot = 3 -; With some comments +noot = 3 \ No newline at end of file diff --git a/test/unit-test.cpp b/test/unit-test.cpp index f33f006..e408b6b 100644 --- a/test/unit-test.cpp +++ b/test/unit-test.cpp @@ -24,53 +24,33 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#define CATCH_CONFIG_RUNNER - -#if CATCH22 -# include -#else -# include -#endif +#define BOOST_TEST_ALTERNATIVE_INIT_API +#include #include #include +namespace tt = boost::test_tools; +namespace utf = boost::unit_test; namespace fs = std::filesystem; -std::filesystem::path gTestDir = std::filesystem::current_path(); +fs::path gTestDir = fs::current_path(); + +// -------------------------------------------------------------------- -int main(int argc, char *argv[]) +bool init_unit_test() { - Catch::Session session; // There must be exactly one instance - - // Build a new parser on top of Catch2's -#if CATCH22 - using namespace Catch::clara; -#else - // Build a new parser on top of Catch2's - using namespace Catch::Clara; -#endif - - auto cli = session.cli() // Get Catch2's command line parser - | Opt(gTestDir, "data-dir") // bind variable to a new option, with a hint string - ["-D"]["--data-dir"] // the option names it will respond to - ("The directory containing the data files"); // description string for the help output - - // Now pass the new composite back to Catch2 so it uses that - session.cli(cli); - - // Let Catch2 (using Clara) parse the command line - int returnCode = session.applyCommandLine(argc, argv); - if (returnCode != 0) // Indicates a command line error - return returnCode; - - return session.run(); + // not a test, just initialize test dir + if (boost::unit_test::framework::master_test_suite().argc == 2) + gTestDir = boost::unit_test::framework::master_test_suite().argv[1]; + + return true; } // -------------------------------------------------------------------- -TEST_CASE("t_1, * utf::tolerance(0.001)") +BOOST_AUTO_TEST_CASE(t_1, * utf::tolerance(0.001)) { int argc = 3; const char *const argv[] = { @@ -89,19 +69,19 @@ TEST_CASE("t_1, * utf::tolerance(0.001)") config.parse(argc, argv); - CHECK(config.has("flag")); - CHECK(not config.has("flag2")); + BOOST_CHECK(config.has("flag")); + BOOST_CHECK(not config.has("flag2")); - CHECK(config.get("param_int_2") == 1); - CHECK_THROWS_AS(config.get("param_int_2"), std::system_error); - CHECK_THROWS_AS(config.get("param_int"), std::system_error); + BOOST_CHECK_EQUAL(config.get("param_int_2"), 1); + BOOST_CHECK_THROW(config.get("param_int_2"), std::system_error); + BOOST_CHECK_THROW(config.get("param_int"), std::system_error); - CHECK(std::to_string(config.get("param_float_2")) == std::to_string(3.14)); - CHECK_THROWS_AS(config.get("param_float_2"), std::system_error); - CHECK_THROWS_AS(config.get("param_float"), std::system_error); + BOOST_TEST(config.get("param_float_2") == 3.14); + BOOST_CHECK_THROW(config.get("param_float_2"), std::system_error); + BOOST_CHECK_THROW(config.get("param_float"), std::system_error); } -TEST_CASE("t_2") +BOOST_AUTO_TEST_CASE(t_2) { int argc = 3; const char *const argv[] = { @@ -116,10 +96,10 @@ TEST_CASE("t_2") config.parse(argc, argv); - CHECK(config.count("verbose") == 5); + BOOST_CHECK_EQUAL(config.count("verbose"), 5); } -TEST_CASE("t_3") +BOOST_AUTO_TEST_CASE(t_3) { int argc = 2; const char *const argv[] = { @@ -134,10 +114,11 @@ TEST_CASE("t_3") config.parse(argc, argv); - CHECK(config.has("param_int")); - CHECK(config.get("param_int") == 42); + BOOST_CHECK(config.has("param_int")); + BOOST_CHECK_EQUAL(config.get("param_int"), 42); } -TEST_CASE("t_4") + +BOOST_AUTO_TEST_CASE(t_4) { int argc = 3; const char *const argv[] = { @@ -152,11 +133,11 @@ TEST_CASE("t_4") config.parse(argc, argv); - CHECK(config.has("param_int")); - CHECK(config.get("param_int") == 42); + BOOST_CHECK(config.has("param_int")); + BOOST_CHECK_EQUAL(config.get("param_int"), 42); } -TEST_CASE("t_5") +BOOST_AUTO_TEST_CASE(t_5) { const char *const argv[] = { "test", "-i", "42", "-j43", nullptr @@ -172,14 +153,14 @@ TEST_CASE("t_5") config.parse(argc, argv); - CHECK(config.has("nr1")); - CHECK(config.has("nr2")); + BOOST_CHECK(config.has("nr1")); + BOOST_CHECK(config.has("nr2")); - CHECK(config.get("nr1") == 42); - CHECK(config.get("nr2") == 43); + BOOST_CHECK_EQUAL(config.get("nr1"), 42); + BOOST_CHECK_EQUAL(config.get("nr2"), 43); } -TEST_CASE("t_6") +BOOST_AUTO_TEST_CASE(t_6) { const char *const argv[] = { "test", "-i", "42", "-j43", "foo", "bar", nullptr @@ -195,18 +176,18 @@ TEST_CASE("t_6") config.parse(argc, argv); - CHECK(config.has("nr1")); - CHECK(config.has("nr2")); + BOOST_CHECK(config.has("nr1")); + BOOST_CHECK(config.has("nr2")); - CHECK(config.get("nr1") == 42); - CHECK(config.get("nr2") == 43); + BOOST_CHECK_EQUAL(config.get("nr1"), 42); + BOOST_CHECK_EQUAL(config.get("nr2"), 43); - CHECK(config.operands().size() == 2); - CHECK(config.operands().front() == "foo"); - CHECK(config.operands().back() == "bar"); + BOOST_CHECK_EQUAL(config.operands().size(), 2); + BOOST_CHECK_EQUAL(config.operands().front(), "foo"); + BOOST_CHECK_EQUAL(config.operands().back(), "bar"); } -TEST_CASE("t_7") +BOOST_AUTO_TEST_CASE(t_7) { const char *const argv[] = { "test", "--", "-i", "42", "-j43", "foo", "bar", nullptr @@ -222,16 +203,16 @@ TEST_CASE("t_7") config.parse(argc, argv); - CHECK(not config.has("nr1")); - CHECK(not config.has("nr2")); + BOOST_CHECK(not config.has("nr1")); + BOOST_CHECK(not config.has("nr2")); - CHECK(config.operands().size() == 5); + BOOST_CHECK_EQUAL(config.operands().size(), 5); auto compare = std::vector{ argv[2], argv[3], argv[4], argv[5], argv[6] }; - CHECK(config.operands() == compare); + BOOST_CHECK(config.operands() == compare); } -TEST_CASE("t_8") +BOOST_AUTO_TEST_CASE(t_8) { const char *const argv[] = { "test", "-i", "foo", "-jbar", nullptr @@ -248,16 +229,16 @@ TEST_CASE("t_8") config.parse(argc, argv); - CHECK(config.has("i")); - CHECK(config.get("i") == "foo"); - CHECK(config.has("j")); - CHECK(config.get("j") == "bar"); + BOOST_CHECK(config.has("i")); + BOOST_CHECK_EQUAL(config.get("i"), "foo"); + BOOST_CHECK(config.has("j")); + BOOST_CHECK_EQUAL(config.get("j"), "bar"); - CHECK(config.has("k")); - CHECK(config.get("k") == "baz"); + BOOST_CHECK(config.has("k")); + BOOST_CHECK_EQUAL(config.get("k"), "baz"); } -TEST_CASE("t_9") +BOOST_AUTO_TEST_CASE(t_9) { auto &config = mcfp::config::instance(); @@ -288,10 +269,10 @@ TEST_CASE("t_9") // std::cerr << '>' << kExpected << '<' << std::endl; // std::cerr << '>' << ss.str() << '<' << std::endl; -// CHECK_EQUAL(ss.str(), kExpected); +// BOOST_CHECK_EQUAL(ss.str(), kExpected); } -TEST_CASE("t_10") +BOOST_AUTO_TEST_CASE(t_10) { std::string s1 = R"(SPDX-License-Identifier: BSD-2-Clause @@ -312,7 +293,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND for (auto line : ww) os << line << std::endl; - CHECK(os.str() == R"(SPDX-License-Identifier: BSD-2-Clause + BOOST_CHECK_EQUAL(os.str(), R"(SPDX-License-Identifier: BSD-2-Clause Copyright (c) 2022 Maarten L. Hekkelman @@ -339,7 +320,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. )"); } -TEST_CASE("t_11") +BOOST_AUTO_TEST_CASE(t_11) { const char *const argv[] = { "test", "-faap", "-fnoot", "-fmies", nullptr @@ -354,16 +335,16 @@ TEST_CASE("t_11") config.parse(argc, argv); - CHECK(config.count("file") == 3); + BOOST_CHECK_EQUAL(config.count("file"), 3); std::vector files = config.get>("file"); - CHECK(files.size() == 3); - CHECK(files[0] == "aap"); - CHECK(files[1] == "noot"); - CHECK(files[2] == "mies"); + BOOST_CHECK_EQUAL(files.size(), 3); + BOOST_CHECK_EQUAL(files[0], "aap"); + BOOST_CHECK_EQUAL(files[1], "noot"); + BOOST_CHECK_EQUAL(files[2], "mies"); } -TEST_CASE("t_12") +BOOST_AUTO_TEST_CASE(t_12) { const char *const argv[] = { "test", "--aap", nullptr @@ -378,16 +359,16 @@ TEST_CASE("t_12") std::error_code ec; config.parse(argc, argv, ec); - CHECK(ec == mcfp::config_error::unknown_option); + BOOST_CHECK(ec == mcfp::config_error::unknown_option); config.set_ignore_unknown(true); ec = {}; config.parse(argc, argv, ec); - CHECK(not ec); + BOOST_CHECK(not ec); } -TEST_CASE("t_13") +BOOST_AUTO_TEST_CASE(t_13) { const char *const argv[] = { "test", "--test=bla", nullptr @@ -400,31 +381,15 @@ TEST_CASE("t_13") "test [options]", mcfp::make_option("test", "")); - CHECK_NOTHROW(config.parse(argc, argv)); - - CHECK(config.has("test")); - CHECK(config.get("test") == "bla"); -} - -TEST_CASE("t_14") -{ - const char *const argv[] = { - "test", "-test=bla", nullptr - }; - int argc = sizeof(argv) / sizeof(char*) - 1; - - auto &config = mcfp::config::instance(); + BOOST_CHECK_NO_THROW(config.parse(argc, argv)); - config.init( - "test [options]", - mcfp::make_option("test", "")); - - CHECK_THROWS_AS(config.parse(argc, argv), std::system_error); + BOOST_TEST(config.has("test")); + BOOST_TEST(config.get("test") == "bla"); } // -------------------------------------------------------------------- -TEST_CASE("file_1, * utf::tolerance(0.001)") +BOOST_AUTO_TEST_CASE(file_1, * utf::tolerance(0.001)) { const std::string_view config_file{ R"( # This is a test configuration @@ -461,24 +426,24 @@ verbose config.parse_config_file(is, ec); - CHECK(not ec); + BOOST_CHECK(not ec); - CHECK(config.has("aap")); - CHECK(config.get("aap") == "1"); + BOOST_CHECK(config.has("aap")); + BOOST_CHECK_EQUAL(config.get("aap"), "1"); - CHECK(config.has("noot")); - CHECK(config.get("noot") == 2); + BOOST_CHECK(config.has("noot")); + BOOST_CHECK_EQUAL(config.get("noot"), 2); - CHECK(config.has("pi")); - CHECK(std::to_string(config.get("pi")) == std::to_string(3.14)); + BOOST_CHECK(config.has("pi")); + BOOST_TEST(config.get("pi") == 3.14); - CHECK(config.has("s")); - CHECK(config.get("s") == "hello, world!"); + BOOST_CHECK(config.has("s")); + BOOST_CHECK_EQUAL(config.get("s"), "hello, world!"); - CHECK(config.has("verbose")); + BOOST_CHECK(config.has("verbose")); } -TEST_CASE("file_2") +BOOST_AUTO_TEST_CASE(file_2) { auto &config = mcfp::config::instance(); @@ -513,14 +478,14 @@ TEST_CASE("file_2") config.parse_config_file(is, ec); - CHECK(ec == err); + BOOST_CHECK(ec == err); if (ec == std::errc()) - CHECK(config.has(option)); + BOOST_CHECK(config.has(option)); } } -TEST_CASE("file_3") +BOOST_AUTO_TEST_CASE(file_3) { auto &config = mcfp::config::instance(); @@ -541,16 +506,16 @@ TEST_CASE("file_3") config.parse_config_file("config", "bla-bla.conf", { gTestDir.string() }, ec); - CHECK(not ec); + BOOST_CHECK(not ec); - CHECK(config.has("aap")); - CHECK(config.get("aap") == "aap"); + BOOST_CHECK(config.has("aap")); + BOOST_CHECK_EQUAL(config.get("aap"), "aap"); - CHECK(config.has("noot")); - CHECK(config.get("noot") == 42); + BOOST_CHECK(config.has("noot")); + BOOST_CHECK_EQUAL(config.get("noot"), 42); } - TEST_CASE("file_4") +BOOST_AUTO_TEST_CASE(file_4) { auto &config = mcfp::config::instance(); @@ -571,11 +536,11 @@ TEST_CASE("file_3") config.parse_config_file("config", "unit-test.conf", { gTestDir.string() }, ec); - CHECK(not ec); + BOOST_CHECK(not ec); - CHECK(config.has("aap")); - CHECK(config.get("aap") == "aap"); + BOOST_CHECK(config.has("aap")); + BOOST_CHECK_EQUAL(config.get("aap"), "aap"); - CHECK(config.has("noot")); - CHECK(config.get("noot") == 3); + BOOST_CHECK(config.has("noot")); + BOOST_CHECK_EQUAL(config.get("noot"), 3); } \ No newline at end of file