diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 26910892..d568351e 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -21,6 +21,7 @@ #include "CLI/StringTools.hpp" #include "CLI/Split.hpp" #include "CLI/Option.hpp" +#include "CLI/Ini.hpp" namespace CLI { @@ -44,13 +45,17 @@ protected: std::vector missing_options; std::deque positionals; std::vector subcommands; - bool parsed{false}; - App* subcommand{nullptr}; - std::string progname{"program"}; + bool parsed {false}; + App* subcommand {nullptr}; + std::string progname {"program"}; Option* help_flag {nullptr}; std::function app_callback; + std::string ini_file; + bool ini_required {false}; + Option* ini_setting {nullptr}; + public: /// Set a callback for the end of parsing. Due to a bug in c++11, @@ -284,6 +289,25 @@ public: } + /// Add a configuration ini file option + void add_config(std::string name="--config", + std::string default_filename="", + std::string help="Read an ini file", + bool required=false) { + + // Remove existing config if present + if(ini_setting != nullptr) { + auto iterator = std::find_if(std::begin(options), std::end(options), + [this](const Option_p &v){return v.get() == ini_setting;}); + if (iterator != std::end(options)) { + options.erase(iterator); + } + } + ini_file = default_filename; + ini_required = required; + ini_setting = add_option(name, ini_file, help, default_filename!=""); + } + /// This allows subclasses to inject code before callbacks but after parse virtual void pre_callback() {} @@ -297,7 +321,7 @@ public: } /// The real work is done here. Expects a reversed vector - void parse(std::vector & args) { + void parse(std::vector & args, bool first_parse=true) { parsed = true; bool positional_only = false; @@ -331,21 +355,36 @@ public: } - for(const Option_p& opt : options) { while (opt->get_positional() && opt->count() < opt->get_expected() && positionals.size() > 0) { opt->get_new(); opt->add_result(0, positionals.front()); positionals.pop_front(); } - if (opt->get_required() && opt->count() < opt->get_expected()) - throw RequiredError(opt->get_name()); if (opt->count() > 0) { if(!opt->run_callback()) - throw ConversionError(opt->get_name()); + throw ConversionError(opt->get_name() + "=" + detail::join(opt->flatten_results())); } - } + + if (first_parse && ini_setting != nullptr && ini_file != "") { + try { + std::vector values = detail::parse_ini(ini_file); + std::reverse(std::begin(values), std::end(values)); + + values.insert(std::begin(values), std::begin(positionals), std::end(positionals)); + return parse(values, false); + } catch (const FileError &e) { + if(ini_required) + throw; + } + } + + for(const Option_p& opt : options) { + if (opt->get_required() && opt->count() < opt->get_expected()) + throw RequiredError(opt->get_name()); + } + if(positionals.size()>0) throw PositionalError("[" + detail::join(positionals) + "]"); @@ -401,7 +440,6 @@ public: while(args.size()>0 && _recognize(args.back()) == Classifer::NONE) { op->add_result(vnum, args.back()); args.pop_back(); - } } else while(num>0 && args.size() > 0) { num--; diff --git a/tests/CLITest.cpp b/tests/CLITest.cpp index bc579c25..a84588d4 100644 --- a/tests/CLITest.cpp +++ b/tests/CLITest.cpp @@ -91,6 +91,16 @@ TEST_F(TApp, OneString) { EXPECT_EQ(str, "mystring"); } +TEST_F(TApp, OneStringEqualVersion) { + std::string str; + app.add_option("-s,--string", str); + args = {"--string=mystring"}; + EXPECT_NO_THROW(run()); + EXPECT_EQ(1, app.count("-s")); + EXPECT_EQ(1, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + TEST_F(TApp, TogetherInt) { int i; @@ -383,8 +393,63 @@ TEST_F(TApp, VectorFancyOpts) { EXPECT_THROW(run(), CLI::ParseError); } +struct TIni : public TApp { + + std::ofstream f{"IniParseSimple.ini"}; + + void run() { + f.close(); + TApp::run(); + } + + ~TIni() { + f.close(); + std::remove("IniParseSimple.ini"); + } + +}; + +TEST_F(TIni, IniParseSimple) { + + int x; + std::string y; + + app.add_option("--something", x); + app.add_option("--else", y); + + app.add_config("--config","", "", true); + + args = {"--config=IniParseSimple.ini"}; + + + ASSERT_TRUE(f.good()); + + f << "[default]" << std::endl; + f << "" << std::endl; + f << "something=7" << std::endl; + f << "else=seven" << std::endl; + + //EXPECT_NO_THROW + (run()); + + EXPECT_EQ(7, x); + EXPECT_EQ("seven", y); +} + + +TEST(Ini, IniDoubleAdd) { + + CLI::App app; + + app.add_config("--first"); + app.add_config("--second"); + + EXPECT_NO_THROW(app.count("--second")); + EXPECT_THROW(app.count("--first"), CLI::OptionNotFound); + +} TEST_F(TApp, BasicSubcommands) { auto sub1 = app.add_subcommand("sub1"); auto sub2 = app.add_subcommand("sub2"); diff --git a/tests/IniParseSimple.ini b/tests/IniParseSimple.ini new file mode 100644 index 00000000..8da3fe2a --- /dev/null +++ b/tests/IniParseSimple.ini @@ -0,0 +1,4 @@ +[default] + +something=7 +else=seven