From 0404a0d61e23f32f5459247d6f9c5292fff38253 Mon Sep 17 00:00:00 2001 From: Eyal Rozenberg Date: Fri, 15 Jun 2018 16:51:37 +0200 Subject: [PATCH] Fixes #1: Support for multiple long names per option An unintrusive implementation - no existing interfaces changed, and a single addition for obtaining all of the different long names. Notes: * Tests added for the new functionality, and existing tests expanded to take it into account. * It is now impossible to specify long names with commas in them (but then, that wasn't properly supported before either, more of an oversight). * The multiple long options are not included in the usage information - just the first one of them is printed --- .../program_options/options_description.hpp | 28 ++- src/options_description.cpp | 132 ++++++----- test/cmdline_test.cpp | 6 +- test/config_test.cfg | 2 +- test/exception_test.cpp | 108 ++++++++- test/options_description_test.cpp | 86 ++++++++ test/parsers_test.cpp | 205 ++++++++++++------ test/required_test.cpp | 49 ++++- test/unicode_test.cpp | 3 +- 9 files changed, 475 insertions(+), 144 deletions(-) diff --git a/include/boost/program_options/options_description.hpp b/include/boost/program_options/options_description.hpp index fac6acc613..90d913d3e3 100644 --- a/include/boost/program_options/options_description.hpp +++ b/include/boost/program_options/options_description.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -106,14 +107,16 @@ namespace program_options { /** Returns the canonical name for the option description to enable the user to recognised a matching option. 1) For short options ('-', '/'), returns the short name prefixed. - 2) For long options ('--' / '-') returns the long name prefixed - 3) All other cases, returns the long name (if present) or the short name, - unprefixed. + 2) For long options ('--' / '-') returns the first long name prefixed + 3) All other cases, returns the first long name (if present) or the short + name, unprefixed. */ std::string canonical_display_name(int canonical_option_style = 0) const; const std::string& long_name() const; + const std::pair long_names() const; + /// Explanation of this option const std::string& description() const; @@ -129,9 +132,24 @@ namespace program_options { private: - option_description& set_name(const char* name); + option_description& set_names(const char* name); + + /** + * a one-character "switch" name - with its prefix, + * so that this is either empty or has length 2 (e.g. "-c" + */ + std::string m_short_name; + + /** + * one or more names by which this option may be specified + * on a command-line or in a config file, which are not + * a single-letter switch. The names here are _without_ + * any prefix. + */ + std::vector m_long_names; + + std::string m_description; - std::string m_short_name, m_long_name, m_description; // shared_ptr is needed to simplify memory management in // copy ctor and destructor. shared_ptr m_value_semantic; diff --git a/src/options_description.cpp b/src/options_description.cpp index 6592a5db30..d4fcfc8a97 100644 --- a/src/options_description.cpp +++ b/src/options_description.cpp @@ -49,21 +49,21 @@ namespace boost { namespace program_options { } option_description:: - option_description(const char* name, + option_description(const char* names, const value_semantic* s) : m_value_semantic(s) { - this->set_name(name); + this->set_names(names); } option_description:: - option_description(const char* name, + option_description(const char* names, const value_semantic* s, const char* description) : m_description(description), m_value_semantic(s) { - this->set_name(name); + this->set_names(names); } option_description::~option_description() @@ -77,38 +77,42 @@ namespace boost { namespace program_options { bool short_ignore_case) const { match_result result = no_match; + std::string local_option = (long_ignore_case ? tolower_(option) : option); - std::string local_long_name((long_ignore_case ? tolower_(m_long_name) : m_long_name)); - - if (!local_long_name.empty()) { - - std::string local_option = (long_ignore_case ? tolower_(option) : option); + for(std::vector::const_iterator it(m_long_names.begin()); it != m_long_names.end(); it++) + { + std::string local_long_name((long_ignore_case ? tolower_(*it) : *it)); - if (*local_long_name.rbegin() == '*') - { - // The name ends with '*'. Any specified name with the given - // prefix is OK. - if (local_option.find(local_long_name.substr(0, local_long_name.length()-1)) - == 0) - result = approximate_match; - } + if (!local_long_name.empty()) { - if (local_long_name == local_option) - { - result = full_match; - } - else if (approx) - { - if (local_long_name.find(local_option) == 0) + + if ((result == no_match) && (*local_long_name.rbegin() == '*')) + { + // The name ends with '*'. Any specified name with the given + // prefix is OK. + if (local_option.find(local_long_name.substr(0, local_long_name.length()-1)) + == 0) + result = approximate_match; + } + + if (local_long_name == local_option) + { + result = full_match; + break; + } + else if (approx) { - result = approximate_match; + if (local_long_name.find(local_option) == 0) + { + result = approximate_match; + } } } + } - + if (result != full_match) { - std::string local_option(short_ignore_case ? tolower_(option) : option); std::string local_short_name(short_ignore_case ? tolower_(m_short_name) : m_short_name); if (local_short_name == local_option) @@ -122,9 +126,12 @@ namespace boost { namespace program_options { const std::string& option_description::key(const std::string& option) const - { - if (!m_long_name.empty()) - if (m_long_name.find('*') != string::npos) + { + // We make the arbitrary choise of using the first long + // name as the key, regardless of anything else + if (!m_long_names.empty()) { + const std::string& first_long_name = *m_long_names.begin(); + if (first_long_name.find('*') != string::npos) // The '*' character means we're long_name // matches only part of the input. So, returning // long name will remove some of the information, @@ -132,7 +139,8 @@ namespace boost { namespace program_options { // in the source. return option; else - return m_long_name; + return first_long_name; + } else return m_short_name; } @@ -140,12 +148,13 @@ namespace boost { namespace program_options { std::string option_description::canonical_display_name(int prefix_style) const { - if (!m_long_name.empty()) + // We prefer the first long name over any others + if (!m_long_names.empty()) { if (prefix_style == command_line_style::allow_long) - return "--" + m_long_name; + return "--" + *m_long_names.begin(); if (prefix_style == command_line_style::allow_long_disguise) - return "-" + m_long_name; + return "-" + *m_long_names.begin(); } // sanity check: m_short_name[0] should be '-' or '/' if (m_short_name.length() == 2) @@ -155,8 +164,8 @@ namespace boost { namespace program_options { if (prefix_style == command_line_style::allow_dash_for_short) return string("-") + m_short_name[1]; } - if (!m_long_name.empty()) - return m_long_name; + if (!m_long_names.empty()) + return *m_long_names.begin(); else return m_short_name; } @@ -165,21 +174,46 @@ namespace boost { namespace program_options { const std::string& option_description::long_name() const { - return m_long_name; + static std::string empty_string(""); + return m_long_names.empty() ? empty_string : *m_long_names.begin(); + } + + const std::pair + option_description::long_names() const + { + return (m_long_names.empty()) + ? std::pair( NULL, 0 ) + : std::pair( &(*m_long_names.begin()), m_long_names.size()); } option_description& - option_description::set_name(const char* _name) + option_description::set_names(const char* _names) { - std::string name(_name); - string::size_type n = name.find(','); - if (n != string::npos) { - assert(n == name.size()-2); - m_long_name = name.substr(0, n); - m_short_name = '-' + name.substr(n+1,1); - } else { - m_long_name = name; + m_long_names.clear(); + std::istringstream iss(_names); + std::string name; + + while(std::getline(iss, name, ',')) { + m_long_names.push_back(name); + } + assert(!m_long_names.empty() && "No option names were specified"); + + bool try_interpreting_last_name_as_a_switch = m_long_names.size() > 1; + if (try_interpreting_last_name_as_a_switch) { + const std::string& last_name = *m_long_names.rbegin(); + if (last_name.length() == 1) { + m_short_name = '-' + last_name; + m_long_names.pop_back(); + // The following caters to the (valid) input of ",c" for some + // character c, where the caller only wants this option to have + // a short name. + if (m_long_names.size() == 1 && (*m_long_names.begin()).empty()) { + m_long_names.clear(); + } + } } + // We could theoretically also ensure no remaining long names + // are empty, or that none of them have length 1 return *this; } @@ -200,12 +234,12 @@ namespace boost { namespace program_options { { if (!m_short_name.empty()) { - return m_long_name.empty() + return m_long_names.empty() ? m_short_name : string(m_short_name).append(" [ --"). - append(m_long_name).append(" ]"); + append(*m_long_names.begin()).append(" ]"); } - return string("--").append(m_long_name); + return string("--").append(*m_long_names.begin()); } std::string diff --git a/test/cmdline_test.cpp b/test/cmdline_test.cpp index e971819362..1fc0af83c9 100644 --- a/test/cmdline_test.cpp +++ b/test/cmdline_test.cpp @@ -463,11 +463,13 @@ void test_additional_parser() desc.add_options() ("response-file", value(), "response file") ("foo", value(), "foo") + ("bar,baz", value(), "bar") ; vector input; input.push_back("@config"); input.push_back("--foo=1"); + input.push_back("--baz=11"); cmdline cmd(input); cmd.set_options_description(desc); @@ -475,11 +477,13 @@ void test_additional_parser() vector