Refactor parser

Major refactor of the parsing code organisation to improve encapsulation
and not modify the input arguments. The returned result no longer has
pointers into the original option specification.
This commit is contained in:
Jarryd Beck 2020-09-23 20:05:00 +10:00
parent fd5cdfd547
commit fedf9d7b57
2 changed files with 190 additions and 135 deletions

View File

@ -30,6 +30,7 @@ THE SOFTWARE.
#include <exception> #include <exception>
#include <iostream> #include <iostream>
#include <limits> #include <limits>
#include <list>
#include <map> #include <map>
#include <memory> #include <memory>
#include <regex> #include <regex>
@ -1013,6 +1014,7 @@ namespace cxxopts
, m_value(std::move(val)) , m_value(std::move(val))
, m_count(0) , m_count(0)
{ {
m_hash = std::hash<std::string>{}(m_long + m_short);
} }
OptionDetails(const OptionDetails& rhs) OptionDetails(const OptionDetails& rhs)
@ -1058,12 +1060,20 @@ namespace cxxopts
return m_long; return m_long;
} }
size_t
hash() const
{
return m_hash;
}
private: private:
std::string m_short; std::string m_short;
std::string m_long; std::string m_long;
String m_desc; String m_desc;
std::shared_ptr<const Value> m_value; std::shared_ptr<const Value> m_value;
int m_count; int m_count;
size_t m_hash;
}; };
struct HelpOptionDetails struct HelpOptionDetails
@ -1198,45 +1208,65 @@ namespace cxxopts
std::string m_value; std::string m_value;
}; };
using ParsedHashMap = std::unordered_map<size_t, OptionValue>;
using NameHashMap = std::unordered_map<std::string, size_t>;
class ParseResult class ParseResult
{ {
public: public:
ParseResult( ParseResult() {}
std::shared_ptr<
std::unordered_map<std::string, std::shared_ptr<OptionDetails>> ParseResult(const ParseResult&) = default;
>,
std::vector<std::string>, ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector<KeyValue> sequential, std::vector<std::string>&& unmatched_args)
bool allow_unrecognised, : m_keys(std::move(keys))
int&, const char**&); , m_values(std::move(values))
, m_sequential(std::move(sequential))
, m_unmatched(std::move(unmatched_args))
{
}
ParseResult& operator=(ParseResult&&) = default;
ParseResult& operator=(const ParseResult&) = default;
size_t size_t
count(const std::string& o) const count(const std::string& o) const
{ {
auto iter = m_options->find(o); auto iter = m_keys.find(o);
if (iter == m_options->end()) if (iter == m_keys.end())
{ {
return 0; return 0;
} }
auto riter = m_results.find(iter->second); auto viter = m_values.find(iter->second);
return riter->second.count(); if (viter == m_values.end())
{
return 0;
}
return viter->second.count();
} }
const OptionValue& const OptionValue&
operator[](const std::string& option) const operator[](const std::string& option) const
{ {
auto iter = m_options->find(option); auto iter = m_keys.find(option);
if (iter == m_options->end()) if (iter == m_keys.end())
{ {
throw_or_mimic<option_not_present_exception>(option); throw_or_mimic<option_not_present_exception>(option);
} }
auto riter = m_results.find(iter->second); auto viter = m_values.find(iter->second);
return riter->second; if (viter == m_values.end())
{
throw_or_mimic<option_not_present_exception>(option);
}
return viter->second;
} }
const std::vector<KeyValue>& const std::vector<KeyValue>&
@ -1245,49 +1275,17 @@ namespace cxxopts
return m_sequential; return m_sequential;
} }
const std::vector<std::string>&
unmatched() const
{
return m_unmatched;
}
private: private:
NameHashMap m_keys;
void ParsedHashMap m_values;
parse(int& argc, const char**& argv);
void
add_to_option(const std::string& option, const std::string& arg);
bool
consume_positional(const std::string& a);
void
parse_option
(
const std::shared_ptr<OptionDetails>& value,
const std::string& name,
const std::string& arg = ""
);
void
parse_default(const std::shared_ptr<OptionDetails>& details);
void
checked_parse_arg
(
int argc,
const char* argv[],
int& current,
const std::shared_ptr<OptionDetails>& value,
const std::string& name
);
const std::shared_ptr<
std::unordered_map<std::string, std::shared_ptr<OptionDetails>>
> m_options;
std::vector<std::string> m_positional;
std::vector<std::string>::iterator m_next_positional;
std::unordered_set<std::string> m_positional_set;
std::unordered_map<std::shared_ptr<OptionDetails>, OptionValue> m_results;
bool m_allow_unrecognised;
std::vector<KeyValue> m_sequential; std::vector<KeyValue> m_sequential;
std::vector<std::string> m_unmatched;
}; };
struct Option struct Option
@ -1312,9 +1310,66 @@ namespace cxxopts
std::string arg_help_; std::string arg_help_;
}; };
using OptionMap = std::unordered_map<std::string, std::shared_ptr<OptionDetails>>;
using PositionalList = std::vector<std::string>;
using PositionalListIterator = PositionalList::const_iterator;
class OptionParser
{
public:
OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised)
: m_options(options)
, m_positional(positional)
, m_allow_unrecognised(allow_unrecognised)
{
}
ParseResult
parse(int argc, const char** argv);
bool
consume_positional(const std::string& a, PositionalListIterator& next);
void
checked_parse_arg
(
int argc,
const char* argv[],
int& current,
const std::shared_ptr<OptionDetails>& value,
const std::string& name
);
void
add_to_option(OptionMap::const_iterator iter, const std::string& option, const std::string& arg);
void
parse_option
(
const std::shared_ptr<OptionDetails>& value,
const std::string& name,
const std::string& arg = ""
);
void
parse_default(const std::shared_ptr<OptionDetails>& details);
private:
void finalise_aliases();
const OptionMap& m_options;
const PositionalList& m_positional;
std::vector<KeyValue> m_sequential;
bool m_allow_unrecognised;
ParsedHashMap m_parsed;
NameHashMap m_keys;
};
class Options class Options
{ {
using OptionMap = std::unordered_map<std::string, std::shared_ptr<OptionDetails>>;
public: public:
explicit Options(std::string program, std::string help_string = "") explicit Options(std::string program, std::string help_string = "")
@ -1325,7 +1380,6 @@ namespace cxxopts
, m_show_positional(false) , m_show_positional(false)
, m_allow_unrecognised(false) , m_allow_unrecognised(false)
, m_options(std::make_shared<OptionMap>()) , m_options(std::make_shared<OptionMap>())
, m_next_positional(m_positional.end())
{ {
} }
@ -1358,7 +1412,7 @@ namespace cxxopts
} }
ParseResult ParseResult
parse(int& argc, const char**& argv); parse(int argc, const char** argv);
OptionAdder OptionAdder
add_options(std::string group = ""); add_options(std::string group = "");
@ -1444,11 +1498,13 @@ namespace cxxopts
std::shared_ptr<OptionMap> m_options; std::shared_ptr<OptionMap> m_options;
std::vector<std::string> m_positional; std::vector<std::string> m_positional;
std::vector<std::string>::iterator m_next_positional;
std::unordered_set<std::string> m_positional_set; std::unordered_set<std::string> m_positional_set;
//mapping from groups to help options //mapping from groups to help options
std::map<std::string, HelpGroupDetails> m_help; std::map<std::string, HelpGroupDetails> m_help;
std::list<OptionDetails> m_option_list;
std::unordered_map<std::string, decltype(m_option_list)::iterator> m_option_map;
}; };
class OptionAdder class OptionAdder
@ -1609,24 +1665,6 @@ namespace cxxopts
} }
} // namespace } // namespace
inline
ParseResult::ParseResult
(
std::shared_ptr<
std::unordered_map<std::string, std::shared_ptr<OptionDetails>>
> options,
std::vector<std::string> positional,
bool allow_unrecognised,
int& argc, const char**& argv
)
: m_options(std::move(options))
, m_positional(std::move(positional))
, m_next_positional(m_positional.begin())
, m_allow_unrecognised(allow_unrecognised)
{
parse(argc, argv);
}
inline inline
void void
Options::add_options Options::add_options
@ -1706,21 +1744,24 @@ OptionAdder::operator()
inline inline
void void
ParseResult::parse_default(const std::shared_ptr<OptionDetails>& details) OptionParser::parse_default(const std::shared_ptr<OptionDetails>& details)
{ {
m_results[details].parse_default(details); // TODO: remove the duplicate code here
auto& store = m_parsed[details->hash()];
store.parse_default(details);
} }
inline inline
void void
ParseResult::parse_option OptionParser::parse_option
( (
const std::shared_ptr<OptionDetails>& value, const std::shared_ptr<OptionDetails>& value,
const std::string& /*name*/, const std::string& /*name*/,
const std::string& arg const std::string& arg
) )
{ {
auto& result = m_results[value]; auto hash = value->hash();
auto& result = m_parsed[hash];
result.parse(value, arg); result.parse(value, arg);
m_sequential.emplace_back(value->long_name(), arg); m_sequential.emplace_back(value->long_name(), arg);
@ -1728,7 +1769,7 @@ ParseResult::parse_option
inline inline
void void
ParseResult::checked_parse_arg OptionParser::checked_parse_arg
( (
int argc, int argc,
const char* argv[], const char* argv[],
@ -1764,43 +1805,36 @@ ParseResult::checked_parse_arg
inline inline
void void
ParseResult::add_to_option(const std::string& option, const std::string& arg) OptionParser::add_to_option(OptionMap::const_iterator iter, const std::string& option, const std::string& arg)
{ {
auto iter = m_options->find(option);
if (iter == m_options->end())
{
throw_or_mimic<option_not_exists_exception>(option);
}
parse_option(iter->second, option, arg); parse_option(iter->second, option, arg);
} }
inline inline
bool bool
ParseResult::consume_positional(const std::string& a) OptionParser::consume_positional(const std::string& a, PositionalListIterator& next)
{ {
while (m_next_positional != m_positional.end()) while (next != m_positional.end())
{ {
auto iter = m_options->find(*m_next_positional); auto iter = m_options.find(*next);
if (iter != m_options->end()) if (iter != m_options.end())
{ {
auto& result = m_results[iter->second]; auto& result = m_parsed[iter->second->hash()];
if (!iter->second->value().is_container()) if (!iter->second->value().is_container())
{ {
if (result.count() == 0) if (result.count() == 0)
{ {
add_to_option(*m_next_positional, a); add_to_option(iter, *next, a);
++m_next_positional; ++next;
return true; return true;
} }
++m_next_positional; ++next;
continue; continue;
} }
add_to_option(*m_next_positional, a); add_to_option(iter, *next, a);
return true; return true;
} }
throw_or_mimic<option_not_exists_exception>(*m_next_positional); throw_or_mimic<option_not_exists_exception>(*next);
} }
return false; return false;
@ -1818,7 +1852,6 @@ void
Options::parse_positional(std::vector<std::string> options) Options::parse_positional(std::vector<std::string> options)
{ {
m_positional = std::move(options); m_positional = std::move(options);
m_next_positional = m_positional.begin();
m_positional_set.insert(m_positional.begin(), m_positional.end()); m_positional_set.insert(m_positional.begin(), m_positional.end());
} }
@ -1832,21 +1865,21 @@ Options::parse_positional(std::initializer_list<std::string> options)
inline inline
ParseResult ParseResult
Options::parse(int& argc, const char**& argv) Options::parse(int argc, const char** argv)
{ {
ParseResult result(m_options, m_positional, m_allow_unrecognised, argc, argv); OptionParser parser(*m_options, m_positional, m_allow_unrecognised);
return result;
return parser.parse(argc, argv);
} }
inline inline ParseResult
void OptionParser::parse(int argc, const char** argv)
ParseResult::parse(int& argc, const char**& argv)
{ {
int current = 1; int current = 1;
int nextKeep = 1;
bool consume_remaining = false; bool consume_remaining = false;
PositionalListIterator next_positional = m_positional.begin();
std::vector<std::string> unmatched;
while (current != argc) while (current != argc)
{ {
@ -1873,13 +1906,12 @@ ParseResult::parse(int& argc, const char**& argv)
//if true is returned here then it was consumed, otherwise it is //if true is returned here then it was consumed, otherwise it is
//ignored //ignored
if (consume_positional(argv[current])) if (consume_positional(argv[current], next_positional))
{ {
} }
else else
{ {
argv[nextKeep] = argv[current]; unmatched.push_back(argv[current]);
++nextKeep;
} }
//if we return from here then it was parsed successfully, so continue //if we return from here then it was parsed successfully, so continue
} }
@ -1893,9 +1925,9 @@ ParseResult::parse(int& argc, const char**& argv)
for (std::size_t i = 0; i != s.size(); ++i) for (std::size_t i = 0; i != s.size(); ++i)
{ {
std::string name(1, s[i]); std::string name(1, s[i]);
auto iter = m_options->find(name); auto iter = m_options.find(name);
if (iter == m_options->end()) if (iter == m_options.end())
{ {
if (m_allow_unrecognised) if (m_allow_unrecognised)
{ {
@ -1927,15 +1959,14 @@ ParseResult::parse(int& argc, const char**& argv)
{ {
const std::string& name = result[1]; const std::string& name = result[1];
auto iter = m_options->find(name); auto iter = m_options.find(name);
if (iter == m_options->end()) if (iter == m_options.end())
{ {
if (m_allow_unrecognised) if (m_allow_unrecognised)
{ {
// keep unrecognised options in argument list, skip to next argument // keep unrecognised options in argument list, skip to next argument
argv[nextKeep] = argv[current]; unmatched.push_back(argv[current]);
++nextKeep;
++current; ++current;
continue; continue;
} }
@ -1964,12 +1995,13 @@ ParseResult::parse(int& argc, const char**& argv)
++current; ++current;
} }
for (auto& opt : *m_options) for (auto& opt : m_options)
{ {
auto& detail = opt.second; auto& detail = opt.second;
const auto& value = detail->value(); const auto& value = detail->value();
auto& store = m_results[detail]; //auto& store = m_results[detail];
auto& store = m_parsed[detail->hash()];
if(value.has_default() && !store.count() && !store.has_default()){ if(value.has_default() && !store.count() && !store.has_default()){
parse_default(detail); parse_default(detail);
@ -1980,7 +2012,7 @@ ParseResult::parse(int& argc, const char**& argv)
{ {
while (current < argc) while (current < argc)
{ {
if (!consume_positional(argv[current])) { if (!consume_positional(argv[current], next_positional)) {
break; break;
} }
++current; ++current;
@ -1988,14 +2020,33 @@ ParseResult::parse(int& argc, const char**& argv)
//adjust argv for any that couldn't be swallowed //adjust argv for any that couldn't be swallowed
while (current != argc) { while (current != argc) {
argv[nextKeep] = argv[current]; unmatched.push_back(argv[current]);
++nextKeep;
++current; ++current;
} }
} }
argc = nextKeep; finalise_aliases();
ParseResult parsed(std::move(m_keys), std::move(m_parsed), std::move(m_sequential), std::move(unmatched));
return parsed;
}
inline
void
OptionParser::finalise_aliases()
{
for (auto& option: m_options)
{
auto& detail = *option.second;
auto hash = detail.hash();
//if (m_parsed.find(hash) != m_parsed.end())
{
m_keys[detail.short_name()] = hash;
m_keys[detail.long_name()] = hash;
}
m_parsed.emplace(hash, OptionValue());
}
} }
inline inline
@ -2034,6 +2085,11 @@ Options::add_option
add_one_option(l, option); add_one_option(l, option);
} }
m_option_list.push_front(*option.get());
auto iter = m_option_list.begin();
m_option_map[s] = iter;
m_option_map[l] = iter;
//add the help details //add the help details
auto& options = m_help[group]; auto& options = m_help[group];

View File

@ -154,7 +154,7 @@ TEST_CASE("All positional", "[positional]")
auto result = options.parse(argc, argv); auto result = options.parse(argc, argv);
REQUIRE(argc == 1); CHECK(result.unmatched().size() == 0);
REQUIRE(positional.size() == 3); REQUIRE(positional.size() == 3);
CHECK(positional[0] == "a"); CHECK(positional[0] == "a");
@ -182,7 +182,7 @@ TEST_CASE("Some positional explicit", "[positional]")
auto result = options.parse(argc, argv); auto result = options.parse(argc, argv);
CHECK(argc == 1); CHECK(result.unmatched().size() == 0);
CHECK(result.count("output")); CHECK(result.count("output"));
CHECK(result["input"].as<std::string>() == "b"); CHECK(result["input"].as<std::string>() == "b");
CHECK(result["output"].as<std::string>() == "a"); CHECK(result["output"].as<std::string>() == "a");
@ -209,11 +209,10 @@ TEST_CASE("No positional with extras", "[positional]")
auto old_argv = argv; auto old_argv = argv;
auto old_argc = argc; auto old_argc = argc;
options.parse(argc, argv); auto result = options.parse(argc, argv);
REQUIRE(argc == old_argc - 1); auto& unmatched = result.unmatched();
CHECK(argv[0] == std::string("extras")); CHECK((unmatched == std::vector<std::string>{"a", "b", "c", "d"}));
CHECK(argv[1] == std::string("a"));
} }
TEST_CASE("Positional not valid", "[positional]") { TEST_CASE("Positional not valid", "[positional]") {
@ -643,9 +642,9 @@ TEST_CASE("Unrecognised options", "[options]") {
SECTION("After allowing unrecognised options") { SECTION("After allowing unrecognised options") {
options.allow_unrecognised_options(); options.allow_unrecognised_options();
CHECK_NOTHROW(options.parse(argc, argv)); auto result = options.parse(argc, argv);
REQUIRE(argc == 3); auto& unmatched = result.unmatched();
CHECK_THAT(argv[1], Catch::Equals("--unknown")); CHECK((unmatched == std::vector<std::string>{"--unknown", "--another_unknown"}));
} }
} }