Skip to content

Commit 5a03ee5

Browse files
phlptppre-commit-ci[bot]henryiii
authored
Allow non standard option names like -option (#1078)
This has been bounced around for a couple years now #474 and a few others have expressed desire to work with non-standard option names. We have been somewhat resistant to that but I think it can be done now. This PR adds a modifier `allow_non_standard_option_names()` It is purposely long, it is purposely off by default. But what it does is allow option names with a single `-` to act like a short option name. With this modifier enabled no single letter short option names are allowed to start with the same letter as a non-standard names. For example `-s` and `-single` would not be allowed. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 7bc90c7 commit 5a03ee5

33 files changed

+241
-48
lines changed

CPPLINT.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ linelength=120 # As in .clang-format
33

44
# Unused filters
55
filter=-build/c++11 # Reports e.g. chrono and thread, which overlap with Chromium's API. Not applicable to general C++ projects.
6+
filter=-build/c++17 # google only restrictions not relevant
67
filter=-build/include_order # Requires unusual include order that encourages creating not self-contained headers
78
filter=-build/include_subdir # Prevents including files in current directory for whatever reason
89
filter=-readability/nolint # Conflicts with clang-tidy
@@ -13,3 +14,4 @@ filter=-runtime/string # Requires not using static const strings which makes th
1314
filter=-whitespace/blank_line # Unnecessarily strict with blank lines that otherwise help with readability
1415
filter=-whitespace/indent # Requires strange 3-space indent of private/protected/public markers
1516
filter=-whitespace/parens,-whitespace/braces # Conflict with clang-format
17+
filter=-whitespace/newline # handled by clang-format

README.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,6 @@ installation fuss.
158158
There are some other possible "features" that are intentionally not supported by
159159
this library:
160160

161-
- Non-standard variations on syntax, like `-long` options. This is non-standard
162-
and should be avoided, so that is enforced by this library.
163161
- Completion of partial options, such as Python's `argparse` supplies for
164162
incomplete arguments. It's better not to guess. Most third party command line
165163
parsers for python actually reimplement command line parsing rather than using
@@ -904,6 +902,14 @@ option_groups. These are:
904902
the form of `/s /long /file:file_name.ext` This option does not change how
905903
options are specified in the `add_option` calls or the ability to process
906904
options in the form of `-s --long --file=file_name.ext`.
905+
- `.allow_non_standard_option_names()`:🚧 Allow specification of single `-` long
906+
form option names. This is not recommended but is available to enable
907+
reworking of existing interfaces. If this modifier is enabled on an app or
908+
subcommand, options or flags can be specified like normal but instead of
909+
throwing an exception, long form single dash option names will be allowed. It
910+
is not allowed to have a single character short option starting with the same
911+
character as a single dash long form name; for example, `-s` and `-single` are
912+
not allowed in the same application.
907913
- `.fallthrough()`: Allow extra unmatched options and positionals to "fall
908914
through" and be matched on a parent option. Subcommands by default are allowed
909915
to "fall through" as in they will first attempt to match on the current

azure-pipelines.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- job: CppLint
2323
pool:
2424
vmImage: "ubuntu-latest"
25-
container: sharaku/cpplint:latest
25+
container: helics/buildenv:cpplint
2626
steps:
2727
- bash: cpplint --counting=detailed --recursive examples include/CLI tests
2828
displayName: Checking against google style guide

examples/custom_parse.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <CLI/CLI.hpp>
1010
#include <iostream>
1111
#include <sstream>
12+
#include <string>
1213

1314
// example file to demonstrate a custom lexical cast function
1415

examples/formatter.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <CLI/CLI.hpp>
88
#include <iostream>
99
#include <memory>
10+
#include <string>
1011

1112
class MyFormatter : public CLI::Formatter {
1213
public:

examples/inter_argument_order.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <CLI/CLI.hpp>
88
#include <algorithm>
99
#include <iostream>
10+
#include <string>
1011
#include <tuple>
1112
#include <vector>
1213

include/CLI/App.hpp

+12
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ class App {
260260
/// This is potentially useful as a modifier subcommand
261261
bool silent_{false};
262262

263+
/// indicator that the subcommand should allow non-standard option arguments, such as -single_dash_flag
264+
bool allow_non_standard_options_{false};
265+
263266
/// Counts the number of times this command/subcommand was parsed
264267
std::uint32_t parsed_{0U};
265268

@@ -392,6 +395,12 @@ class App {
392395
return this;
393396
}
394397

398+
/// allow non standard option names
399+
App *allow_non_standard_option_names(bool allowed = true) {
400+
allow_non_standard_options_ = allowed;
401+
return this;
402+
}
403+
395404
/// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
396405
App *disabled_by_default(bool disable = true) {
397406
if(disable) {
@@ -1146,6 +1155,9 @@ class App {
11461155
/// Get the status of silence
11471156
CLI11_NODISCARD bool get_silent() const { return silent_; }
11481157

1158+
/// Get the status of silence
1159+
CLI11_NODISCARD bool get_allow_non_standard_option_names() const { return allow_non_standard_options_; }
1160+
11491161
/// Get the status of disabled
11501162
CLI11_NODISCARD bool get_immediate_callback() const { return immediate_callback_; }
11511163

include/CLI/Option.hpp

+6-2
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,13 @@ class Option : public OptionBase<Option> {
341341
///@}
342342

343343
/// Making an option by hand is not defined, it must be made by the App class
344-
Option(std::string option_name, std::string option_description, callback_t callback, App *parent)
344+
Option(std::string option_name,
345+
std::string option_description,
346+
callback_t callback,
347+
App *parent,
348+
bool allow_non_standard = false)
345349
: description_(std::move(option_description)), parent_(parent), callback_(std::move(callback)) {
346-
std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name));
350+
std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name), allow_non_standard);
347351
}
348352

349353
public:

include/CLI/Split.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ CLI11_INLINE std::vector<std::pair<std::string, std::string>> get_default_flag_v
3939

4040
/// Get a vector of short names, one of long names, and a single name
4141
CLI11_INLINE std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
42-
get_names(const std::vector<std::string> &input);
42+
get_names(const std::vector<std::string> &input, bool allow_non_standard = false);
4343

4444
} // namespace detail
4545
// [CLI11:split_hpp:end]

include/CLI/Timer.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include <array>
2020
#include <chrono>
21+
#include <cstdio>
2122
#include <functional>
2223
#include <iostream>
2324
#include <string>

include/CLI/impl/App_inl.hpp

+44-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
// [CLI11:public_includes:set]
1818
#include <algorithm>
19+
#include <iostream>
1920
#include <memory>
2021
#include <string>
2122
#include <utility>
@@ -161,7 +162,7 @@ CLI11_INLINE Option *App::add_option(std::string option_name,
161162
std::string option_description,
162163
bool defaulted,
163164
std::function<std::string()> func) {
164-
Option myopt{option_name, option_description, option_callback, this};
165+
Option myopt{option_name, option_description, option_callback, this, allow_non_standard_options_};
165166

166167
if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; }) ==
167168
std::end(options_)) {
@@ -191,9 +192,34 @@ CLI11_INLINE Option *App::add_option(std::string option_name,
191192
}
192193
}
193194
}
195+
if(allow_non_standard_options_ && !myopt.snames_.empty()) {
196+
for(auto &sname : myopt.snames_) {
197+
if(sname.length() > 1) {
198+
std::string test_name;
199+
test_name.push_back('-');
200+
test_name.push_back(sname.front());
201+
auto *op = get_option_no_throw(test_name);
202+
if(op != nullptr) {
203+
throw(OptionAlreadyAdded("added option interferes with existing short option: " + sname));
204+
}
205+
}
206+
}
207+
for(auto &opt : options_) {
208+
for(const auto &osn : opt->snames_) {
209+
if(osn.size() > 1) {
210+
std::string test_name;
211+
test_name.push_back(osn.front());
212+
if(myopt.check_sname(test_name)) {
213+
throw(OptionAlreadyAdded("added option interferes with existing non standard option: " +
214+
osn));
215+
}
216+
}
217+
}
218+
}
219+
}
194220
options_.emplace_back();
195221
Option_p &option = options_.back();
196-
option.reset(new Option(option_name, option_description, option_callback, this));
222+
option.reset(new Option(option_name, option_description, option_callback, this, allow_non_standard_options_));
197223

198224
// Set the default string capture function
199225
option->default_function(func);
@@ -1888,7 +1914,8 @@ App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type,
18881914
});
18891915

18901916
// Option not found
1891-
if(op_ptr == std::end(options_)) {
1917+
while(op_ptr == std::end(options_)) {
1918+
// using while so we can break
18921919
for(auto &subc : subcommands_) {
18931920
if(subc->name_.empty() && !subc->disabled_) {
18941921
if(subc->_parse_arg(args, current_type, local_processing_only)) {
@@ -1899,6 +1926,20 @@ App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type,
18991926
}
19001927
}
19011928
}
1929+
if(allow_non_standard_options_ && current_type == detail::Classifier::SHORT && current.size() > 2) {
1930+
std::string narg_name;
1931+
std::string nvalue;
1932+
detail::split_long(std::string{'-'} + current, narg_name, nvalue);
1933+
op_ptr = std::find_if(std::begin(options_), std::end(options_), [narg_name](const Option_p &opt) {
1934+
return opt->check_sname(narg_name);
1935+
});
1936+
if(op_ptr != std::end(options_)) {
1937+
arg_name = narg_name;
1938+
value = nvalue;
1939+
rest.clear();
1940+
break;
1941+
}
1942+
}
19021943

19031944
// don't capture missing if this is a nameless subcommand and nameless subcommands can't fallthrough
19041945
if(parent_ != nullptr && name_.empty()) {

include/CLI/impl/Split_inl.hpp

+20-8
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ CLI11_INLINE std::vector<std::pair<std::string, std::string>> get_default_flag_v
103103
}
104104

105105
CLI11_INLINE std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
106-
get_names(const std::vector<std::string> &input) {
106+
get_names(const std::vector<std::string> &input, bool allow_non_standard) {
107107

108108
std::vector<std::string> short_names;
109109
std::vector<std::string> long_names;
@@ -113,23 +113,35 @@ get_names(const std::vector<std::string> &input) {
113113
continue;
114114
}
115115
if(name.length() > 1 && name[0] == '-' && name[1] != '-') {
116-
if(name.length() == 2 && valid_first_char(name[1]))
116+
if(name.length() == 2 && valid_first_char(name[1])) {
117117
short_names.emplace_back(1, name[1]);
118-
else if(name.length() > 2)
119-
throw BadNameString::MissingDash(name);
120-
else
118+
} else if(name.length() > 2) {
119+
if(allow_non_standard) {
120+
name = name.substr(1);
121+
if(valid_name_string(name)) {
122+
short_names.push_back(name);
123+
} else {
124+
throw BadNameString::BadLongName(name);
125+
}
126+
} else {
127+
throw BadNameString::MissingDash(name);
128+
}
129+
} else {
121130
throw BadNameString::OneCharName(name);
131+
}
122132
} else if(name.length() > 2 && name.substr(0, 2) == "--") {
123133
name = name.substr(2);
124-
if(valid_name_string(name))
134+
if(valid_name_string(name)) {
125135
long_names.push_back(name);
126-
else
136+
} else {
127137
throw BadNameString::BadLongName(name);
138+
}
128139
} else if(name == "-" || name == "--" || name == "++") {
129140
throw BadNameString::ReservedName(name);
130141
} else {
131-
if(!pos_name.empty())
142+
if(!pos_name.empty()) {
132143
throw BadNameString::MultiPositionalNames(name);
144+
}
133145
if(valid_name_string(name)) {
134146
pos_name = name;
135147
} else {

tests/AppTest.cpp

+51
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include <cstdlib>
1414
#include <limits>
1515
#include <map>
16+
#include <string>
17+
#include <utility>
18+
#include <vector>
1619

1720
TEST_CASE_METHOD(TApp, "OneFlagShort", "[app]") {
1821
app.add_flag("-c,--count");
@@ -663,6 +666,15 @@ TEST_CASE_METHOD(TApp, "singledash", "[app]") {
663666
} catch(...) {
664667
CHECK(false);
665668
}
669+
app.allow_non_standard_option_names();
670+
try {
671+
app.add_option("-!I{am}bad");
672+
} catch(const CLI::BadNameString &e) {
673+
std::string str = e.what();
674+
CHECK_THAT(str, Contains("!I{am}bad"));
675+
} catch(...) {
676+
CHECK(false);
677+
}
666678
}
667679

668680
TEST_CASE_METHOD(TApp, "FlagLikeOption", "[app]") {
@@ -2389,6 +2401,45 @@ TEST_CASE_METHOD(TApp, "OrderedModifyingTransforms", "[app]") {
23892401
CHECK(std::vector<std::string>({"one21", "two21"}) == val);
23902402
}
23912403

2404+
// non standard options
2405+
TEST_CASE_METHOD(TApp, "nonStandardOptions", "[app]") {
2406+
std::string string1;
2407+
CHECK_THROWS_AS(app.add_option("-single", string1), CLI::BadNameString);
2408+
app.allow_non_standard_option_names();
2409+
CHECK(app.get_allow_non_standard_option_names());
2410+
app.add_option("-single", string1);
2411+
args = {"-single", "string1"};
2412+
2413+
run();
2414+
2415+
CHECK(string1 == "string1");
2416+
}
2417+
2418+
TEST_CASE_METHOD(TApp, "nonStandardOptions2", "[app]") {
2419+
std::vector<std::string> strings;
2420+
app.allow_non_standard_option_names();
2421+
app.add_option("-single,--single,-m", strings);
2422+
args = {"-single", "string1", "--single", "string2"};
2423+
2424+
run();
2425+
2426+
CHECK(strings == std::vector<std::string>{"string1", "string2"});
2427+
}
2428+
2429+
TEST_CASE_METHOD(TApp, "nonStandardOptionsIntersect", "[app]") {
2430+
std::vector<std::string> strings;
2431+
app.allow_non_standard_option_names();
2432+
app.add_option("-s,-t");
2433+
CHECK_THROWS_AS(app.add_option("-single,--single", strings), CLI::OptionAlreadyAdded);
2434+
}
2435+
2436+
TEST_CASE_METHOD(TApp, "nonStandardOptionsIntersect2", "[app]") {
2437+
std::vector<std::string> strings;
2438+
app.allow_non_standard_option_names();
2439+
app.add_option("-single,--single", strings);
2440+
CHECK_THROWS_AS(app.add_option("-s,-t"), CLI::OptionAlreadyAdded);
2441+
}
2442+
23922443
TEST_CASE_METHOD(TApp, "ThrowingTransform", "[app]") {
23932444
std::string val;
23942445
auto *m = app.add_option("-m,--mess", val);

0 commit comments

Comments
 (0)