diff --git a/README.md b/README.md index 81bd897a6..1b6215677 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ The add commands return a pointer to an internally stored `Option`. If you set t * `->envname(name)`: Gets the value from the environment if present and not passed on the command line. * `->group(name)`: The help group to put the option in. No effect for positional options. Defaults to `"Options"`. `"Hidden"` will not show up in the help print. * `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments). +* `->take_last()`: Only take the last option/flag given on the command line, automatically true for bool flags * `->check(CLI::ExistingFile)`: Requires that the file exists if given. * `->check(CLI::ExistingDirectory)`: Requires that the directory exists. * `->check(CLI::NonexistentPath)`: Requires that the path does not exist. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 0cbe4944e..32d2ab4a2 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -351,22 +351,23 @@ class App { return opt; } - /// Bool version + /// Bool version - defaults to allowing multiple passings, but can be forced to one if `take_last(false)` is used. template ::value, detail::enabler> = detail::dummy> Option *add_flag(std::string name, T &count, ///< A varaible holding true if passed std::string description = "") { count = false; - CLI::callback_t fun = [&count](CLI::results_t) { + CLI::callback_t fun = [&count](CLI::results_t res) { count = true; - return true; + return res.size() == 1; }; Option *opt = add_option(name, fun, description, false); if(opt->get_positional()) throw IncorrectConstruction("Flags cannot be positional"); opt->set_custom_option("", 0); + opt->take_last(); return opt; } diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index 9cada2707..e23a6ceae 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -74,6 +74,9 @@ class Option { /// The number of expected values, 0 for flag, -1 for unlimited vector int expected_{1}; + /// Only take the last argument (requires `expected_ == 1`) + bool last_{false}; + /// A private setting to allow args to not be able to accept incorrect expected values bool changeable_{false}; @@ -155,10 +158,20 @@ class Option { throw IncorrectConstruction("Cannot make a flag take arguments!"); else if(!changeable_) throw IncorrectConstruction("You can only change the expected arguments for vectors"); + else if(last_) + throw IncorrectConstruction("You can't change expected arguments after you've set take_last!"); expected_ = value; return this; } + /// Take the last argument if given multiple times + Option *take_last(bool value = true) { + if(expected_ != 0 && expected_ != 1) + throw IncorrectConstruction("take_last only works for flags and single value options!"); + last_ = value; + return this; + } + /// Adds a validator Option *check(std::function validator) { @@ -243,6 +256,9 @@ class Option { /// The number of arguments the option expects int get_expected() const { return expected_; } + /// The status of the take last flag + bool get_take_last() const { return last_; } + /// True if this has a default value int get_default() const { return default_; } @@ -340,7 +356,16 @@ class Option { /// Process the callback void run_callback() const { - if(!callback_(results_)) + bool result; + // If take_last, only operate on the final item + if(last_) { + results_t partial_result = {results_.back()}; + result = !callback_(partial_result); + } else { + result = !callback_(results_); + } + + if(result) throw ConversionError(get_name() + "=" + detail::join(results_)); if(!validators_.empty()) { for(const std::string &result : results_) @@ -407,7 +432,7 @@ class Option { return std::find(std::begin(lnames_), std::end(lnames_), name) != std::end(lnames_); } - /// Puts a result at position r + /// Puts a result at the end, unless last_ is set, in which case it just keeps the last one void add_result(std::string s) { results_.push_back(s); callback_run_ = false; diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 4d2ff0492..7a636a811 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -167,6 +167,20 @@ TEST_F(TApp, BoolAndIntFlags) { EXPECT_EQ((unsigned int)2, uflag); } +TEST_F(TApp, BoolOnlyFlag) { + bool bflag; + app.add_flag("-b", bflag)->take_last(false); + + args = {"-b"}; + EXPECT_NO_THROW(run()); + EXPECT_TRUE(bflag); + + app.reset(); + + args = {"-b", "-b"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + TEST_F(TApp, ShortOpts) { unsigned long long funnyint; @@ -204,6 +218,18 @@ TEST_F(TApp, DefaultOpts) { EXPECT_EQ("9", s); } +TEST_F(TApp, TakeLastOpt) { + + std::string str; + app.add_option("--str", str)->take_last(); + + args = {"--str=one", "--str=two"}; + + run(); + + EXPECT_EQ(str, "two"); +} + TEST_F(TApp, EnumTest) { enum Level : std::int32_t { High, Medium, Low }; Level level = Level::Low; diff --git a/tests/CreationTest.cpp b/tests/CreationTest.cpp index a1c7b9c7e..839d66e8b 100644 --- a/tests/CreationTest.cpp +++ b/tests/CreationTest.cpp @@ -137,6 +137,20 @@ TEST_F(TApp, IncorrectConstructionVectorAsFlag) { EXPECT_THROW(cat->expected(0), CLI::IncorrectConstruction); } +TEST_F(TApp, IncorrectConstructionVectorTakeLast) { + std::vector vec; + auto cat = app.add_option("--vec", vec); + EXPECT_THROW(cat->take_last(), CLI::IncorrectConstruction); +} + +TEST_F(TApp, IncorrectConstructionTakeLastExpected) { + std::vector vec; + auto cat = app.add_option("--vec", vec); + cat->expected(1); + ASSERT_NO_THROW(cat->take_last()); + EXPECT_THROW(cat->expected(2), CLI::IncorrectConstruction); +} + TEST_F(TApp, IncorrectConstructionRequiresCannotFind) { auto cat = app.add_flag("--cat"); EXPECT_THROW(cat->requires("--nothing"), CLI::IncorrectConstruction);