From 426a5dbb765023e9a51aafd84d6929a500266669 Mon Sep 17 00:00:00 2001 From: Zhihao Yuan Date: Mon, 25 Nov 2019 18:44:04 -0600 Subject: [PATCH] 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); + } +}