Skip to content
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
96 changes: 56 additions & 40 deletions include/argon/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,27 @@ class argument_parser {
return *this;
}

/**
* @brief Set the flag character.
* @param chr The flag character.
* @return Reference to the argument parser.
* @throws argon::invalid_configuration if the flag character is not a printable
* ASCII character or if any arguments have already been added to the parser.
* @note The default flag character is `'-'`.
*/
argument_parser& flag_char(const char chr) {
if (not this->_positional_args.empty() or not this->_optional_args.empty())
throw invalid_configuration("The flag character must be set before adding any "
"arguments!");

if (not std::isprint(static_cast<unsigned char>(chr)))
throw invalid_configuration("The flag character must be a printable ASCII character!");

this->_flag_char = chr;
this->_primary_flag_prefix = std::string(this->_primary_flag_prefix_length, chr);
return *this;
}

/**
* @brief Add default arguments to the argument parser.
* @tparam ArgvRange Type of the positional argument discriminator range.
Expand Down Expand Up @@ -305,10 +326,10 @@ class argument_parser {
) {
this->_validate_group(group);

const auto full_name = group._format_arg_name(base_name);
auto full_name = group._format_arg_name(base_name);
this->_verify_arg_name_pattern(full_name);

const detail::argument_name arg_name(std::make_optional<std::string>(full_name));
const detail::argument_name arg_name(std::move(full_name));
if (this->_is_arg_name_used(arg_name))
throw invalid_configuration::argument_name_used(arg_name);

Expand Down Expand Up @@ -368,18 +389,13 @@ class argument_parser {
) {
this->_validate_group(group);

const auto full_name = group._format_arg_name(base_name);
auto full_name = group._format_arg_name(base_name);
this->_verify_arg_name_pattern(full_name);

const auto arg_name =
name_discr == n_primary
? detail::
argument_name{std::make_optional<std::string>(full_name), std::nullopt, this->_flag_prefix_char}
: detail::argument_name{
std::nullopt,
std::make_optional<std::string>(full_name),
this->_flag_prefix_char
};
? detail::argument_name{std::move(full_name), "", this->_flag_char}
: detail::argument_name{"", std::move(full_name), this->_flag_char};

if (this->_is_arg_name_used(arg_name))
throw invalid_configuration::argument_name_used(arg_name);
Expand Down Expand Up @@ -407,16 +423,14 @@ class argument_parser {
) {
this->_validate_group(group);

const auto full_primary_name = group._format_arg_name(base_primary_name);
auto full_primary_name = group._format_arg_name(base_primary_name);
this->_verify_arg_name_pattern(full_primary_name);

const auto full_secondary_name = group._format_arg_name(base_secondary_name);
auto full_secondary_name = group._format_arg_name(base_secondary_name);
this->_verify_arg_name_pattern(full_secondary_name);

const detail::argument_name arg_name(
std::make_optional<std::string>(full_primary_name),
std::make_optional<std::string>(full_secondary_name),
this->_flag_prefix_char
std::move(full_primary_name), std::move(full_secondary_name), this->_flag_char
);
if (this->_is_arg_name_used(arg_name))
throw invalid_configuration::argument_name_used(arg_name);
Expand Down Expand Up @@ -954,12 +968,12 @@ class argument_parser {
arg_name, "An argument name cannot contain whitespaces."
);

if (arg_name.front() == this->_flag_prefix_char)
if (arg_name.front() == this->_flag_char)
throw invalid_configuration::invalid_argument_name(
arg_name,
std::format(
"An argument name cannot begin with a flag prefix character ({}).",
this->_flag_prefix_char
this->_flag_char
)
);

Expand All @@ -985,29 +999,19 @@ class argument_parser {
/**
* @brief Returns a unary predicate function which checks if the given name matches the argument's name
* @param arg_name The name of the argument.
* @param m_type The match type used within the predicate.
* @return Argument predicate based on the provided name.
*/
[[nodiscard]] auto _name_match_predicate(
const detail::argument_name& arg_name,
const detail::argument_name::match_type m_type = detail::argument_name::m_any
) const noexcept {
return [&arg_name, m_type](const arg_ptr_t& arg) {
return arg->name().match(arg_name, m_type);
};
[[nodiscard]] auto _name_match_predicate(const detail::argument_name& arg_name) const noexcept {
return [&arg_name](const arg_ptr_t& arg) { return arg->name().match(arg_name); };
}

/**
* @brief Check if an argument name is already used.
* @param arg_name The name of the argument.
* @param m_type The match type used to find the argument.
* @return True if the argument name is already used, false otherwise.
*/
[[nodiscard]] bool _is_arg_name_used(
const detail::argument_name& arg_name,
const detail::argument_name::match_type m_type = detail::argument_name::m_any
) const noexcept {
const auto predicate = this->_name_match_predicate(arg_name, m_type);
[[nodiscard]] bool _is_arg_name_used(const detail::argument_name& arg_name) const noexcept {
const auto predicate = this->_name_match_predicate(arg_name);

if (std::ranges::find_if(this->_positional_args, predicate) != this->_positional_args.end())
return true;
Expand Down Expand Up @@ -1167,10 +1171,10 @@ class argument_parser {
if (util::contains_whitespaces(arg_value))
return detail::argument_token::t_value;

if (arg_value.starts_with(this->_flag_prefix))
if (arg_value.starts_with(this->_primary_flag_prefix))
return detail::argument_token::t_flag_primary;

if (arg_value.starts_with(this->_flag_prefix_char))
if (arg_value.starts_with(this->_flag_char))
return detail::argument_token::t_flag_secondary;

return detail::argument_token::t_value;
Expand Down Expand Up @@ -1453,7 +1457,7 @@ class argument_parser {
* @return The argument with the specified name, if found; otherwise, std::nullopt.
* @throws argon::lookup_failure if an argument with the given name cannot be found.
*/
arg_ptr_t _get_argument(std::string_view arg_name) const {
[[nodiscard]] arg_ptr_t _get_argument(std::string_view arg_name) const {
const auto predicate = this->_name_match_predicate(arg_name);

if (auto pos_arg_it = std::ranges::find_if(this->_positional_args, predicate);
Expand Down Expand Up @@ -1543,14 +1547,19 @@ class argument_parser {
}
}

// --- attributes ---

std::string _name = ""; ///< The name of the parser.
std::string _program_name =
""; ///< The name of the program in the format "<parent-parser-names>... <program-name>".
std::optional<std::string> _program_version = std::nullopt; ///< The version of the program.
std::optional<std::string> _program_description =
std::nullopt; ///< The description of the program.
bool _verbose = false; ///< Verbosity flag.
unknown_policy _unknown_policy = unknown_policy::fail; ///< Policy for unknown arguments.
char _flag_char = '-'; ///< The character used as a flag prefix.
std::string _primary_flag_prefix = "--"; ///< The primary flag prefix.

// --- parsing cfg & state ---

arg_ptr_vec_t _positional_args = {}; ///< The list of positional arguments.
arg_ptr_vec_t _optional_args = {}; ///< The list of optional arguments.
Expand All @@ -1559,15 +1568,22 @@ class argument_parser {
argument_group& _gr_optional_args; ///< The optional argument group.
arg_parser_ptr_vec_t _subparsers = {}; ///< The list of subparsers.

bool _invoked =
// --- cfg flags ---

bool _verbose : 1 = false; ///< Verbosity flag.

// --- parsing state flags ---

bool _invoked : 1 =
false; ///< A flag indicating whether the parser has been invoked to parse arguments.
bool _finalized = false; ///< A flag indicating whether the parsing process has been finalized.
bool _finalized : 1 =
false; ///< A flag indicating whether the parsing process has been finalized.

// --- constants ---

static constexpr std::uint8_t _primary_flag_prefix_length = 2u;
static constexpr std::uint8_t _secondary_flag_prefix_length = 1u;
static constexpr char _flag_prefix_char = '-';
static constexpr std::string_view _flag_prefix = "--";
static constexpr std::uint8_t _indent_width = 2;
static constexpr std::uint8_t _indent_width = 2u;
};

namespace detail {
Expand Down
64 changes: 36 additions & 28 deletions include/argon/detail/argument_name.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

#include <cstdint>
#include <format>
#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <string_view>

Expand Down Expand Up @@ -40,15 +41,11 @@ struct argument_name {
* @param secondary The secondary (short) name of the argument.
* @param flag_char The flag character (used for optional argument names).
*/
argument_name(
std::optional<std::string> primary,
std::optional<std::string> secondary = std::nullopt,
std::optional<char> flag_char = std::nullopt
explicit argument_name(
std::string primary, std::string secondary = "", char flag_char = flag_char_sentinel
)
: primary(std::move(primary)),
secondary(std::move(secondary)),
flag_char(std::move(flag_char)) {
if (not (this->primary or this->secondary))
: primary(std::move(primary)), secondary(std::move(secondary)), flag_char(flag_char) {
if (this->primary.empty() and this->secondary.empty())
throw std::logic_error("An argument name cannot be empty! At least one of "
"primary/secondary must be specified");
}
Expand All @@ -65,6 +62,16 @@ struct argument_name {
return this->primary == other.primary and this->secondary == other.secondary;
}

/// @brief Checks if the argument name instance has a primary name.
[[nodiscard]] bool has_primary() const noexcept {
return not this->primary.empty();
}

/// @brief Checks if the argument name instance has a secondary name.
[[nodiscard]] bool has_secondary() const noexcept {
return not this->secondary.empty();
}

/**
* @brief Matches the given string to the argument_name instance.
* @param arg_name The name string to match.
Expand All @@ -75,11 +82,12 @@ struct argument_name {
const noexcept {
switch (m_type) {
case m_any:
return this->primary == arg_name or this->secondary == arg_name;
return (this->has_primary() and this->primary == arg_name)
or (this->has_secondary() and this->secondary == arg_name);
case m_primary:
return this->primary == arg_name;
return this->has_primary() and this->primary == arg_name;
case m_secondary:
return this->secondary == arg_name;
return this->has_secondary() and this->secondary == arg_name;
}

return false;
Expand All @@ -88,34 +96,31 @@ struct argument_name {
/**
* @brief Matches the given argument name to the argument_name instance.
* @param arg_name The argument_name instance to match.
* @param m_type UNUSED - necessary to match the signature of the `string_view` overload of the `match` function.
* @return True if arg_name's primary or secondary value matches the argument_name instance.
*/
[[nodiscard]] bool match(
const argument_name& arg_name, [[maybe_unused]] const match_type m_type = m_any
) const noexcept {
if (arg_name.primary and this->match(arg_name.primary.value()))
[[nodiscard]] bool match(const argument_name& arg_name) const noexcept {
if (arg_name.has_primary() and this->match(arg_name.primary))
return true;

if (arg_name.secondary)
return this->match(arg_name.secondary.value());
if (arg_name.has_secondary())
return this->match(arg_name.secondary);

return false;
}

/**
* @brief Get a string representation of the argument_name.
* @param flag_char The character used for the argument flag prefix.
*/
[[nodiscard]] std::string str() const noexcept {
// if flag_char = nullopt, then the fallback character doesn't matter - the string will be empty
const std::string fc(this->flag_char.has_value(), this->flag_char.value_or(char()));
const std::string fc(
static_cast<std::size_t>(this->flag_char != flag_char_sentinel), this->flag_char
);

std::string primary_str =
this->primary ? std::format("{}{}{}", fc, fc, this->primary.value()) : "";
std::string separator = this->primary and this->secondary ? ", " : "";
this->has_primary() ? std::format("{}{}{}", fc, fc, this->primary) : "";
std::string separator = this->has_primary() and this->has_secondary() ? ", " : "";
std::string secondary_str =
this->secondary ? std::format("{}{}", fc, this->secondary.value()) : "";
this->has_secondary() ? std::format("{}{}", fc, this->secondary) : "";

return std::format("{}{}{}", primary_str, separator, secondary_str);
}
Expand All @@ -131,9 +136,12 @@ struct argument_name {
return os;
}

const std::optional<std::string> primary; ///< The primary name of the argument.
const std::optional<std::string> secondary; ///< The optional (short) name of the argument.
const std::optional<char> flag_char; ///< The flag character (used for optional argument names).
const std::string primary; ///< The primary name of the argument.
const std::string secondary; ///< The optional (short) name of the argument.
const char flag_char; ///< The flag character (used for optional argument names).

// --- constants ---
static constexpr char flag_char_sentinel = '\0'; ///< Sentinel value for the flag character.
};

/**
Expand Down
6 changes: 2 additions & 4 deletions tests/include/argument_parser_test_fixture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,9 @@ struct argument_parser_test_fixture {
}

[[nodiscard]] argument_name init_arg_name(
const std::size_t i, std::optional<char> flag_char = std::nullopt
const std::size_t i, char flag_char = argument_name::flag_char_sentinel
) const {
return argument_name(
init_arg_name_primary(i), init_arg_name_secondary(i), std::move(flag_char)
);
return argument_name{init_arg_name_primary(i), init_arg_name_secondary(i), flag_char};
}

template <
Expand Down
Loading
Loading