Skip to content

Commit

Permalink
[MISC] Refactor parse()
Browse files Browse the repository at this point in the history
  • Loading branch information
eseiler committed Feb 22, 2024
1 parent c0556da commit 9f9ad78
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 60 deletions.
150 changes: 100 additions & 50 deletions include/sharg/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ class parser
* \throws sharg::too_many_arguments if the command line call contained more arguments than expected.
* \throws sharg::too_few_arguments if the command line call contained less arguments than expected.
* \throws sharg::validation_error if the argument was not excepted by the provided validator.
* \throws sharg::user_input_error if a subparser was configured at construction but a subcommand is missing.
*
* \details
*
Expand Down Expand Up @@ -419,63 +418,25 @@ class parser
if (parse_was_called)
throw design_error("The function parse() must only be called once!");

// Before creating the detail::version_checker, we have to make sure that
// malicious code cannot be injected through the app name.
if (!std::regex_match(info.app_name, app_name_regex))
{
throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\").")};
}

for (auto & sub : this->subcommands)
{
if (!std::regex_match(sub, app_name_regex))
{
throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\")."};
}
}
parse_was_called = true;

detail::version_checker app_version{info.app_name, info.version, info.url};
// User input sanitization must happen before version check!
verify_app_and_subcommand_names();

init();

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."};
}

if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
{
// must be done before calling parse on the format because this might std::exit
std::promise<bool> app_version_prom;
version_check_future = app_version_prom.get_future();
app_version(std::move(app_version_prom));
}
// 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();

std::visit(
[this]<typename T>(T & f)
{
if constexpr (std::same_as<T, detail::format_tdl>)
f.parse(info, executable_name);
else
f.parse(info);
},
format);
parse_was_called = true;
// The version check, which might exit the program, must be called before calling parse on the format.
run_version_check();

// Parse the command line arguments.
parse_format();

// Exit after parsing any special format.
if (!std::holds_alternative<detail::format_parse>(format))
Expand Down Expand Up @@ -1062,6 +1023,95 @@ class parser
if (parse_was_called)
throw design_error{detail::to_string(function_name.data(), " may only be used before calling parse().")};
}

/*!\brief Verifies that the app and subcommand names are correctly formatted.
* \throws sharg::design_error if the app name is not correctly formatted.
* \throws sharg::design_error if the subcommand names are not correctly formatted.
* \details
* The app name must only contain alphanumeric characters, '_', or '-'.
* The subcommand names must only contain alphanumeric characters, '_', or '-'.
*/
inline void verify_app_and_subcommand_names() const
{
// Before creating the detail::version_checker, we have to make sure that
// malicious code cannot be injected through the app name.
if (!std::regex_match(info.app_name, app_name_regex))
{
throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\").")};
}

for (auto & sub : this->subcommands)
{
if (!std::regex_match(sub, app_name_regex))
{
throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\")."};
}
}
}

/*!\brief Runs the version check if the user has not disabled it.
* \details
* If the user has not disabled the version check, the function will start a detached thread that will call the
* sharg::detail::version_checker and print a message if a new version is available.
*/
inline void run_version_check()
{
detail::version_checker app_version{info.app_name, info.version, info.url};

if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
{
// must be done before calling parse on the format because this might std::exit
std::promise<bool> app_version_prom;
version_check_future = app_version_prom.get_future();
app_version(std::move(app_version_prom));
}
}

/*!\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.
* \throws sharg::required_option_missing if the user did not provide a required option.
* \throws sharg::too_many_arguments if the command line call contained more arguments than expected.
* \throws sharg::too_few_arguments if the command line call contained less arguments than expected.
* \throws sharg::validation_error if the argument was not excepted by the provided validator.
* \details
* This function calls the parse function of the format member variable.
*/
inline void parse_format()
{
auto format_parse_fn = [this]<typename format_t>(format_t & f)
{
if constexpr (std::same_as<format_t, detail::format_tdl>)
f.parse(info, executable_name);
else
f.parse(info);
};

std::visit(std::move(format_parse_fn), format);
}
};

} // namespace sharg
12 changes: 6 additions & 6 deletions test/unit/parser/format_parse_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -681,16 +681,16 @@ TEST_F(format_parse_test, subcommand_parser_success)
TEST_F(format_parse_test, subcommand_parser_error)
{
// incorrect sub command regardless of following arguments, https://github.com/seqan/seqan3/issues/2172
auto top_level_parser = get_subcommand_parser({"subiddysub", "-f"}, {"sub1"});
EXPECT_THROW(top_level_parser.parse(), sharg::parser_error);
auto parser = get_subcommand_parser({"subiddysub", "-f"}, {"sub1"});
EXPECT_THROW(parser.parse(), sharg::parser_error);

// incorrect sub command with no other arguments
top_level_parser = get_subcommand_parser({"subiddysub"}, {"sub1"});
EXPECT_THROW(top_level_parser.parse(), sharg::parser_error);
parser = get_subcommand_parser({"subiddysub"}, {"sub1"});
EXPECT_THROW(parser.parse(), sharg::parser_error);

// incorrect sub command with trailing special option, https://github.com/seqan/sharg-parser/issues/171
top_level_parser = get_subcommand_parser({"subiddysub", "-h"}, {"sub1"});
EXPECT_THROW(top_level_parser.parse(), sharg::parser_error);
parser = get_subcommand_parser({"subiddysub", "-h"}, {"sub1"});
EXPECT_THROW(parser.parse(), sharg::parser_error);
}

TEST_F(format_parse_test, issue1544)
Expand Down
9 changes: 5 additions & 4 deletions test/unit/parser/parser_design_error_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,18 @@ TEST_F(design_error_test, subcommand_parser_error)
EXPECT_THROW(parser.parse(), sharg::design_error);

// no positional options are allowed
parser = get_subcommand_parser({"foo"}, {"foo"});
parser = get_subcommand_parser({"-f", "foo"}, {"foo"});

EXPECT_THROW(parser.add_positional_option(flag_value, sharg::config{}), sharg::design_error);
EXPECT_THROW(parser.add_option(flag_value, sharg::config{.short_id = 'o'}), sharg::design_error);
EXPECT_NO_THROW(parser.add_flag(flag_value, sharg::config{.short_id = 'f'}));
EXPECT_EQ(flag_value, false);

EXPECT_THROW(parser.get_sub_parser(), sharg::design_error);
EXPECT_EQ(flag_value, false);
EXPECT_NO_THROW(parser.parse()); // Prints nothing, but sets sub_parser.
EXPECT_NO_THROW(parser.get_sub_parser());
EXPECT_EQ(flag_value, true);

flag_value = false;

// no options are allowed
parser = get_subcommand_parser({"-o", "true"}, {"foo"});
Expand All @@ -282,7 +284,6 @@ TEST_F(design_error_test, subcommand_parser_error)
EXPECT_EQ(flag_value, false);
EXPECT_THROW(parser.parse(), sharg::too_few_arguments);
EXPECT_EQ(flag_value, false);
EXPECT_THROW(parser.parse(), sharg::user_input_error); // Todo: Subcommand is required?
}

TEST_F(design_error_test, not_allowed_after_parse)
Expand Down

0 comments on commit 9f9ad78

Please sign in to comment.