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

[MISC] Allow options for subcommands #244

Merged
merged 3 commits into from
Mar 13, 2024
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
159 changes: 84 additions & 75 deletions include/sharg/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#pragma once

#include <set>
#include <unordered_set>
#include <variant>

#include <sharg/config.hpp>
Expand Down Expand Up @@ -426,9 +426,6 @@ class parser
// Determine the format and subcommand.
determine_format_and_subcommand();

// If a subcommand was provided, check that it is valid.
verify_subcommand();

// Apply all defered operations to the parser, e.g., `add_option`, `add_flag`, `add_positional_option`.
for (auto & operation : operations)
operation();
Expand Down Expand Up @@ -512,7 +509,7 @@ class parser
}
}

if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
if (!used_ids.contains(std::string{id}))
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};

// we only need to search for an option before the `option_end_identifier` (`--`)
Expand Down Expand Up @@ -744,8 +741,8 @@ class parser
detail::format_copyright>
format{detail::format_short_help{}};

//!\brief List of option/flag identifiers that are already used.
std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
//!\brief List of option/flag identifiers (excluding -/--) that are already used.
std::unordered_set<std::string> used_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};

//!\brief The command line arguments that will be passed to the format.
std::vector<std::string> format_arguments{};
Expand All @@ -756,14 +753,18 @@ class parser
//!\brief The command that lead to calling this parser, e.g. [./build/bin/raptor, build]
std::vector<std::string> executable_name{};

//!\brief Set of option identifiers (including -/--) that have been added via `add_option`.
std::unordered_set<std::string> options{};

//!\brief Vector of functions that stores all calls.
std::vector<std::function<void()>> operations;

/*!\brief Initializes the sharg::parser class on construction.
/*!\brief Handles format and subcommand detection.
* \throws sharg::too_few_arguments if option --export-help was specified without a value
* \throws sharg::too_few_arguments if option --version-check was specified without a value
* \throws sharg::validation_error if the value passed to option --export-help was invalid.
* \throws sharg::validation_error if the value passed to option --version-check was invalid.
* \throws sharg::user_input_error if the subcommand is unknown.
* \details
*
* This function adds all command line parameters to the format_arguments member variable
Expand All @@ -788,23 +789,27 @@ class parser
void determine_format_and_subcommand()
{
assert(!arguments.empty());

auto it = arguments.begin();
std::string_view arg{*it};

executable_name.emplace_back(*it);
++it;
executable_name.emplace_back(arg);

// Helper function for going to the next argument. This makes it more obvious that we are
// Helper function for reading the next argument. This makes it more obvious that we are
// incrementing `it` (version-check, and export-help).
auto go_to_next_arg = [this, &it](std::string_view message) -> auto
auto read_next_arg = [this, &it, &arg]() -> bool
{
assert(it != arguments.end());

if (++it == arguments.end())
throw too_few_arguments{message.data()};
return false;

arg = *it;
return true;
};

// Helper function for finding and processing subcommands.
auto found_and_processed_subcommand = [this, &it](std::string_view arg) -> bool
auto found_subcommand = [this, &it, &arg]() -> bool
{
if (subcommands.empty())
return false;
Expand All @@ -824,25 +829,46 @@ class parser
}
else
eseiler marked this conversation as resolved.
Show resolved Hide resolved
{
// Positional options are forbidden by design. Todo: Allow options. Forbidden in check_option_config.
// Positional options are forbidden by design.
// Flags and options, which both start with '-', are allowed for the top-level parser.
// Otherwise, this is a wrongly spelled subcommand. The error will be thrown in parse().
// Otherwise, this is an unknown subcommand.
if (!arg.starts_with('-'))
{
format_arguments.emplace_back(arg);
return true;
std::string message = "You specified an unknown subcommand! Available subcommands are: [";
for (std::string const & command : subcommands)
message += command + ", ";
message.replace(message.size() - 2, 2, "]. Use -h/--help for more information.");

throw user_input_error{message};
}
}

return false;
};

// Process the arguments.
for (; it != arguments.end(); ++it)
for (; read_next_arg();)
{
std::string_view arg{*it};
// The argument is a known option.
if (options.contains(std::string{arg}))
{
// No futher checks are needed.
format_arguments.emplace_back(arg);

// Consume the next argument (the option value) if possible.
if (read_next_arg())
{
format_arguments.emplace_back(arg);
continue;
}
else // Too few arguments. This is handled by format_parse.
{
break;
}
}

if (found_and_processed_subcommand(arg))
// If we have a subcommand, all further arguments are passed to the subparser.
if (found_subcommand())
break;

if (arg == "-h" || arg == "--help")
Expand All @@ -868,8 +894,8 @@ class parser
// --export-help man
if (arg.empty())
{
go_to_next_arg("Option --export-help must be followed by a value.");
arg = *it;
if (!read_next_arg())
throw too_few_arguments{"Option --export-help must be followed by a value."};
}
else // --export-help=man
{
Expand All @@ -891,8 +917,8 @@ class parser
}
else if (arg == "--version-check")
{
go_to_next_arg("Option --version-check must be followed by a value.");
arg = *it;
if (!read_next_arg())
throw too_few_arguments{"Option --version-check must be followed by a value."};

if (arg == "1" || arg == "true")
version_check_user_decision = true;
Expand All @@ -903,6 +929,7 @@ class parser
}
else
{
// Flags, positional options, options using an alternative syntax (--optionValue, --option=value), etc.
format_arguments.emplace_back(arg);
}
}
Expand All @@ -927,7 +954,7 @@ class parser
{
if (detail::format_parse::is_empty_id(id))
return false;
return (!(used_option_ids.insert(std::string({id}))).second);
return (!(used_ids.insert(std::string({id}))).second);
}

/*!\brief Verifies that the short and the long identifiers are correctly formatted.
Expand All @@ -941,44 +968,47 @@ class parser
*/
void verify_identifiers(char const short_id, std::string const & long_id)
{
constexpr std::string_view valid_chars{"@_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
auto is_valid = [&valid_chars](char const c)
auto is_valid = [](char const c) -> bool
{
return valid_chars.find(c) != std::string::npos;
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') // alphanumeric
|| c == '@' || c == '_' || c == '-'; // additional characters
};

if (id_exists(short_id))
throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
if (id_exists(long_id))
throw design_error("Option Identifier '" + long_id + "' was already used before.");
if (long_id.length() == 1)
throw design_error("Long IDs must be either empty, or longer than one character.");
if ((short_id != '\0') && !is_valid(short_id))
throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
if (long_id.size() > 0 && (long_id[0] == '-'))
throw design_error("First character of long ID cannot be '-'.");

std::for_each(long_id.begin(),
long_id.end(),
[&is_valid](char c)
{
if (!((c == '-') || is_valid(c)))
throw design_error(
"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
});
if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
throw design_error("Option Identifiers cannot both be empty.");
if (short_id == '\0' && long_id.empty())
throw design_error{"Short and long identifiers may not both be empty."};

if (short_id != '\0')
{
if (short_id == '-' || !is_valid(short_id))
throw design_error{"Short identifiers may only contain alphanumeric characters, '_', or '@'."};
if (id_exists(short_id))
throw design_error{"Short identifier '" + std::string(1, short_id) + "' was already used before."};
}

if (!long_id.empty())
{
if (long_id.size() == 1)
throw design_error{"Long identifiers must be either empty or longer than one character."};
if (long_id[0] == '-')
throw design_error{"Long identifiers may not use '-' as first character."};
if (!std::ranges::all_of(long_id, is_valid))
throw design_error{"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'."};
if (id_exists(long_id))
throw design_error{"Long identifier '" + long_id + "' was already used before."};
}
}

//!brief Verify the configuration given to a sharg::parser::add_option call.
template <typename validator_t>
void verify_option_config(config<validator_t> const & config)
{
if (!subcommands.empty())
throw design_error{"You may only specify flags for the top-level parser."};

verify_identifiers(config.short_id, config.long_id);

if (config.short_id != '\0')
options.emplace(std::string{"-"} + config.short_id);
if (!config.long_id.empty())
options.emplace(std::string{"--"} + config.long_id);

if (config.required && !config.default_message.empty())
throw design_error{"A required option cannot have a default message."};
}
Expand All @@ -1005,7 +1035,7 @@ class parser
throw design_error{"Positional options are always required and therefore cannot be advanced nor hidden!"};

if (!subcommands.empty())
throw design_error{"You may only specify flags for the top-level parser."};
throw design_error{"You may only specify flags and options for the top-level parser."};

if (has_positional_list_option)
throw design_error{"You added a positional option with a list value before so you cannot add "
Expand Down Expand Up @@ -1075,27 +1105,6 @@ class parser
}
}

/*!\brief Verifies that the subcommand was correctly specified.
* \throws sharg::too_few_arguments if a subparser was configured at construction but a subcommand is missing.
*/
inline void verify_subcommand()
{
if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
{
assert(!subcommands.empty());
std::string subcommands_str{"["};
for (std::string const & command : subcommands)
subcommands_str += command + ", ";
subcommands_str.replace(subcommands_str.size() - 2, 2, "]"); // replace last ", " by "]"

throw too_few_arguments{"You misspelled the subcommand! Please specify which sub-program "
"you want to use: one of "
+ subcommands_str
+ ". Use -h/--help for more "
"information."};
}
}

/*!\brief Parses the command line arguments according to the format.
* \throws sharg::option_declared_multiple_times if an option that is not a list was declared multiple times.
* \throws sharg::user_input_error if an incorrect argument is given as (positional) option value.
Expand Down
33 changes: 33 additions & 0 deletions test/include/sharg/test/test_fixture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,39 @@ class test_fixture : public ::testing::Test
toggle_guardian();
return testing::internal::GetCapturedStdout();
}

static inline std::string basic_options_str = " Common options\n"
" -h, --help\n"
" Prints the help page.\n"
" -hh, --advanced-help\n"
" Prints the help page including advanced options.\n"
" --version\n"
" Prints the version information.\n"
" --copyright\n"
" Prints the copyright/license information.\n"
" --export-help (std::string)\n"
" Export the help page information. Value must be one of "
#if SHARG_HAS_TDL
"[html, man,\n ctd, cwl].\n";
#else
"[html, man].\n";
#endif

static inline std::string version_str(std::string_view const subcommand_with_dash)
{
return std::string{"VERSION\n"}
+ " Last update:\n"
" test_parser"
+ subcommand_with_dash.data()
+ " version:\n"
" Sharg version: "
+ sharg::sharg_version_cstring + '\n';
}

static inline std::string version_str()
{
return version_str("");
}
};

class early_exit_guardian
Expand Down
Loading
Loading