diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e421012c..13ae3393 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -128,9 +128,9 @@ jobs: - name: Build run: meson compile -C build-meson - cmake-config: - name: CMake config check - runs-on: ubuntu-20.04 + cmake-config-ubuntu-1804: + name: CMake config check (Ubuntu 18.04) + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v3 @@ -175,6 +175,12 @@ jobs: cmake-version: "3.10" if: success() || failure() + cmake-config-ubuntu-2004: + name: CMake config check (Ubuntu 20.04) + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Check CMake 3.11 (full) uses: ./.github/actions/quick_cmake with: @@ -212,6 +218,12 @@ jobs: cmake-version: "3.16" if: success() || failure() + cmake-config-ubuntu-2204: + name: CMake config check (Ubuntu 22.04) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Check CMake 3.17 uses: ./.github/actions/quick_cmake with: diff --git a/.gitignore b/.gitignore index cc1b9d0c..6f8a8d04 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ a.out* /CMakeFiles/* /cmake_install.cmake /*.kdev4 +/.vscode /html/* !/meson.build +/CMakeUserPresets.json /node_modules/* /package.json diff --git a/CLI11.hpp.in b/CLI11.hpp.in index a4d6d08d..0c9e1ee2 100644 --- a/CLI11.hpp.in +++ b/CLI11.hpp.in @@ -40,10 +40,24 @@ {macros_hpp} +{slim_windows_h_hpp} + {validators_hpp_filesystem} +{encoding_includes} + +{argv_inl_includes} + namespace {namespace} {{ +{encoding_hpp} + +{encoding_inl_hpp} + +{argv_hpp} + +{argv_inl_hpp} + {string_tools_hpp} {string_tools_inl_hpp} diff --git a/CPPLINT.cfg b/CPPLINT.cfg index 24dd8652..40bec371 100644 --- a/CPPLINT.cfg +++ b/CPPLINT.cfg @@ -4,6 +4,7 @@ linelength=120 # As in .clang-format # Unused filters filter=-build/c++11 # Reports e.g. chrono and thread, which overlap with Chromium's API. Not applicable to general C++ projects. filter=-build/include_order # Requires unusual include order that encourages creating not self-contained headers +filter=-build/include_subdir # Prevents including files in current directory for whatever reason filter=-readability/nolint # Conflicts with clang-tidy filter=-readability/check # Catch uses CHECK(a == b) (Tests only) filter=-build/namespaces # Currently using it for one test (Tests only) diff --git a/README.md b/README.md index fdf0b0bd..bd9e58db 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ set with a simple and intuitive interface. - [Formatting](#formatting) - [Subclassing](#subclassing) - [How it works](#how-it-works) + - [Unicode support](#unicode-support) - [Utilities](#utilities) - [Other libraries](#other-libraries) - [API](#api) @@ -164,9 +165,6 @@ this library: option to disable it). - Autocomplete: This might eventually be added to both Plumbum and CLI11, but it is not supported yet. -- Wide strings / unicode: Since this uses the standard library only, it might be - hard to properly implement, but I would be open to suggestions in how to do - this. ## Install @@ -278,13 +276,13 @@ To set up, add options, and run, your main function will look something like this: ```cpp -int main(int argc, char** argv) { +int main() { CLI::App app{"App description"}; std::string filename = "default"; app.add_option("-f,--file", filename, "A help string"); - CLI11_PARSE(app, argc, argv); + CLI11_PARSE(app); return 0; } ``` @@ -293,7 +291,7 @@ int main(int argc, char** argv) { ```cpp try { - app.parse(argc, argv); + app.parse(); } catch (const CLI::ParseError &e) { return app.exit(e); } @@ -306,6 +304,25 @@ inside the catch block; for example, help flags intentionally short-circuit all other processing for speed and to ensure required options and the like do not interfere. +

+ +
Note: Why are argc and argv not used? (click to expand)

+ +`argc` and `argv` may contain incorrect information on Windows when unicode text +is passed in. Check out a section on [unicode support](#unicode-support) below. + +If this is not a concern, you can explicitly pass `argc` and `argv` from main or +from an external preprocessor of CLI arguments to `parse`: + +```cpp +int main(int argc, char** argv) { + // ... + + CLI11_PARSE(app, argc, argv); + return 0; +} +``` +


@@ -1468,6 +1485,96 @@ app.add_option("--fancy-count", [](std::vector val){ }); ``` +### Unicode support + +CLI11 supports Unicode and wide strings as defined in the +[UTF-8 Everywhere](http://utf8everywhere.org/) manifesto. In particular: + +- The library can parse a wide version of command-line arguments on Windows, + which are converted internally to UTF-8 (more on this below); +- You can store option values in `std::wstring`, in which case they will be + converted to a correct wide string encoding on your system (UTF-16 on Windows + and UTF-32 on most other systems); +- Instead of storing wide strings, it is recommended to use provided `widen` and + `narrow` functions to convert to and from wide strings when actually necessary + (such as when calling into Windows APIs). + +When using the command line on Windows with unicode arguments, your `main` +function may already receive broken Unicode. Parsing `argv` at that point will +not give you a correct string. To fix this, you have three options: + +1. If you pass unmodified command-line arguments to CLI11, call `app.parse()` + instead of `app.parse(argc, argv)` (or `CLI11_PARSE(app)` instead of + `CLI11_PARSE(app, argc, argv)`). The library will find correct arguments + itself. + + ```cpp + int main() { + CLI::App app; + // ... + CLI11_PARSE(app); + } + ``` + +2. Get correct arguments with which the program was originally executed using + provided functions: `CLI::argc()` and `CLI::argv()`. These two methods are + the only cross-platform ways of handling unicode correctly. + + ```cpp + int main() { + CLI::App app; + // ... + CLI11_PARSE(app, CLI::argc(), CLI::argv()); + } + ``` + +3. Use the Windows-only non-standard `wmain` function, which accepts + `wchar_t *argv[]` instead of `char* argv[]`. Parsing this will allow CLI to + convert wide strings to UTF-8 without losing information. + + ```cpp + int wmain(int argc, wchar_t *argv[]) { + CLI::App app; + // ... + CLI11_PARSE(app, argc, argv); + } + ``` + +4. Retrieve arguments yourself by using Windows APIs like + [`CommandLineToArgvW`](https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw) + and pass them to CLI. This is what the library is doing under the hood in + `CLI::argv()`. + +The library provides functions to convert between UTF-8 and wide strings: + +```cpp +namespace CLI { + std::string narrow(const std::wstring &str); + std::string narrow(const wchar_t *str); + std::string narrow(const wchar_t *str, std::size_t size); + std::string narrow(std::wstring_view str); // C++17 + + std::wstring widen(const std::string &str); + std::wstring widen(const char *str); + std::wstring widen(const char *str, std::size_t size); + std::wstring widen(std::string_view str); // C++17 +} +``` + +#### Note on using Unicode paths + +When creating a `filesystem::path` from a UTF-8 path on Windows, you need to +convert it to a wide string first. CLI11 provides a platform-independent +`to_path` function, which will convert a UTF-8 string to path, the right way: + +```cpp +std::string utf8_name = "Hello Halló Привет 你好 👩‍🚀❤️.txt"; + +std::filesystem::path p = CLI::to_path(utf8_name); +std::ifstream stream(CLI::to_path(utf8_name)); +// etc. +``` + ### Utilities There are a few other utilities that are often useful in CLI programming. These diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 27aa8dab..2676445d 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -35,9 +35,9 @@ namespace CLI { // [CLI11:app_hpp:verbatim] #ifndef CLI11_PARSE -#define CLI11_PARSE(app, argc, argv) \ +#define CLI11_PARSE(app, ...) \ try { \ - (app).parse((argc), (argv)); \ + (app).parse(__VA_ARGS__); \ } catch(const CLI::ParseError &e) { \ return (app).exit(e); \ } @@ -837,15 +837,25 @@ class App { /// Reset the parsed data void clear(); + /// Parse the command-line arguments passed to the main function of the executable. + /// This overload will correctly parse unicode arguments on Windows. + void parse(); + /// Parses the command line - throws errors. /// This must be called after the options are in but before the rest of the program. void parse(int argc, const char *const *argv); + void parse(int argc, const wchar_t *const *argv); + private: + template void parse_char_t(int argc, const CharT *const *argv); + + public: /// Parse a single string as if it contained command line arguments. /// This function splits the string into arguments then calls parse(std::vector &) /// the function takes an optional boolean argument specifying if the programName is included in the string to /// process void parse(std::string commandline, bool program_name_included = false); + void parse(std::wstring commandline, bool program_name_included = false); /// The real work is done here. Expects a reversed vector. /// Changes the vector to the remaining options. diff --git a/include/CLI/Argv.hpp b/include/CLI/Argv.hpp new file mode 100644 index 00000000..35d81a6e --- /dev/null +++ b/include/CLI/Argv.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include + +namespace CLI { +// [CLI11:argv_hpp:verbatim] + +/// 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(); + +// [CLI11:argv_hpp:end] +} // namespace CLI + +#ifndef CLI11_COMPILE +#include "impl/Argv_inl.hpp" +#endif diff --git a/include/CLI/CLI.hpp b/include/CLI/CLI.hpp index ae4fc604..fa9d4bb5 100644 --- a/include/CLI/CLI.hpp +++ b/include/CLI/CLI.hpp @@ -13,6 +13,10 @@ #include "Macros.hpp" +#include "Encoding.hpp" + +#include "Argv.hpp" + #include "StringTools.hpp" #include "Error.hpp" diff --git a/include/CLI/Encoding.hpp b/include/CLI/Encoding.hpp new file mode 100644 index 00000000..379e33b2 --- /dev/null +++ b/include/CLI/Encoding.hpp @@ -0,0 +1,54 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include + +// [CLI11:public_includes:set] +#include +// [CLI11:public_includes:end] + +// [CLI11:encoding_includes:verbatim] +#ifdef CLI11_CPP17 +#include +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include +#include // NOLINT(build/include) +#endif // CLI11_HAS_FILESYSTEM +// [CLI11:encoding_includes:end] + +namespace CLI { +// [CLI11:encoding_hpp:verbatim] + +/// Convert a wide string to a narrow string. +CLI11_INLINE std::string narrow(const std::wstring &str); +CLI11_INLINE std::string narrow(const wchar_t *str); +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t size); + +/// Convert a narrow string to a wide string. +CLI11_INLINE std::wstring widen(const std::string &str); +CLI11_INLINE std::wstring widen(const char *str); +CLI11_INLINE std::wstring widen(const char *str, std::size_t size); + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str); +CLI11_INLINE std::wstring widen(std::string_view str); +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +/// Convert a char-string to a native path correctly. +CLI11_INLINE std::filesystem::path to_path(std::string_view str); +#endif // CLI11_HAS_FILESYSTEM + +// [CLI11:encoding_hpp:end] +} // namespace CLI + +#ifndef CLI11_COMPILE +#include "impl/Encoding_inl.hpp" +#endif diff --git a/include/CLI/Macros.hpp b/include/CLI/Macros.hpp index e8543903..c7ac94e8 100644 --- a/include/CLI/Macros.hpp +++ b/include/CLI/Macros.hpp @@ -66,6 +66,62 @@ #endif #endif +/** availability */ +#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM +#if __has_include() +// Filesystem cannot be used if targeting macOS < 10.15 +#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 +#define CLI11_HAS_FILESYSTEM 0 +#elif defined(__wasi__) +// As of wasi-sdk-14, filesystem is not implemented +#define CLI11_HAS_FILESYSTEM 0 +#else +#include +#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 +#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 +#define CLI11_HAS_FILESYSTEM 1 +#elif defined(__GLIBCXX__) +// if we are using gcc and Version <9 default to no filesystem +#define CLI11_HAS_FILESYSTEM 0 +#else +#define CLI11_HAS_FILESYSTEM 1 +#endif +#else +#define CLI11_HAS_FILESYSTEM 0 +#endif +#endif +#endif +#endif + +/** availability */ +#if defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 5 +#define CLI11_HAS_CODECVT 0 +#else +#define CLI11_HAS_CODECVT 1 +#include +#endif + +/** disable deprecations */ +#if defined(__GNUC__) // GCC or clang +#define CLI11_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") +#define CLI11_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + +#elif defined(_MSC_VER) +#define CLI11_DIAGNOSTIC_PUSH __pragma(warning(push)) +#define CLI11_DIAGNOSTIC_POP __pragma(warning(pop)) + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED __pragma(warning(disable : 4996)) + +#else +#define CLI11_DIAGNOSTIC_PUSH +#define CLI11_DIAGNOSTIC_POP + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +#endif + /** Inline macro **/ #ifdef CLI11_COMPILE #define CLI11_INLINE diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 1555b299..14a668f8 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -18,6 +18,7 @@ #include // [CLI11:public_includes:end] +#include "Encoding.hpp" #include "StringTools.hpp" namespace CLI { @@ -242,8 +243,10 @@ struct is_mutable_container< decltype(std::declval().clear()), decltype(std::declval().insert(std::declval().end())>(), std::declval()))>, - void>> - : public conditional_t::value, std::false_type, std::true_type> {}; + void>> : public conditional_t::value || + std::is_constructible::value, + std::false_type, + std::true_type> {}; // check to see if an object is a mutable container (fail by default) template struct is_readable_container : std::false_type {}; @@ -546,6 +549,8 @@ enum class object_category : int { // string like types string_assignable = 23, string_constructible = 24, + wstring_assignable = 25, + wstring_constructible = 26, other = 45, // special wrapper or container types wrapper_value = 50, @@ -613,6 +618,27 @@ struct classify_object< static constexpr object_category value{object_category::string_constructible}; }; +/// Wide strings +template +struct classify_object::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && + std::is_assignable::value>::type> { + static constexpr object_category value{object_category::wstring_assignable}; +}; + +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> { + static constexpr object_category value{object_category::wstring_constructible}; +}; + /// Enumerations template struct classify_object::value>::type> { static constexpr object_category value{object_category::enumeration}; @@ -625,12 +651,13 @@ template struct classify_object struct uncommon_type { - using type = typename std::conditional::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !is_complex::value && - !is_mutable_container::value && !std::is_enum::value, - std::true_type, - std::false_type>::type; + using type = typename std::conditional< + !std::is_floating_point::value && !std::is_integral::value && + !std::is_assignable::value && !std::is_constructible::value && + !std::is_assignable::value && !std::is_constructible::value && + !is_complex::value && !is_mutable_container::value && !std::is_enum::value, + std::true_type, + std::false_type>::type; static constexpr bool value = type::value; }; @@ -1005,6 +1032,23 @@ bool lexical_cast(const std::string &input, T &output) { return true; } +/// Wide strings +template < + typename T, + enable_if_t::value == object_category::wstring_assignable, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = widen(input); + return true; +} + +template < + typename T, + enable_if_t::value == object_category::wstring_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = T{widen(input)}; + return true; +} + /// Enumerations template ::value == object_category::enumeration, detail::enabler> = detail::dummy> @@ -1133,7 +1177,9 @@ template ::value && (classify_object::value == object_category::string_assignable || - classify_object::value == object_category::string_constructible), + classify_object::value == object_category::string_constructible || + classify_object::value == object_category::wstring_assignable || + classify_object::value == object_category::wstring_constructible), detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { return lexical_cast(input, output); @@ -1144,7 +1190,9 @@ template ::value && std::is_assignable::value && classify_object::value != object_category::string_assignable && - classify_object::value != object_category::string_constructible, + classify_object::value != object_category::string_constructible && + classify_object::value != object_category::wstring_assignable && + classify_object::value != object_category::wstring_constructible, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { if(input.empty()) { diff --git a/include/CLI/Validators.hpp b/include/CLI/Validators.hpp index 28bdcb2d..59d800de 100644 --- a/include/CLI/Validators.hpp +++ b/include/CLI/Validators.hpp @@ -26,34 +26,6 @@ // [CLI11:validators_hpp_filesystem:verbatim] -// C standard library -// Only needed for existence checking -#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM -#if __has_include() -// Filesystem cannot be used if targeting macOS < 10.15 -#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 -#define CLI11_HAS_FILESYSTEM 0 -#elif defined(__wasi__) -// As of wasi-sdk-14, filesystem is not implemented -#define CLI11_HAS_FILESYSTEM 0 -#else -#include -#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 -#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 -#define CLI11_HAS_FILESYSTEM 1 -#elif defined(__GLIBCXX__) -// if we are using gcc and Version <9 default to no filesystem -#define CLI11_HAS_FILESYSTEM 0 -#else -#define CLI11_HAS_FILESYSTEM 1 -#endif -#else -#define CLI11_HAS_FILESYSTEM 0 -#endif -#endif -#endif -#endif - #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 #include // NOLINT(build/include) #else diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index b7ef7cfd..01d74e6d 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -9,6 +9,9 @@ // This include is only needed for IDEs to discover symbols #include +#include +#include + // [CLI11:public_includes:set] #include #include @@ -474,17 +477,31 @@ CLI11_INLINE void App::clear() { } } -CLI11_INLINE void App::parse(int argc, const char *const *argv) { +CLI11_INLINE void App::parse() { parse(argc(), argv()); } // LCOV_EXCL_LINE + +CLI11_INLINE void App::parse(int argc, const char *const *argv) { parse_char_t(argc, argv); } +CLI11_INLINE void App::parse(int argc, const wchar_t *const *argv) { parse_char_t(argc, argv); } + +namespace detail { + +// Do nothing or perform narrowing +CLI11_INLINE const char *maybe_narrow(const char *str) { return str; } +CLI11_INLINE std::string maybe_narrow(const wchar_t *str) { return narrow(str); } + +} // namespace detail + +template CLI11_INLINE void App::parse_char_t(int argc, const CharT *const *argv) { // If the name is not set, read from command line if(name_.empty() || has_automatic_name_) { has_automatic_name_ = true; - name_ = argv[0]; + name_ = detail::maybe_narrow(argv[0]); } std::vector args; args.reserve(static_cast(argc) - 1U); for(auto i = static_cast(argc) - 1U; i > 0U; --i) - args.emplace_back(argv[i]); + args.emplace_back(detail::maybe_narrow(argv[i])); + parse(std::move(args)); } @@ -515,6 +532,10 @@ CLI11_INLINE void App::parse(std::string commandline, bool program_name_included parse(std::move(args)); } +CLI11_INLINE void App::parse(std::wstring commandline, bool program_name_included) { + parse(narrow(commandline), program_name_included); +} + CLI11_INLINE void App::parse(std::vector &args) { // Clear if parsed if(parsed_ > 0) diff --git a/include/CLI/impl/Argv_inl.hpp b/include/CLI/impl/Argv_inl.hpp new file mode 100644 index 00000000..f19849e2 --- /dev/null +++ b/include/CLI/impl/Argv_inl.hpp @@ -0,0 +1,162 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +// This include is only needed for IDEs to discover symbols +#include + +#include + +// [CLI11:public_includes:set] +#include +#include +#include +#include +#include +// [CLI11:public_includes:end] + +#ifdef _WIN32 +#include "SlimWindowsH.hpp" +#endif // _WIN32 + +// [CLI11:argv_inl_includes:verbatim] +#if defined(_WIN32) +#include +#include +#elif defined(__APPLE__) +#include +#endif +// [CLI11:argv_inl_includes:end] + +namespace CLI { +// [CLI11:argv_inl_hpp:verbatim] + +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; + +#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); + + return cmdline; + }(); + + 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); + + 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())); + } + + return static_args_result; + }(); + + return static_args; +#endif +} + +} // namespace detail + +CLI11_INLINE const char *const *argv() { return detail::args().data(); } +CLI11_INLINE int argc() { return static_cast(detail::args().size()); } + +// [CLI11:argv_inl_hpp:end] +} // namespace CLI diff --git a/include/CLI/impl/Encoding_inl.hpp b/include/CLI/impl/Encoding_inl.hpp new file mode 100644 index 00000000..f5d7e9a8 --- /dev/null +++ b/include/CLI/impl/Encoding_inl.hpp @@ -0,0 +1,154 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +// This include is only needed for IDEs to discover symbols +#include +#include + +// [CLI11:public_includes:set] +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// [CLI11:public_includes:end] + +namespace CLI { +// [CLI11:encoding_inl_hpp:verbatim] + +namespace detail { + +#if !CLI11_HAS_CODECVT +/// Attempt to set one of the acceptable unicode locales for conversion +CLI11_INLINE void set_unicode_locale() { + static const std::array unicode_locales{{"C.UTF-8", "en_US.UTF-8", ".UTF-8"}}; + + for(const auto &locale_name : unicode_locales) { + if(std::setlocale(LC_ALL, locale_name) != nullptr) { + return; + } + } + throw std::runtime_error("CLI::narrow: could not set locale to C.UTF-8"); +} + +template struct scope_guard_t { + F closure; + + explicit scope_guard_t(F closure_) : closure(closure_) {} + ~scope_guard_t() { closure(); } +}; + +template CLI11_NODISCARD CLI11_INLINE scope_guard_t scope_guard(F &&closure) { + return scope_guard_t{std::forward(closure)}; +} + +#endif // !CLI11_HAS_CODECVT + +CLI11_DIAGNOSTIC_PUSH +CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +CLI11_INLINE std::string narrow_impl(const wchar_t *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert>().to_bytes(str, str + str_size); + +#else + return std::wstring_convert>().to_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const wchar_t *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::wcsrtombs(nullptr, &it, 0, &state); + if(new_size == static_cast(-1)) { + throw std::runtime_error("CLI::narrow: conversion error in std::wcsrtombs at offset " + + std::to_string(it - str)); + } + std::string result(new_size, '\0'); + std::wcsrtombs(const_cast(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_INLINE std::wstring widen_impl(const char *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert>().from_bytes(str, str + str_size); + +#else + return std::wstring_convert>().from_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const char *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::mbsrtowcs(nullptr, &it, 0, &state); + if(new_size == static_cast(-1)) { + throw std::runtime_error("CLI::widen: conversion error in std::mbsrtowcs at offset " + + std::to_string(it - str)); + } + std::wstring result(new_size, L'\0'); + std::mbsrtowcs(const_cast(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_DIAGNOSTIC_POP + +} // namespace detail + +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t str_size) { return detail::narrow_impl(str, str_size); } +CLI11_INLINE std::string narrow(const std::wstring &str) { return detail::narrow_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::string narrow(const wchar_t *str) { return detail::narrow_impl(str, std::wcslen(str)); } + +CLI11_INLINE std::wstring widen(const char *str, std::size_t str_size) { return detail::widen_impl(str, str_size); } +CLI11_INLINE std::wstring widen(const std::string &str) { return detail::widen_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::wstring widen(const char *str) { return detail::widen_impl(str, std::strlen(str)); } + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str) { return detail::narrow_impl(str.data(), str.size()); } +CLI11_INLINE std::wstring widen(std::string_view str) { return detail::widen_impl(str.data(), str.size()); } +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +CLI11_INLINE std::filesystem::path to_path(std::string_view str) { + return std::filesystem::path{ +#ifdef _WIN32 + widen(str) +#else + str +#endif // _WIN32 + }; +} +#endif // CLI11_HAS_FILESYSTEM + +// [CLI11:encoding_inl_hpp:end] +} // namespace CLI diff --git a/include/CLI/impl/SlimWindowsH.hpp b/include/CLI/impl/SlimWindowsH.hpp new file mode 100644 index 00000000..1a8d7feb --- /dev/null +++ b/include/CLI/impl/SlimWindowsH.hpp @@ -0,0 +1,101 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once +// [CLI11:slim_windows_h_hpp:verbatim] +#ifdef _WIN32 +// The most slimmed-down version of Windows.h. +#define WIN32_LEAN_AND_MEAN +#define WIN32_EXTRA_LEAN + +// Enable components based on necessity. +#define NOGDICAPMASKS +#define NOVIRTUALKEYCODES +#define NOWINMESSAGES +#define NOWINSTYLES +#define NOSYSMETRICS +#define NOMENUS +#define NOICONS +#define NOKEYSTATES +#define NOSYSCOMMANDS +#define NORASTEROPS +#define NOSHOWWINDOW +#define OEMRESOURCE +#define NOATOM +#define NOCLIPBOARD +#define NOCOLOR +#define NOCTLMGR +#define NODRAWTEXT +#define NOGDI +#define NOKERNEL +#define NOUSER +#define NONLS +#define NOMB +#define NOMEMMGR +#define NOMETAFILE +#define NOMINMAX +#define NOMSG +#define NOOPENFILE +#define NOSCROLL +#define NOSERVICE +#define NOSOUND +#define NOTEXTMETRIC +#define NOWH +#define NOWINOFFSETS +#define NOCOMM +#define NOKANJI +#define NOHELP +#define NOPROFILER +#define NODEFERWINDOWPOS +#define NOMCX + +#include "Windows.h" + +#undef WIN32_LEAN_AND_MEAN +#undef WIN32_EXTRA_LEAN + +#undef NOGDICAPMASKS +#undef NOVIRTUALKEYCODES +#undef NOWINMESSAGES +#undef NOWINSTYLES +#undef NOSYSMETRICS +#undef NOMENUS +#undef NOICONS +#undef NOKEYSTATES +#undef NOSYSCOMMANDS +#undef NORASTEROPS +#undef NOSHOWWINDOW +#undef OEMRESOURCE +#undef NOATOM +#undef NOCLIPBOARD +#undef NOCOLOR +#undef NOCTLMGR +#undef NODRAWTEXT +#undef NOGDI +#undef NOKERNEL +#undef NOUSER +#undef NONLS +#undef NOMB +#undef NOMEMMGR +#undef NOMETAFILE +#undef NOMINMAX +#undef NOMSG +#undef NOOPENFILE +#undef NOSCROLL +#undef NOSERVICE +#undef NOSOUND +#undef NOTEXTMETRIC +#undef NOWH +#undef NOWINOFFSETS +#undef NOCOMM +#undef NOKANJI +#undef NOHELP +#undef NOPROFILER +#undef NODEFERWINDOWPOS +#undef NOMCX + +#endif // _WIN32 +// [CLI11:slim_windows_h_hpp:end] diff --git a/include/CLI/impl/Validators_inl.hpp b/include/CLI/impl/Validators_inl.hpp index d6ac4fde..a2295ecd 100644 --- a/include/CLI/impl/Validators_inl.hpp +++ b/include/CLI/impl/Validators_inl.hpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -127,7 +128,7 @@ namespace detail { #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 CLI11_INLINE path_type check_path(const char *file) noexcept { std::error_code ec; - auto stat = std::filesystem::status(file, ec); + auto stat = std::filesystem::status(to_path(file), ec); if(ec) { return path_type::nonexistent; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9409b86c..89eb7e11 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,7 +13,9 @@ set(CLI11_headers ${CLI11_headerLoc}/StringTools.hpp ${CLI11_headerLoc}/TypeTools.hpp ${CLI11_headerLoc}/Validators.hpp - ${CLI11_headerLoc}/Version.hpp) + ${CLI11_headerLoc}/Version.hpp + ${CLI11_headerLoc}/Encoding.hpp + ${CLI11_headerLoc}/Argv.hpp) set(CLI11_implLoc "${PROJECT_SOURCE_DIR}/include/CLI/impl") @@ -24,7 +26,10 @@ set(CLI11_impl_headers ${CLI11_implLoc}/Option_inl.hpp ${CLI11_implLoc}/Split_inl.hpp ${CLI11_implLoc}/StringTools_inl.hpp - ${CLI11_implLoc}/Validators_inl.hpp) + ${CLI11_implLoc}/Validators_inl.hpp + ${CLI11_implLoc}/Encoding_inl.hpp + ${CLI11_implLoc}/Argv_inl.hpp + ${CLI11_implLoc}/SlimWindowsH.hpp) set(CLI11_library_headers ${CLI11_headerLoc}/CLI.hpp ${CLI11_headerLoc}/Timer.hpp) diff --git a/src/Precompile.cpp b/src/Precompile.cpp index 511b4735..5afd54cb 100644 --- a/src/Precompile.cpp +++ b/src/Precompile.cpp @@ -5,7 +5,9 @@ // SPDX-License-Identifier: BSD-3-Clause #include +#include #include +#include #include #include #include diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index fc5dee0e..5a944b13 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -7,6 +7,7 @@ #include "app_helper.hpp" #include +#include #include #include #include @@ -261,6 +262,28 @@ TEST_CASE_METHOD(TApp, "OneString", "[app]") { CHECK("mystring" == str); } +TEST_CASE_METHOD(TApp, "OneWideString", "[app]") { + std::wstring str; + app.add_option("-s,--string", str); + args = {"--string", "mystring"}; + run(); + CHECK(app.count("-s") == 1u); + CHECK(app.count("--string") == 1u); + CHECK(L"mystring" == str); +} + +TEST_CASE_METHOD(TApp, "OneStringWideInput", "[app][unicode]") { + std::string str; + app.add_option("-s,--string", str); + + std::array cmdline{{L"app", L"--string", L"mystring"}}; + app.parse(static_cast(cmdline.size()), cmdline.data()); + + CHECK(app.count("-s") == 1u); + CHECK(app.count("--string") == 1u); + CHECK("mystring" == str); +} + TEST_CASE_METHOD(TApp, "OneStringWindowsStyle", "[app]") { std::string str; app.add_option("-s,--string", str); @@ -282,6 +305,16 @@ TEST_CASE_METHOD(TApp, "OneStringSingleStringInput", "[app]") { CHECK("mystring" == str); } +TEST_CASE_METHOD(TApp, "OneStringSingleWideStringInput", "[app][unicode]") { + std::string str; + app.add_option("-s,--string", str); + + app.parse(L"--string mystring"); + CHECK(app.count("-s") == 1u); + CHECK(app.count("--string") == 1u); + CHECK("mystring" == str); +} + TEST_CASE_METHOD(TApp, "OneStringEqualVersion", "[app]") { std::string str; app.add_option("-s,--string", str); @@ -2463,3 +2496,21 @@ TEST_CASE("C20_compile", "simple") { app.parse("--flag"); CHECK_FALSE(flag->empty()); } + +// #14 +TEST_CASE("System Args", "[app]") { + const char *commandline = CLI11_SYSTEM_ARGS_EXE " 1234 false \"hello world\""; + int retval = std::system(commandline); + + if(retval == -1) { + FAIL("Executable '" << commandline << "' reported different argc count"); + } + + if(retval > 0) { + FAIL("Executable '" << commandline << "' reported different argv at index " << (retval - 1)); + } + + if(retval != 0) { + FAIL("Executable '" << commandline << "' failed with an unknown return code"); + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 360d57a8..1a9ad523 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -49,7 +49,8 @@ set(CLI11_TESTS StringParseTest ComplexTypeTest TrueFalseTest - OptionGroupTest) + OptionGroupTest + EncodingTest) if(WIN32) list(APPEND CLI11_TESTS WindowsTest) @@ -89,6 +90,40 @@ else() target_include_directories(catch_main PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") endif() +# Add special target that copies the data directory for tests +file( + GLOB_RECURSE DATA_FILES + LIST_DIRECTORIES false + RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/data/*") + +foreach(DATA_FILE IN LISTS DATA_FILES) + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${DATA_FILE}" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/${DATA_FILE}" + "${CMAKE_CURRENT_BINARY_DIR}/${DATA_FILE}" + MAIN_DEPENDENCY "${CMAKE_CURRENT_SOURCE_DIR}/${DATA_FILE}" + VERBATIM) + target_sources(catch_main PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${DATA_FILE}") +endforeach() + +# Build dependent applications which are launched from test code +set(CLI11_DEPENDENT_APPLICATIONS system_args) + +foreach(APP IN LISTS CLI11_DEPENDENT_APPLICATIONS) + add_executable(${APP} applications/${APP}.cpp) + target_include_directories(${APP} PRIVATE ${CMAKE_SOURCE_DIR}/include) + add_dependencies(catch_main ${APP}) +endforeach() + +function(add_dependent_application_definitions TARGET) + foreach(APP IN LISTS CLI11_DEPENDENT_APPLICATIONS) + string(TOUPPER ${APP} APP_UPPERCASE) + target_compile_definitions(${TARGET} + PRIVATE CLI11_${APP_UPPERCASE}_EXE="$") + endforeach() +endfunction() + # Target must already exist macro(add_catch_test TESTNAME) target_link_libraries(${TESTNAME} PUBLIC catch_main) @@ -108,6 +143,8 @@ foreach(T IN LISTS CLI11_TESTS) set_property(SOURCE ${T}.cpp PROPERTY LANGUAGE CUDA) endif() add_executable(${T} ${T}.cpp) + + add_dependent_application_definitions(${T}) add_sanitizers(${T}) if(NOT CLI11_CUDA_TESTS) target_link_libraries(${T} PRIVATE CLI11_warnings) @@ -117,6 +154,7 @@ foreach(T IN LISTS CLI11_TESTS) if(CLI11_SINGLE_FILE AND CLI11_SINGLE_FILE_TESTS) add_executable(${T}_Single ${T}.cpp) + add_dependent_application_definitions(${T}_Single) target_link_libraries(${T}_Single PRIVATE CLI11_SINGLE) add_catch_test(${T}_Single) set_property(TARGET ${T}_Single PROPERTY FOLDER "Tests Single File") @@ -125,6 +163,7 @@ endforeach() foreach(T IN LISTS CLI11_MULTIONLY_TESTS) add_executable(${T} ${T}.cpp) + add_dependent_application_definitions(${T}) add_sanitizers(${T}) target_link_libraries(${T} PUBLIC CLI11) add_catch_test(${T}) diff --git a/tests/EncodingTest.cpp b/tests/EncodingTest.cpp new file mode 100644 index 00000000..b026ee01 --- /dev/null +++ b/tests/EncodingTest.cpp @@ -0,0 +1,104 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "app_helper.hpp" + +#include +#include + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include +#endif // CLI11_HAS_FILESYSTEM + +// "abcd" +static const std::string abcd_str = "abcd"; // NOLINT(runtime/string) +static const std::wstring abcd_wstr = L"abcd"; // NOLINT(runtime/string) + +// "𓂀𓂀𓂀" - 4-byte utf8 characters +static const std::array egypt_utf8_codeunits{ + {0xF0, 0x93, 0x82, 0x80, 0xF0, 0x93, 0x82, 0x80, 0xF0, 0x93, 0x82, 0x80}}; +static const std::string egypt_str(reinterpret_cast(egypt_utf8_codeunits.data())); + +#ifdef _WIN32 +static const std::array egypt_utf16_codeunits{{0xD80C, 0xDC80, 0xD80C, 0xDC80, 0xD80C, 0xDC80}}; +static const std::wstring egypt_wstr(reinterpret_cast(egypt_utf16_codeunits.data())); + +#else +static const std::array egypt_utf32_codeunits{{0x00013080, 0x00013080, 0x00013080}}; +static const std::wstring egypt_wstr(reinterpret_cast(egypt_utf32_codeunits.data())); + +#endif + +// "Hello Halló Привет 你好 👩‍🚀❤️" - many languages and complex emojis +static const std::array hello_utf8_codeunits{ + {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x48, 0x61, 0x6c, 0x6c, 0xc3, 0xb3, 0x20, 0xd0, 0x9f, 0xd1, 0x80, + 0xd0, 0xb8, 0xd0, 0xb2, 0xd0, 0xb5, 0xd1, 0x82, 0x20, 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd, 0x20, 0xf0, + 0x9f, 0x91, 0xa9, 0xe2, 0x80, 0x8d, 0xf0, 0x9f, 0x9a, 0x80, 0xe2, 0x9d, 0xa4, 0xef, 0xb8, 0x8f}}; +static const std::string hello_str(reinterpret_cast(hello_utf8_codeunits.data())); + +#ifdef _WIN32 +static const std::array hello_utf16_codeunits{ + {0x0048, 0x0065, 0x006c, 0x006c, 0x006f, 0x0020, 0x0048, 0x0061, 0x006c, 0x006c, + 0x00f3, 0x0020, 0x041f, 0x0440, 0x0438, 0x0432, 0x0435, 0x0442, 0x0020, 0x4f60, + 0x597d, 0x0020, 0xd83d, 0xdc69, 0x200d, 0xd83d, 0xde80, 0x2764, 0xfe0f}}; +static const std::wstring hello_wstr(reinterpret_cast(hello_utf16_codeunits.data())); + +#else +static const std::array hello_utf32_codeunits{ + {0x00000048, 0x00000065, 0x0000006c, 0x0000006c, 0x0000006f, 0x00000020, 0x00000048, 0x00000061, 0x0000006c, + 0x0000006c, 0x000000f3, 0x00000020, 0x0000041f, 0x00000440, 0x00000438, 0x00000432, 0x00000435, 0x00000442, + 0x00000020, 0x00004f60, 0x0000597d, 0x00000020, 0x0001f469, 0x0000200d, 0x0001f680, 0x00002764, 0x0000fe0f}}; +static const std::wstring hello_wstr(reinterpret_cast(hello_utf32_codeunits.data())); + +#endif + +// #14 +TEST_CASE("Encoding: Widen", "[unicode]") { + using CLI::widen; + + CHECK(abcd_wstr == widen(abcd_str)); + CHECK(egypt_wstr == widen(egypt_str)); + CHECK(hello_wstr == widen(hello_str)); + + CHECK(hello_wstr == widen(hello_str.c_str())); + CHECK(hello_wstr == widen(hello_str.c_str(), hello_str.size())); + +#ifdef CLI11_CPP17 + CHECK(hello_wstr == widen(std::string_view{hello_str})); +#endif // CLI11_CPP17 +} + +// #14 +TEST_CASE("Encoding: Narrow", "[unicode]") { + using CLI::narrow; + + CHECK(abcd_str == narrow(abcd_wstr)); + CHECK(egypt_str == narrow(egypt_wstr)); + CHECK(hello_str == narrow(hello_wstr)); + + CHECK(hello_str == narrow(hello_wstr.c_str())); + CHECK(hello_str == narrow(hello_wstr.c_str(), hello_wstr.size())); + +#ifdef CLI11_CPP17 + CHECK(hello_str == narrow(std::wstring_view{hello_wstr})); +#endif // CLI11_CPP17 +} + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +// #14 +TEST_CASE("Encoding: to_path roundtrip", "[unicode]") { + using std::filesystem::path; + +#ifdef _WIN32 + std::wstring native_str = CLI::widen(hello_str); +#else + std::string native_str = hello_str; +#endif // _WIN32 + + CHECK(CLI::to_path(hello_str).native() == native_str); +} + +#endif // CLI11_HAS_FILESYSTEM diff --git a/tests/app_helper.hpp b/tests/app_helper.hpp index db98b29a..5479e486 100644 --- a/tests/app_helper.hpp +++ b/tests/app_helper.hpp @@ -13,6 +13,9 @@ #endif #include "catch.hpp" +#include +#include +#include #include #include #include @@ -67,3 +70,54 @@ inline void unset_env(std::string name) { unsetenv(name.c_str()); #endif } + +CLI11_INLINE void check_identical_files(const char *path1, const char *path2) { + std::string err1 = CLI::ExistingFile(path1); + if(!err1.empty()) { + FAIL("Could not open " << path1 << ": " << err1); + } + + std::string err2 = CLI::ExistingFile(path2); + if(!err2.empty()) { + FAIL("Could not open " << path2 << ": " << err2); + } + + // open files at the end to compare size first + std::ifstream file1(path1, std::ifstream::ate | std::ifstream::binary); + std::ifstream file2(path2, std::ifstream::ate | std::ifstream::binary); + + if(!file1.good()) { + FAIL("File " << path1 << " is corrupted"); + } + + if(!file2.good()) { + FAIL("File " << path2 << " is corrupted"); + } + + if(file1.tellg() != file2.tellg()) { + FAIL("Different file sizes:\n " << file1.tellg() << " bytes in " << path1 << "\n " << file2.tellg() + << " bytes in " << path2); + } + + // rewind files + file1.seekg(0); + file2.seekg(0); + + std::array buffer1; + std::array buffer2; + + for(size_t ibuffer = 0; file1.good(); ++ibuffer) { + // Flawfinder: ignore + file1.read(reinterpret_cast(buffer1.data()), static_cast(buffer1.size())); + // Flawfinder: ignore + file2.read(reinterpret_cast(buffer2.data()), static_cast(buffer2.size())); + + for(size_t i = 0; i < static_cast(file1.gcount()); ++i) { + if(buffer1[i] != buffer2[i]) { + FAIL(std::hex << std::setfill('0') << "Different bytes at position " << (ibuffer * 10240 + i) << ":\n " + << "0x" << std::setw(2) << static_cast(buffer1[i]) << " in " << path1 << "\n " + << "0x" << std::setw(2) << static_cast(buffer2[i]) << " in " << path2); + } + } + } +} diff --git a/tests/applications/system_args.cpp b/tests/applications/system_args.cpp new file mode 100644 index 00000000..e1e77ba6 --- /dev/null +++ b/tests/applications/system_args.cpp @@ -0,0 +1,22 @@ +// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include + +int main(int argc, char **argv) { + if(argc != CLI::argc()) { + return -1; + } + + for(int i = 0; i < argc; i++) { + if(std::strcmp(argv[i], CLI::argv()[i]) != 0) { + return i + 1; + } + } + + return 0; +} diff --git a/tests/data/unicode.txt b/tests/data/unicode.txt new file mode 100644 index 00000000..f430f542 --- /dev/null +++ b/tests/data/unicode.txt @@ -0,0 +1 @@ +Hello Halló Привет 你好 👩‍🚀❤️ diff --git a/tests/meson.build b/tests/meson.build index 27e22161..b37574d5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -57,6 +57,20 @@ testnames = [ ['link_test_2', {'link_with': link_test_lib}], ] +dependent_applications = [ + 'system_args' +] +dependent_applications_definitions = [] +#dependent_applications_targets = [] +foreach app: dependent_applications + app_target = executable(app, 'applications'/app + '.cpp', + build_by_default: false + ) + + #dependent_applications_targets += dependency(app_target) + dependent_applications_definitions += '-DCLI11_@0@_EXE="@1@"'.format(app.to_upper(), app_target) +endforeach + if host_machine.system() == 'windows' testnames += [['WindowsTest', {}]] endif @@ -69,7 +83,7 @@ foreach n: testnames name = n[0] kwargs = n[1] t = executable(name, name + '.cpp', - cpp_args: kwargs.get('cpp_args', []), + cpp_args: kwargs.get('cpp_args', []) + dependent_applications_definitions, build_by_default: false, dependencies: [testdep] + kwargs.get('dependencies', []), link_with: kwargs.get('link_with', [])