Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix build, Add support for multiple values and empty values #14

Merged
merged 4 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.bfg
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
94 changes: 91 additions & 3 deletions include/flags.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace flags {
namespace detail {
using argument_map =
std::unordered_map<std::string_view, std::optional<std::string_view>>;
std::unordered_map<std::string_view, std::vector<std::optional<std::string_view>>>;

// Non-destructively parses the argv tokens.
// * If the token begins with a -, it will be considered an option.
Expand All @@ -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);
}

Expand Down Expand Up @@ -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();
}

Expand All @@ -83,11 +89,21 @@ struct parser {
inline std::optional<std::string_view> 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<std::optional<std::string_view>> 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 <T>.
// If the value cannot be properly parsed or the key does not exist, returns
// nullopt.
Expand Down Expand Up @@ -131,6 +147,62 @@ inline std::optional<bool> get(const argument_map& options,
return std::nullopt;
}

// Coerces the string values of the given option into std::vector<T>.
// 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 <class T>
std::vector<std::optional<T>> get_multiple(const argument_map& options,
const std::string_view& option) {
std::vector<std::optional<T>> 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<std::optional<std::string_view>> get_multiple(
const argument_map& options, const std::string_view& option) {
return get_values(options, option);
}

template <>
inline std::vector<std::optional<std::string>> get_multiple(
const argument_map& options, const std::string_view& option) {
const auto views = get_values(options, option);
std::vector<std::optional<std::string>> values(views.begin(), views.end());
return values;
}

// Special case for booleans: if the value is in the falsities array (see get<bool>)
// the option will be considered falsy. Otherwise, it will be considered truthy just
// for being present.
template <>
inline std::vector<std::optional<bool>> get_multiple(
const argument_map& options, const std::string_view& option) {
const auto views = get_values(options, option);
std::vector<std::optional<bool>> 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 <T>.
// If the value cannot be properly parsed or the key does not exist, returns
// nullopt.
Expand Down Expand Up @@ -181,6 +253,22 @@ struct args {
return get<T>(option).value_or(default_value);
}

template <class T>
std::vector<std::optional<T>> get_multiple(const std::string_view& option) const {
return detail::get_multiple<T>(parser_.options(), option);
}

template <class T>
std::vector<T> get_multiple(const std::string_view& option, T&& default_value) const {
const auto items = get_multiple<T>(option);
std::vector<T> values;
values.reserve(items.size());
for(const auto& item : items) {
values.push_back(item ? *item : default_value);
}
return values;
}

template <class T>
std::optional<T> get(size_t positional_index) const {
return detail::get<T>(parser_.positional_arguments(), positional_index);
Expand Down
68 changes: 68 additions & 0 deletions test/flags.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <cstring>
#include <algorithm>

using namespace mettle;

Expand Down Expand Up @@ -39,6 +41,19 @@ char** initialize_argv(const std::initializer_list<const char*> args) {
return argv;
}

// Same as above but using a std::vector
char** initialize_argv(const std::vector<const char*>& args) {
char** argv =
reinterpret_cast<char**>(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;
Expand All @@ -52,6 +67,9 @@ struct args_fixture {
static args_fixture create(const std::initializer_list<const char*> args) {
return {args.size(), initialize_argv(args)};
}
static args_fixture create(const std::vector<const char*>& args) {
return {args.size(), initialize_argv(args)};
}
~args_fixture() { cleanup_argv(argv_); }

size_t argc() const { return argc_; }
Expand Down Expand Up @@ -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<std::string>("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<std::string>("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<int>("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<const char*> 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<bool>("foo");
for (const auto& foo : foos1) {
expect(foo && *foo, equal_to(false));
}
const auto foos2 = fixture.args().get_multiple<bool>("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<bool>("foo");
expect(foos.size(), equal_to(4));
for (const auto& foo : foos) {
expect(static_cast<bool>(foo), equal_to(true));
}
});

// Basic number parsing. Verifying ints are truncated and doubles are
// succesfully parsed.
_.test("numbers", []() {
Expand Down