Skip to content

Commit

Permalink
Add support for multi value command line options
Browse files Browse the repository at this point in the history
  • Loading branch information
Zitrax committed Aug 27, 2023
1 parent e54490a commit 4367853
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 21 deletions.
107 changes: 89 additions & 18 deletions src/arg_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,31 @@

namespace zit {

/*
template <typename T>
concept HasValueType = requires { typename T::value_type; };
template <typename T>
struct ExtractValueType {
using type = T;
bool is_container = false;
};
template <HasValueType T>
struct ExtractValueType<T> {
using type = typename T::value_type;
bool is_container = true;
};
*/

/**
* Simple argument parser
*/
class ArgParser {
private:
public:
enum class Type { INT, UINT, FLOAT, STRING, BOOL };

private:
struct BaseArg {
explicit BaseArg(std::string option, Type type)
: m_option(std::move(option)), m_type(type) {}
Expand All @@ -35,6 +53,7 @@ class ArgParser {
[[nodiscard]] auto is_provided() const { return m_provided; }
[[nodiscard]] auto is_required() const { return m_required; }
[[nodiscard]] auto is_help_arg() const { return m_help_arg; }
[[nodiscard]] auto is_multi() const { return m_is_multi; }

void set_help_arg(bool help_arg) { m_help_arg = help_arg; }
void set_provided(bool provided) { m_provided = provided; }
Expand All @@ -47,6 +66,7 @@ class ArgParser {
bool m_provided = false;
bool m_required = false;
bool m_help_arg = false;
bool m_is_multi = false;
};

template <typename T>
Expand Down Expand Up @@ -75,43 +95,72 @@ class ArgParser {
: BaseArg(std::move(option), ArgParser::typeEnum<T>()) {
if constexpr (std::is_same_v<T, bool>) {
// Special for bool - a non-provided arg always default to false
m_dst = false;
m_default = {false};
}
}

void* dst() override { return &m_dst; }

[[nodiscard]] auto dst() const { return m_dst; }
void set_dst(T t) { m_dst = std::move(t); }
[[nodiscard]] auto dst() const { return m_dst.empty() ? m_default : m_dst; }
void set_dst(T t) {
if (!m_is_multi && !m_dst.empty()) {
throw std::runtime_error(
"Multiple values provided for single value option: " + m_option);
}
m_dst.emplace_back(t);
}

/** Help text for option */
auto& help(const std::string& help) {
m_help = help;
return *this;
}

/** Makes the option required */
auto& required() {
m_required = true;
return *this;
}

/** Default value for option */
auto& default_value(T t) {
m_dst = std::move(t);
m_default = {t};
return *this;
}

/** Default values for multi value option */
auto& default_value(std::vector<T> t) {
if (!m_is_multi && t.size() > 1) {
throw std::runtime_error(
"Can't default to more than one value for single value option");
}
m_default = t;
return *this;
}

/** Mark this option as providing help, for example for "--help". This will
* override the check on other required arguments */
auto& help_arg() {
m_help_arg = true;
return *this;
}

/** Provide aliases that can be used for the same option */
auto& aliases(const std::set<std::string>& aliases) {
m_aliases = aliases;
return *this;
}

/** Allow multiple values for the option. Then use get_multi to retrieve all
* the values. */
auto& multi() {
m_is_multi = true;
return *this;
}

private:
// NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members)
std::optional<T> m_dst{};
std::vector<T> m_dst;
std::vector<T> m_default;
};

template <typename T>
Expand All @@ -128,6 +177,22 @@ class ArgParser {
std::vector<std::unique_ptr<BaseArg>> m_options{};
bool m_parsed = false;

template <typename T>
[[nodiscard]] auto get_internal(const std::string& option) const {
const auto match = find(option);
if (match == m_options.end()) {
throw std::runtime_error("No option: " + option);
}
const auto* opt = dynamic_cast<const Arg<T>*>(match->get());
if (!opt) {
throw std::runtime_error("Invalid type for option: " + option);
}
if (opt->is_required() && !opt->is_provided()) {
throw std::runtime_error("No value for required option: " + option);
}
return std::make_tuple(opt->dst(), opt->is_multi());
}

public:
explicit ArgParser(std::string desc) : m_desc(std::move(desc)) {}

Expand All @@ -144,20 +209,26 @@ class ArgParser {

template <typename T>
[[nodiscard]] T get(const std::string& option) const {
const auto match = find(option);
if (match == m_options.end()) {
throw std::runtime_error("No option: " + option);
auto [dst, multi] = get_internal<T>(option);
if (multi) {
throw std::runtime_error(
"get() called on multi value option, use get_multi");
}
const auto* opt = dynamic_cast<const Arg<T>*>(match->get());
if (!opt) {
throw std::runtime_error("Invalid type for option: " + option);
if (!dst.empty()) {
return dst.front();
}
if (opt->is_required() && !opt->is_provided()) {
throw std::runtime_error("No value for required option: " + option);
throw std::runtime_error("No value provided for option: " + option);
}

template <typename T>
[[nodiscard]] std::vector<T> get_multi(const std::string& option) const {
auto [dst, multi] = get_internal<T>(option);
if (!multi) {
throw std::runtime_error(
"get?multi() called on single value option, use get");
}
auto dst = opt->dst();
if (dst.has_value()) {
return dst.value();
if (!dst.empty()) {
return dst;
}
throw std::runtime_error("No value provided for option: " + option);
}
Expand Down
51 changes: 48 additions & 3 deletions tests/test_arg_parser.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#include "arg_parser.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include <limits.h>
#include <stdexcept>

using namespace zit;

using ::testing::ElementsAreArray;

TEST(arg_parser, duplicate) {
ArgParser parser("desc");
parser.add_option<bool>("--test");
Expand Down Expand Up @@ -53,6 +56,8 @@ TEST(arg_parser, int) {
ArgParser parser("desc");
parser.add_option<int>("--test").default_value(2);
EXPECT_FALSE(parser.is_provided("--test"));
EXPECT_THROW(std::ignore = parser.get_multi<int>("--test"),
std::runtime_error);
EXPECT_EQ(parser.get<int>("--test"), 2);
}

Expand All @@ -66,17 +71,56 @@ TEST(arg_parser, int) {

{
ArgParser parser("desc");
parser.add_option<int>("--test").default_value(2);
parser.add_option<int>("--test");
parser.parse({"cmd", "--test", "-3"});
EXPECT_EQ(parser.get<int>("--test"), -3);
}

{
ArgParser parser("desc");
parser.add_option<int>("--test");
EXPECT_THROW(parser.parse({"cmd", "--test", "-3", "--test", "4"}),
std::runtime_error);
EXPECT_EQ(parser.get<int>("--test"), -3);
}
}

TEST(arg_parser, int_multi) {
{
ArgParser parser("desc");
auto& option = parser.add_option<int>("--test").multi();
EXPECT_EQ(option.get_type(), ArgParser::Type::INT);
parser.parse({"cmd", "--test", "-3"});
EXPECT_THROW(std::ignore = parser.get<int>("--test"), std::runtime_error);
EXPECT_THAT(parser.get_multi<int>("--test"), ElementsAreArray({-3}));
}

{
ArgParser parser("desc");
auto& option = parser.add_option<int>("--test").multi();
EXPECT_EQ(option.get_type(), ArgParser::Type::INT);
parser.parse({"cmd", "--test", "-3", "--test", "4"});
EXPECT_THROW(std::ignore = parser.get<int>("--test"), std::runtime_error);
EXPECT_THAT(parser.get_multi<int>("--test"), ElementsAreArray({-3, 4}));
}

{
ArgParser parser("desc");
auto& option =
parser.add_option<int>("--test").multi().default_value({1, 2, 3});
EXPECT_EQ(option.get_type(), ArgParser::Type::INT);
parser.parse({"cmd"});
EXPECT_THROW(std::ignore = parser.get<int>("--test"), std::runtime_error);
EXPECT_THAT(parser.get_multi<int>("--test"), ElementsAreArray({1, 2, 3}));
}
}

TEST(arg_parser, unsigned) {
{
ArgParser parser("desc");
parser.add_option<unsigned>("--test");
EXPECT_THROW(std::ignore = parser.get<unsigned>("--test"), std::runtime_error);
EXPECT_THROW(std::ignore = parser.get<unsigned>("--test"),
std::runtime_error);
}

{
Expand Down Expand Up @@ -138,7 +182,8 @@ TEST(arg_parser, string) {
{
ArgParser parser("desc");
parser.add_option<std::string>("--test");
EXPECT_THROW(std::ignore = parser.get<std::string>("--test"), std::runtime_error);
EXPECT_THROW(std::ignore = parser.get<std::string>("--test"),
std::runtime_error);
}

{
Expand Down

0 comments on commit 4367853

Please sign in to comment.