From e9609dfea5a8210f9e1a23c0d80f53df7664e71a Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Thu, 3 Jul 2025 19:46:01 +0100 Subject: cli: add a basic command-line option parser --- nihil.cli/parse_flags.test.cc | 523 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 nihil.cli/parse_flags.test.cc (limited to 'nihil.cli/parse_flags.test.cc') diff --git a/nihil.cli/parse_flags.test.cc b/nihil.cli/parse_flags.test.cc new file mode 100644 index 0000000..5fa84a2 --- /dev/null +++ b/nihil.cli/parse_flags.test.cc @@ -0,0 +1,523 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.cli; +import nihil.core; + +namespace { + +inline auto constexpr *test_tags = "[nihil][nihil.cli][nihil.cli_parse_flags]"; + +SCENARIO("Parsing several flags with flag_parser", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string s_flag; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-a'") { + auto ret = posix_parse(flags, std::array{"-a"sv}).value(); + + THEN ("a_flag is set") { + REQUIRE(ret.a_flag); + } + + AND_THEN ("The other flags are not") { + REQUIRE(!ret.b_flag); + REQUIRE(ret.s_flag.empty()); + } + } + + WHEN ("We parse '-ab'") { + auto ret = posix_parse(flags, std::array{"-ab"sv}).value(); + + THEN ("a_flag and b_flag are set") { + REQUIRE(ret.a_flag); + REQUIRE(ret.b_flag); + } + + AND_THEN ("s_flag is not set") { + REQUIRE(ret.s_flag.empty()); + } + } + + WHEN ("We parse '-sarg'") { + auto ret = posix_parse(flags, std::array{"-sarg"sv}).value(); + + THEN ("s_flag is set to 'arg'") { + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("The other flags are not set") { + REQUIRE(!ret.a_flag); + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-s arg'") { + auto ret = posix_parse(flags, std::array{"-s"sv, "arg"sv}).value(); + + THEN ("s_flag is set to 'arg'") { + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("The other flags are not set") { + REQUIRE(!ret.a_flag); + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-a -sarg'") { + auto ret = posix_parse(flags, std::array{"-a"sv, "-sarg"sv}).value(); + + THEN ("a_flag is set and s_flag is set to 'arg'") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-asarg'") { + auto ret = posix_parse(flags, std::array{"-asarg"sv}).value(); + + THEN ("a_flag is set and s_flag is set to 'arg'") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + } +} + +SCENARIO("Calling flag_parser with incorrect options", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") { + struct test_flags + { + bool a_flag{}; + std::string s_flag; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-x'") { + auto ret = posix_parse(flags, std::array{"-x"sv}); + + THEN ("An unknown argument error is returned") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-x: unknown option"); + } + } + + WHEN ("We parse '-s'") { + auto ret = posix_parse(flags, std::array{"-s"sv}); + + THEN ("A missing argument error is returned") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-s: argument required"); + } + } + } +} + +SCENARIO("Terminating the option list with '--'", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag parser with -a and -b options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string arg1; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option("arg1", &test_flags::arg1)); + + WHEN ("We parse '-a -- -b'") { + auto ret = posix_parse(flags, std::array{"-a"sv, "--"sv, "-b"sv}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN ("a_flag is set and b_flag is not") { + REQUIRE(ret->a_flag == true); + REQUIRE(ret->b_flag == false); + } + AND_THEN ("arg1 is set") { + REQUIRE(ret->arg1 == "-b"); + } + } + } +} + +SCENARIO("Creating a posix_usage() string", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag parser with only boolean options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '[-ab]'") { + REQUIRE(usage == "[-ab]"); + } + } + } + + GIVEN ("A flag parser with boolean and string options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string c_flag; + std::optional d_flag; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('c', "cflag", &test_flags::c_flag), + option('d', "dflag", &test_flags::d_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '[-ab] -c [-d ]'") { + REQUIRE(usage == "[-ab] -c [-d ]"); + } + } + } + + GIVEN ("A flag parser with only string options") { + struct test_flags + { + std::string c_flag; + std::string d_flag; + }; + + auto flags = options(option('c', "cflag", &test_flags::c_flag), + option('d', "dflag", &test_flags::d_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '-c -d '") { + REQUIRE(usage == "-c -d "); + } + } + } +} + +SCENARIO("Calling posix_parse() with an (argc, argv) vector", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") + { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string s_flag; + }; + + auto flags = options(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-a -s foo'") { + // const_cast the args to char** to match the actual signature of main(). + auto argv = std::array{"progname", "-a", "-s", "foo", + static_cast(nullptr)}; + auto ret = posix_parse(flags, argv.size() - 1, + const_cast(argv.data())) + .value(); + + THEN ("a_flag and s_flag are set") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "foo"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + } +} + +SCENARIO("Calling posix_parse() with required arguments", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with two required arguments") + { + struct test_flags + { + std::string arg1; + std::string arg2; + }; + + auto flags = options(option("arg1", &test_flags::arg1), + option("arg2", &test_flags::arg2)); + + WHEN ("We parse 'foo bar'") { + auto ret = posix_parse(flags, std::array{"foo", "bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2 == "bar"); + } + } + + WHEN ("We parse '-- -foo -bar'") { + auto ret = posix_parse(flags, std::array{"--", "-foo", "-bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "-foo"); + REQUIRE(ret->arg2 == "-bar"); + } + } + } +} + +SCENARIO("Calling posix_parse() with required and optional arguments", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with a required and an optional argument") + { + struct test_flags + { + std::string arg1; + std::optional arg2; + }; + + auto flags = options(option("arg1", &test_flags::arg1), + option("arg2", &test_flags::arg2)); + + WHEN ("We parse 'foo bar'") { + auto ret = posix_parse(flags, std::array{"foo", "bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2 == "bar"); + } + } + + WHEN ("We parse 'foo'") { + auto ret = posix_parse(flags, std::array{"foo"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("arg1 is set and arg2 is not") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2.has_value() == false); + } + } + } +} + +SCENARIO("Parsing integers with posix_parse()", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with an int16_t flag") + { + struct test_flags + { + std::int16_t iflag{}; + }; + + auto flags = options(option('i', "iflag", &test_flags::iflag)); + + WHEN ("The value is a small positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "30000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == 30000); + } + } + + WHEN ("The value is a small negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-30000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == -30000); + } + } + + WHEN ("The value is a large positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "40000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too large"); + } + } + + WHEN ("The value is a large negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-40000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too small"); + } + } + + WHEN ("The value is not a number") { + auto ret = posix_parse(flags, std::array{"-i", "foo"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected an integer"); + } + } + } + + GIVEN ("An options parser with a uint16_t flag") + { + struct test_flags + { + std::uint16_t iflag{}; + }; + + auto flags = options(option('i', "iflag", &test_flags::iflag)); + + WHEN ("The value is a positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "60000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == 60000); + } + } + + WHEN ("The value is a large positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "80000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too large"); + } + } + + WHEN ("The value is a negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-1"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected a non-negative integer"); + } + } + + WHEN ("The value is not a number") { + auto ret = posix_parse(flags, std::array{"-i", "foo"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected an integer"); + } + } + } +} + +} // anonymous namespace -- cgit v1.2.3