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', [])