diff --git a/build.bfg b/build.bfg index 52bc8d4..d0cd5f4 100644 --- a/build.bfg +++ b/build.bfg @@ -9,12 +9,12 @@ project('flags', version='1.0pre') global_options(['-std=c++1z', '-Wall', '-Wextra', '-Werror', '-pedantic'], lang='c++') mettle = package('mettle') -includes = header_directory('include', include='flags.h') +includes = header_directory('include', include='*.h') driver = test_driver( 'mettle -o verbose' ) -for src in find_files('test', '*.cc'): +for src in find_files('test/*.cc'): path = os.path.join('test', src.path.basename()) test(executable( splitext(path)[0], diff --git a/include/flags.h b/include/flags.h index 7dcd61b..3063f49 100644 --- a/include/flags.h +++ b/include/flags.h @@ -13,7 +13,7 @@ namespace flags { namespace detail { using argument_map = - std::unordered_map>; + std::unordered_map>>; // Non-destructively parses the argv tokens. // * If the token begins with a -, it will be considered an option. @@ -38,6 +38,11 @@ struct parser { private: // Advance the state machine for the current token. void churn(const std::string_view& item) { + if(item.empty()) + { + on_value(item); + return; + } item.at(0) == '-' ? on_option(item) : on_value(item); } @@ -70,7 +75,8 @@ struct parser { return; } // Consume the preceding option and assign its value. - options_.emplace(*current_option_, value); + // operator[] will insert an empty vector if needed + options_[*current_option_].emplace_back(std::move(value)); current_option_.reset(); } @@ -83,11 +89,21 @@ struct parser { inline std::optional get_value( const argument_map& options, const std::string_view& option) { if (const auto it = options.find(option); it != options.end()) { - return it->second; + // If a key exists, there must be at least one value + return it->second[0]; } return std::nullopt; } +// If a key exists, return a vector with its values +inline std::vector> get_values( + const argument_map& options, const std::string_view& option) { + if (const auto it = options.find(option); it != options.end()) { + return it->second; + } + return {}; +} + // Coerces the string value of the given option into . // If the value cannot be properly parsed or the key does not exist, returns // nullopt. @@ -131,6 +147,62 @@ inline std::optional get(const argument_map& options, return std::nullopt; } +// Coerces the string values of the given option into std::vector. +// If a value cannot be properly parsed it is not added. If there are +// no suitable values or the key does not exist, returns nullopt. +template +std::vector> get_multiple(const argument_map& options, + const std::string_view& option) { + std::vector> values; + const auto views = get_values(options, option); + for (const auto &view : views) { + if (!view) { + values.push_back(std::nullopt); + continue; + } + if (T value; std::istringstream(std::string(*view)) >> value) { + values.push_back(value); + } else { + values.push_back(std::nullopt); + } + } + return values; +} + +// Since the values are already stored as strings, there's no need to use `>>`. +template <> +inline std::vector> get_multiple( + const argument_map& options, const std::string_view& option) { + return get_values(options, option); +} + +template <> +inline std::vector> get_multiple( + const argument_map& options, const std::string_view& option) { + const auto views = get_values(options, option); + std::vector> values(views.begin(), views.end()); + return values; +} + +// Special case for booleans: if the value is in the falsities array (see get) +// the option will be considered falsy. Otherwise, it will be considered truthy just +// for being present. +template <> +inline std::vector> get_multiple( + const argument_map& options, const std::string_view& option) { + const auto views = get_values(options, option); + std::vector> values; + for (const auto view : views) { + if (!view) { + values.push_back(true); + continue; + } + values.push_back(std::none_of(falsities.begin(), falsities.end(), + [&view](auto falsity) { return view == falsity; })); + } + return values; +} + // Coerces the string value of the given positional index into . // If the value cannot be properly parsed or the key does not exist, returns // nullopt. @@ -181,6 +253,22 @@ struct args { return get(option).value_or(default_value); } + template + std::vector> get_multiple(const std::string_view& option) const { + return detail::get_multiple(parser_.options(), option); + } + + template + std::vector get_multiple(const std::string_view& option, T&& default_value) const { + const auto items = get_multiple(option); + std::vector values; + values.reserve(items.size()); + for(const auto& item : items) { + values.push_back(item ? *item : default_value); + } + return values; + } + template std::optional get(size_t positional_index) const { return detail::get(parser_.positional_arguments(), positional_index); diff --git a/test/flags.cc b/test/flags.cc index d50a76f..e40a88c 100644 --- a/test/flags.cc +++ b/test/flags.cc @@ -5,6 +5,8 @@ #include #include #include +#include +#include using namespace mettle; @@ -39,6 +41,19 @@ char** initialize_argv(const std::initializer_list args) { return argv; } +// Same as above but using a std::vector +char** initialize_argv(const std::vector& args) { + char** argv = + reinterpret_cast(malloc((args.size() + 2) * sizeof(char*))); + initialize_arg(argv, 0, "TEST"); + std::size_t i = 0; + for (const auto& arg : args) { + initialize_arg(argv, ++i, arg); + } + argv[args.size() + 1] = NULL; + return argv; +} + // Cleans up every item within argv, then argv itself. void cleanup_argv(char** argv) { size_t index = 0; @@ -52,6 +67,9 @@ struct args_fixture { static args_fixture create(const std::initializer_list args) { return {args.size(), initialize_argv(args)}; } + static args_fixture create(const std::vector& args) { + return {args.size(), initialize_argv(args)}; + } ~args_fixture() { cleanup_argv(argv_); } size_t argc() const { return argc_; } @@ -184,6 +202,56 @@ suite<> flag_parsing("flag parsing", [](auto& _) { equal_to(std::string(LOREM_IPSUM))); }); + // Empty values + _.test("empty", [](){ + const auto fixture = args_fixture::create({"--foo", ""}); + expect(*fixture.args().get("foo"), equal_to("")); + }); + + // Multiple values for one flag + _.test("multiple", [](){ + const auto fixture = args_fixture::create({"--foo", "bar", "--foo", "baz"}); + auto foo = fixture.args().get_multiple("foo"); + expect(foo.size(), equal_to(2)); + expect(foo[0].value(), equal_to("bar")); + expect(foo[1].value(), equal_to("baz")); + }); + + _.test("multiple with type", [](){ + const auto fixture = args_fixture::create({"-x", "-x", "1", "-x", "2"}); + auto x = fixture.args().get_multiple("x", 0); + expect(x.size(), equal_to(3)); + expect(x[0], equal_to(0)); + expect(x[1], equal_to(1)); + expect(x[2], equal_to(2)); + }); + + _.test("multiple falsities", [](){ + std::vector args; + for (const auto falsity : flags::detail::falsities) { + args.push_back("--foo"); + args.push_back(falsity); + } + const auto fixture = args_fixture::create(args); + const auto foos1 = fixture.args().get_multiple("foo"); + for (const auto& foo : foos1) { + expect(foo && *foo, equal_to(false)); + } + const auto foos2 = fixture.args().get_multiple("foo", true); + for (const auto& foo : foos2) { + expect(foo, equal_to(false)); + } + }); + + _.test("multiple valueless flags", [](){ + const auto fixture = args_fixture::create({"--foo", "-foo", "--foo", "-foo"}); + const auto foos = fixture.args().get_multiple("foo"); + expect(foos.size(), equal_to(4)); + for (const auto& foo : foos) { + expect(static_cast(foo), equal_to(true)); + } + }); + // Basic number parsing. Verifying ints are truncated and doubles are // succesfully parsed. _.test("numbers", []() {