From ea2f16d26411cc047c1280d7b54a95e37d149b75 Mon Sep 17 00:00:00 2001 From: Zhihao Yuan Date: Sat, 23 Nov 2019 13:35:43 -0600 Subject: [PATCH 1/3] Upgrade and describe supported toolchains --- .travis.yml | 7 +++++++ README.md | 11 +++++++---- include/argparse.hpp | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f98d23..01c6545 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,13 @@ matrix: dist: bionic language: cpp compiler: gcc + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-8 + env: CXX=g++-8 CC=gcc-8 - os: osx osx_image: xcode10.2 language: cpp diff --git a/README.md b/README.md index 586eca5..bc29600 100644 --- a/README.md +++ b/README.md @@ -587,10 +587,13 @@ $ ./main fex baz ``` -## Supported Compilers -* GCC >= 7.0.0 -* Clang >= 4.0 -* MSVC >= 2017 +## Supported Toolchains + +| Compiler | Standard Library | Test Environment | +| :------------------- | :--------------- | :----------------- | +| GCC >= 8.3.0 | libstdc++ | Ubuntu 18.04 | +| Clang >= 7.0.0 | libc++ | Xcode 10.2 | +| MSVC >= 14.16 | Microsoft STL | Visual Studio 2017 | ## Contributing Contributions are welcome, have a look at the [CONTRIBUTING.md](CONTRIBUTING.md) document for more information. diff --git a/include/argparse.hpp b/include/argparse.hpp index 6bf6383..6ce6f83 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -30,6 +30,7 @@ SOFTWARE. #pragma once #include #include +#include #include #include #include From 426a5dbb765023e9a51aafd84d6929a500266669 Mon Sep 17 00:00:00 2001 From: Zhihao Yuan Date: Mon, 25 Nov 2019 18:44:04 -0600 Subject: [PATCH 2/3] Parse integers in the .scan fluent interface --- include/argparse.hpp | 137 ++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/test_scan.cpp | 173 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 test/test_scan.cpp diff --git a/include/argparse.hpp b/include/argparse.hpp index 6ce6f83..b5ef1ed 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -77,6 +77,25 @@ struct is_string_like : std::conjunction, std::is_convertible> {}; +template constexpr bool standard_signed_integer = false; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; + +template constexpr bool standard_unsigned_integer = false; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> +constexpr bool standard_unsigned_integer = true; + +template +constexpr bool standard_integer = + standard_signed_integer || standard_unsigned_integer; + template constexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, std::index_sequence) { @@ -92,6 +111,85 @@ constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) { std::tuple_size_v>>{}); } +constexpr auto pointer_range(std::string_view s) noexcept { + return std::tuple(s.data(), s.data() + s.size()); +} + +template +constexpr bool starts_with(std::basic_string_view prefix, + std::basic_string_view s) noexcept { + return s.substr(0, prefix.size()) == prefix; +} + +enum class chars_format { + scientific = 0x1, + fixed = 0x2, + hex = 0x4, + general = fixed | scientific +}; + +struct consume_hex_prefix_result { + bool is_hexadecimal; + std::string_view rest; +}; + +using namespace std::literals; + +constexpr auto consume_hex_prefix(std::string_view s) + -> consume_hex_prefix_result { + if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { + s.remove_prefix(2); + return {true, s}; + } else { + return {false, s}; + } +} + +template +inline auto do_from_chars(std::string_view s) -> T { + T x; + auto [first, last] = pointer_range(s); + auto [ptr, ec] = std::from_chars(first, last, x, Param); + if (ec == std::errc()) { + if (ptr == last) + return x; + else + throw std::invalid_argument{"pattern does not match to the end"}; + } else if (ec == std::errc::invalid_argument) { + throw std::invalid_argument{"pattern not found"}; + } else if (ec == std::errc::result_out_of_range) { + throw std::range_error{"not representable"}; + } else { + return x; // unreachable + } +} + +template struct parse_number { + auto operator()(std::string_view s) -> T { + return do_from_chars(s); + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + if (auto [ok, rest] = consume_hex_prefix(s); ok) + return do_from_chars(rest); + else + throw std::invalid_argument{"pattern not found"}; + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + if (auto [ok, rest] = consume_hex_prefix(s); ok) + return do_from_chars(rest); + else if (starts_with("0"sv, s)) + return do_from_chars(rest); + else + return do_from_chars(rest); + } +}; + } // namespace details class ArgumentParser; @@ -162,6 +260,45 @@ public: return *this; } + template + auto scan() -> std::enable_if_t, Argument &> { + static_assert(!(std::is_const_v || std::is_volatile_v), + "T should not be cv-qualified"); + auto is_one_of = [](char c, auto... x) constexpr { + return ((c == x) || ...); + }; + + if constexpr (is_one_of(Shape, 'd') && details::standard_integer) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'i') && details::standard_integer) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'u') && + details::standard_unsigned_integer) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'o') && + details::standard_unsigned_integer) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'x', 'X') && + details::standard_unsigned_integer) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'a', 'A') && + std::is_floating_point_v) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'e', 'E') && + std::is_floating_point_v) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'f', 'F') && + std::is_floating_point_v) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'g', 'G') && + std::is_floating_point_v) + action(details::parse_number()); + else + static_assert(alignof(T) == 0, "No scan specification for T"); + + return *this; + } + Argument &nargs(int aNumArgs) { if (aNumArgs < 0) throw std::logic_error("Number of arguments must be non-negative"); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 72aa695..a1f4c82 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -37,6 +37,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_parse_args.cpp test_positional_arguments.cpp test_required_arguments.cpp + test_scan.cpp test_value_semantics.cpp ) set_source_files_properties(main.cpp diff --git a/test/test_scan.cpp b/test/test_scan.cpp new file mode 100644 index 0000000..c55e5a8 --- /dev/null +++ b/test/test_scan.cpp @@ -0,0 +1,173 @@ +#include +#include +#include + +using doctest::test_suite; + +TEST_CASE_TEMPLATE("Parse a decimal integer argument" * test_suite("scan"), T, + int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, + uint32_t, uint64_t) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'d', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0"}); + REQUIRE(program.get("-n") == 0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "5"}); + REQUIRE(program.get("-n") == 5); + } + + SUBCASE("negative") { + if constexpr (std::is_signed_v) { + program.parse_args({"test", "-n", "-128"}); + REQUIRE(program.get("-n") == -128); + } else { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-135"}), + std::invalid_argument); + } + } + + SUBCASE("left-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", " 32"}), + std::invalid_argument); + } + + SUBCASE("right-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "12 "}), + std::invalid_argument); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+12"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS( + program.parse_args({"test", "-n", "987654321987654321987654321"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse an octal integer argument" * test_suite("scan"), T, + uint8_t, uint16_t, uint32_t, uint64_t) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'o', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0"}); + REQUIRE(program.get("-n") == 0); + } + + SUBCASE("with octal base") { + program.parse_args({"test", "-n", "066"}); + REQUIRE(program.get("-n") == 066); + } + + SUBCASE("minus sign produces an optional argument") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-003"}), + std::runtime_error); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+012"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS( + program.parse_args({"test", "-n", "02000000000000000000001"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse a hexadecimal integer argument" * test_suite("scan"), + T, uint8_t, uint16_t, uint32_t, uint64_t) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'X', T>(); + + SUBCASE("with hex digit") { + program.parse_args({"test", "-n", "0x1a"}); + REQUIRE(program.get("-n") == 0x1a); + } + + SUBCASE("minus sign produces an optional argument") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-0x1"}), + std::runtime_error); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+0x1a"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0XFFFFFFFFFFFFFFFF1"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse integer argument of any format" * test_suite("scan"), + T, int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, + uint32_t, uint64_t) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'i', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0"}); + REQUIRE(program.get("-n") == 0); + } + + SUBCASE("octal") { + program.parse_args({"test", "-n", "077"}); + REQUIRE(program.get("-n") == 077); + } + + SUBCASE("no negative octal") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-0777"}), + std::runtime_error); + } + + SUBCASE("hex") { + program.parse_args({"test", "-n", "0X2c"}); + REQUIRE(program.get("-n") == 0X2c); + } + + SUBCASE("no negative hex") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-0X2A"}), + std::runtime_error); + } + + SUBCASE("decimal") { + program.parse_args({"test", "-n", "98"}); + REQUIRE(program.get("-n") == 98); + } + + SUBCASE("negative decimal") { + if constexpr (std::is_signed_v) { + program.parse_args({"test", "-n", "-39"}); + REQUIRE(program.get("-n") == -39); + } else { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-39"}), + std::invalid_argument); + } + } + + SUBCASE("left-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "\t32"}), + std::invalid_argument); + } + + SUBCASE("right-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "32\n"}), + std::invalid_argument); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+670"}), + std::invalid_argument); + } +} From e8a44d289d88c0611caa8f5cf2b1a251569c410c Mon Sep 17 00:00:00 2001 From: Zhihao Yuan Date: Mon, 25 Nov 2019 23:47:14 -0600 Subject: [PATCH 3/3] Parse floating-point numbers in .scan fixes: p-ranav/argparse#63 --- include/argparse.hpp | 71 ++++++++++++++++++ test/test_scan.cpp | 175 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) diff --git a/include/argparse.hpp b/include/argparse.hpp index b5ef1ed..32ab73a 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -30,6 +30,7 @@ SOFTWARE. #pragma once #include #include +#include #include #include #include @@ -190,6 +191,76 @@ template struct parse_number { } }; +template constexpr auto generic_strtod = nullptr; +template <> constexpr auto generic_strtod = strtof; +template <> constexpr auto generic_strtod = strtod; +template <> constexpr auto generic_strtod = strtold; + +template inline auto do_strtod(std::string const &s) -> T { + if (isspace(static_cast(s[0])) || s[0] == '+') + throw std::invalid_argument{"pattern not found"}; + + auto [first, last] = pointer_range(s); + char *ptr; + + errno = 0; + if (auto x = generic_strtod(first, &ptr); errno == 0) { + if (ptr == last) + return x; + else + throw std::invalid_argument{"pattern does not match to the end"}; + } else if (errno == ERANGE) { + throw std::range_error{"not representable"}; + } else { + return x; // unreachable + } +} + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::general does not parse hexfloat"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) + throw std::invalid_argument{"chars_format::hex parses hexfloat"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::scientific does not parse hexfloat"}; + if (s.find_first_of("eE") == s.npos) + throw std::invalid_argument{ + "chars_format::scientific requires exponent part"}; + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) + throw std::invalid_argument{ + "chars_format::fixed does not parse hexfloat"}; + if (s.find_first_of("eE") != s.npos) + throw std::invalid_argument{ + "chars_format::fixed does not parse exponent part"}; + + return do_strtod(s); + } +}; + } // namespace details class ArgumentParser; diff --git a/test/test_scan.cpp b/test/test_scan.cpp index c55e5a8..64b6f58 100644 --- a/test/test_scan.cpp +++ b/test/test_scan.cpp @@ -171,3 +171,178 @@ TEST_CASE_TEMPLATE("Parse integer argument of any format" * test_suite("scan"), std::invalid_argument); } } + +#define FLOAT_G(t, literal) \ + ([] { \ + if constexpr (std::is_same_v) \ + return literal##f; \ + else if constexpr (std::is_same_v) \ + return literal; \ + else if constexpr (std::is_same_v) \ + return literal##l; \ + }()) + +TEST_CASE_TEMPLATE("Parse floating-point argument of general format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'g', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0"}); + REQUIRE(program.get("-n") == 0.); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12)); + } + + SUBCASE("left-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "\t.32"}), + std::invalid_argument); + } + + SUBCASE("right-padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", ".32\n"}), + std::invalid_argument); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12"}), + std::invalid_argument); + } + + SUBCASE("plus sign after padding is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", " +.12"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1a.3p+1"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "1.3e+5000"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse hexadecimal floating-point argument" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'a', T>(); + + SUBCASE("zero") { + // binary-exponent-part is not optional in C++ grammar + program.parse_args({"test", "-n", "0x0"}); + REQUIRE(program.get("-n") == 0x0.p0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "0x1a.3p+1"}); + REQUIRE(program.get("-n") == 0x1a.3p+1); + } + + SUBCASE("minus sign produces an optional argument") { + // XXX may worth a fix + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "-0x0.12p1"}), + std::runtime_error); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+0x1p0"}), + std::invalid_argument); + } + + SUBCASE("general format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14"}), + std::invalid_argument); + } +} + +TEST_CASE_TEMPLATE("Parse floating-point argument of scientific format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'e', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", "0e0"}); + REQUIRE(program.get("-n") == 0e0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14e-1"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14e-1)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12e+1"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12e+1)); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12e+1"}), + std::invalid_argument); + } + + SUBCASE("fixed format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1.33p+0"}), + std::invalid_argument); + } + + SUBCASE("does not fit") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "1.3e+5000"}), + std::range_error); + } +} + +TEST_CASE_TEMPLATE("Parse floating-point argument of fixed format" * + test_suite("scan"), + T, float, double, long double) { + argparse::ArgumentParser program("test"); + program.add_argument("-n").scan<'f', T>(); + + SUBCASE("zero") { + program.parse_args({"test", "-n", ".0"}); + REQUIRE(program.get("-n") == .0); + } + + SUBCASE("non-negative") { + program.parse_args({"test", "-n", "3.14"}); + REQUIRE(program.get("-n") == FLOAT_G(T, 3.14)); + } + + SUBCASE("negative") { + program.parse_args({"test", "-n", "-0.12"}); + REQUIRE(program.get("-n") == FLOAT_G(T, -0.12)); + } + + SUBCASE("plus sign is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "+.12"}), + std::invalid_argument); + } + + SUBCASE("scientific format is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "3.14e+0"}), + std::invalid_argument); + } + + SUBCASE("hexfloat is not allowed") { + REQUIRE_THROWS_AS(program.parse_args({"test", "-n", "0x1.33p+0"}), + std::invalid_argument); + } +}