diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index b8c9daf2..44394883 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -53,26 +53,71 @@ namespace argparse { namespace details { // namespace for helper methods -template struct is_container_helper {}; - -template +template struct is_container : std::false_type {}; template <> struct is_container : std::false_type {}; template -struct is_container< - T, - std::conditional_t().begin()), - decltype(std::declval().end()), - decltype(std::declval().size())>, - void>> : std::true_type {}; +struct is_container().begin()), + decltype(std::declval().end()), + decltype(std::declval().size())>> + : std::true_type {}; template static constexpr bool is_container_v = is_container::value; +template +struct is_streamable : std::false_type {}; + +template +struct is_streamable< + T, std::void_t() << std::declval())>> + : std::true_type {}; + +template +static constexpr bool is_streamable_v = is_streamable::value; + +template +static constexpr bool is_representable_v = + is_streamable_v || is_container_v; + +constexpr size_t repr_max_container_size = 5; + +template std::string repr(T const &val) { + if constexpr (std::is_same_v) { + return val ? "true" : "false"; + } else if constexpr (std::is_convertible_v) { + return '"' + std::string{std::string_view{val}} + '"'; + } else if constexpr (is_container_v) { + std::stringstream out; + out << "{"; + const auto size = val.size(); + if (size > 1) { + out << repr(*val.begin()); + std::for_each( + std::next(val.begin()), + std::next(val.begin(), std::min(size, repr_max_container_size) - 1), + [&out](const auto &v) { out << " " << repr(v); }); + if (size <= repr_max_container_size) + out << " "; + else + out << "..."; + } + if (size > 0) + out << repr(*std::prev(val.end())); + out << "}"; + return out.str(); + } else if constexpr (is_streamable_v) { + std::stringstream out; + out << val; + return out.str(); + } else { + return ""; + } +} + namespace { template constexpr bool standard_signed_integer = false; @@ -90,7 +135,7 @@ template <> constexpr bool standard_unsigned_integer = true; template <> constexpr bool standard_unsigned_integer = true; -} +} // namespace template constexpr bool standard_integer = @@ -197,7 +242,7 @@ template <> constexpr auto generic_strtod = strtof; template <> constexpr auto generic_strtod = strtod; template <> constexpr auto generic_strtod = strtold; -} +} // namespace template inline auto do_strtod(std::string const &s) -> T { if (isspace(static_cast(s[0])) || s[0] == '+') @@ -294,8 +339,9 @@ class Argument { return *this; } - Argument &default_value(std::any aDefaultValue) { - mDefaultValue = std::move(aDefaultValue); + template Argument &default_value(T &&aDefaultValue) { + mDefaultValueRepr = details::repr(aDefaultValue); + mDefaultValue = std::forward(aDefaultValue); return *this; } @@ -485,6 +531,11 @@ class Argument { stream << nameStream.str() << "\t" << argument.mHelp; if (argument.mIsRequired && !argument.mDefaultValue.has_value()) stream << "[Required]"; + if (argument.mDefaultValue.has_value()) { + if (!argument.mHelp.empty()) + stream << " "; + stream << "[default: " << argument.mDefaultValueRepr << "]"; + } stream << "\n"; return stream; } @@ -735,6 +786,7 @@ class Argument { std::string_view mUsedName; std::string mHelp; std::any mDefaultValue; + std::string mDefaultValueRepr; std::any mImplicitValue; using valued_action = std::function; using void_action = std::function; @@ -750,11 +802,10 @@ class Argument { class ArgumentParser { public: - explicit ArgumentParser(std::string aProgramName = {}, std::string aVersion = "1.0") + explicit ArgumentParser(std::string aProgramName = {}, + std::string aVersion = "1.0") : mProgramName(std::move(aProgramName)), mVersion(std::move(aVersion)) { - add_argument("-h", "--help") - .help("shows help message and exits") - .nargs(0); + add_argument("-h", "--help").help("shows help message and exits").nargs(0); add_argument("-v", "--version") .help("prints version information and exits") .nargs(0); @@ -850,7 +901,8 @@ class ArgumentParser { * @throws std::logic_error if the option has no value * @throws std::bad_any_cast if the option is not of type T */ - template T get(std::string_view aArgumentName) const { + template + T get(std::string_view aArgumentName) const { return (*this)[aArgumentName].get(); } @@ -889,7 +941,7 @@ class ArgumentParser { } stream << "\n\n"; - if(!parser.mDescription.empty()) + if (!parser.mDescription.empty()) stream << parser.mDescription << "\n\n"; if (!parser.mPositionalArguments.empty()) @@ -909,7 +961,7 @@ class ArgumentParser { stream << mOptionalArgument; } - if(!parser.mEpilog.empty()) + if (!parser.mEpilog.empty()) stream << parser.mEpilog << "\n\n"; } @@ -963,7 +1015,7 @@ class ArgumentParser { std::cout << *this; std::exit(0); } - // the second optional argument is --version + // the second optional argument is --version else if (tArgument == std::next(mOptionalArguments.begin(), 1)) { std::cout << mVersion << "\n"; std::exit(0); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 592fee1a..d3d23010 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_parent_parsers.cpp test_parse_args.cpp test_positional_arguments.cpp + test_repr.cpp test_required_arguments.cpp test_scan.cpp test_value_semantics.cpp diff --git a/test/test_help.cpp b/test/test_help.cpp index 25b87af2..78438eae 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -1,15 +1,25 @@ -#include #include +#include using doctest::test_suite; TEST_CASE("Users can format help message" * test_suite("help")) { argparse::ArgumentParser program("test"); - program.add_argument("input") - .help("positional input"); - program.add_argument("-c") - .help("optional input"); + SUBCASE("Simple arguments") { + program.add_argument("input").help("positional input"); + program.add_argument("-c").help("optional input"); + } + SUBCASE("Default values") { + program.add_argument("-a").default_value(42); + program.add_argument("-b").default_value(4.4e-7); + program.add_argument("-c") + .default_value(std::vector{1, 2, 3, 4, 5}) + .nargs(5); + program.add_argument("-d").default_value("I am a string"); + program.add_argument("-e").default_value(std::optional{}); + program.add_argument("-f").default_value(false); + } std::ostringstream s; s << program; REQUIRE_FALSE(s.str().empty()); diff --git a/test/test_repr.cpp b/test/test_repr.cpp new file mode 100644 index 00000000..dc7d1c7e --- /dev/null +++ b/test/test_repr.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +using doctest::test_suite; + +TEST_CASE("Test bool representation" * test_suite("repr")) { + REQUIRE(argparse::details::repr(true) == "true"); + REQUIRE(argparse::details::repr(false) == "false"); +} + +TEST_CASE_TEMPLATE("Test built-in int types representation" * + test_suite("repr"), + T, char, short, int, long long, unsigned char, unsigned, + unsigned long long) { + std::stringstream ss; + T v = 42; + ss << v; + REQUIRE(argparse::details::repr(v) == ss.str()); +} + +TEST_CASE_TEMPLATE("Test built-in float types representation" * + test_suite("repr"), + T, float, double, long double) { + std::stringstream ss; + T v = 0.3333333333; + ss << v; + REQUIRE(argparse::details::repr(v) == ss.str()); +} + +TEST_CASE_TEMPLATE("Test container representation" * test_suite("repr"), T, + std::vector, std::list, std::set) { + T empty; + T one = {42}; + T small = {1, 2, 3}; + T big = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + + REQUIRE(argparse::details::repr(empty) == "{}"); + REQUIRE(argparse::details::repr(one) == "{42}"); + REQUIRE(argparse::details::repr(small) == "{1 2 3}"); + REQUIRE(argparse::details::repr(big) == "{1 2 3 4...15}"); +} + +TEST_CASE_TEMPLATE("Test string representation" * test_suite("repr"), T, + char const *, std::string, std::string_view) { + T empty = ""; + T str = "A A A#"; + + REQUIRE(argparse::details::repr(empty) == "\"\""); + REQUIRE(argparse::details::repr(str) == "\"A A A#\""); +} + +TEST_CASE("Test unknown representation" * test_suite("repr")) { + struct TestClass {}; + REQUIRE(argparse::details::repr(TestClass{}) == ""); +}