Commit f346f29802026dc75a7ebf26aa70827b481e16b6

Authored by Philip Top
Committed by GitHub
1 parent 967bfe0e

Config short (#443)

* add a get_single_name function for options, and allow short names to be used for configuration output.

* add config input to handle short and positional options

* add some tests about short options and positional options in config files

* allow use of envname_ in config files

* update doc book and readme with fixes

* formatting update

* some formatting updates

* add some notes on the config file generation

* just try modifying a comment
README.md
... ... @@ -714,7 +714,7 @@ sub.subcommand = true
714 714 Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, `enable`; or `false`, `off`, `0`, `no`, `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not necessarily mean that subcommand was passed, it just sets the "defaults"). You cannot set positional-only arguments. ๐Ÿ†• Subcommands can be triggered from configuration files if the `configurable` flag was set on the subcommand. Then the use of `[subcommand]` notation will trigger a subcommand and cause it to act as if it were on the command line.
715 715  
716 716 To print a configuration file from the passed
717   -arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details.
  717 +arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include the app and option descriptions. See [Config files](https://cliutils.github.io/CLI11/book/chapters/config.html) for some additional details.
718 718  
719 719 ### Inheriting defaults
720 720  
... ...
book/chapters/config.md
... ... @@ -80,7 +80,27 @@ The main differences are in vector notation and comment character. Note: CLI11
80 80  
81 81 ## Writing out a configure file
82 82  
83   -To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions.
  83 +To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, write_description=false)`, where `default_also` will also show any defaulted arguments, and `write_description` will include option descriptions and the App description
  84 +
  85 +```cpp
  86 +
  87 + CLI::App app;
  88 + app.add_option(...);
  89 + // several other options
  90 + CLI11_PARSE(app, argc, argv);
  91 + //the config printout should be after the parse to capture the given arguments
  92 + std::cout<<app.config_to_str(true,true);
  93 +```
  94 +
  95 +if a prefix is needed to print before the options, for example to print a config for just a subcommand, the config formatter can be obtained directly.
  96 +
  97 +```cpp
  98 +
  99 + auto fmtr=app.get_config_formatter();
  100 + //std::string to_config(const App *app, bool default_also, bool write_description, std::string prefix)
  101 + fmtr->to_config(&app,true,true,"sub.");
  102 + //prefix can be used to set a prefix before each argument, like "sub."
  103 +```
84 104  
85 105 ### Customization of configure file output
86 106 The default config parser/generator has some customization points that allow variations on the TOML format. The default formatter has a base configuration that matches the TOML format. It defines 5 characters that define how different aspects of the configuration are handled
... ... @@ -115,14 +135,10 @@ The default configuration file will read INI files, but will write out files in
115 135 ```cpp
116 136 app.config_formatter(std::make_shared<CLI::ConfigINI>());
117 137 ```
118   -which makes use of a predefined modification of the ConfigBase class which TOML also uses.
  138 +which makes use of a predefined modification of the ConfigBase class which TOML also uses. If a custom formatter is used that is not inheriting from the from ConfigBase class `get_config_formatter_base() will return a nullptr, so some care must be exercised in its us with custom configurations.
119 139  
120 140 ## Custom formats
121 141  
122   -{% hint style='info' %}
123   -New in CLI11 1.6
124   -{% endhint %}
125   -
126 142 You can invent a custom format and set that instead of the default INI formatter. You need to inherit from `CLI::Config` and implement the following two functions:
127 143  
128 144 ```cpp
... ... @@ -145,3 +161,11 @@ See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples
145 161 Configuration files can be used to trigger subcommands if a subcommand is set to configure. By default configuration file just set the default values of a subcommand. But if the `configure()` option is set on a subcommand then the if the subcommand is utilized via a `[subname]` block in the configuration file it will act as if it were called from the command line. Subsubcommands can be triggered via [subname.subsubname]. Using the `[[subname]]` will be as if the subcommand were triggered multiple times from the command line. This functionality can allow the configuration file to act as a scripting file.
146 162  
147 163 For custom configuration files this behavior can be triggered by specifying the parent subcommands in the structure and `++` as the name to open a new subcommand scope and `--` to close it. These names trigger the different callbacks of configurable subcommands.
  164 +
  165 +## Implementation Notes
  166 +The config file input works with any form of the option given: Long, short, positional, or the environment variable name. When generating a config file it will create a name in following priority.
  167 +
  168 +1. First long name
  169 +1. Positional name
  170 +1. First short name
  171 +1. Environment name
... ...
include/CLI/App.hpp
... ... @@ -1491,7 +1491,7 @@ class App {
1491 1491 return this;
1492 1492 }
1493 1493 /// Produce a string that could be read in as a config of the current values of the App. Set default_also to
1494   - /// include default arguments. Prefix will add a string to the beginning of each option.
  1494 + /// include default arguments. write_descriptions will print a description for the App and for each option.
1495 1495 std::string config_to_str(bool default_also = false, bool write_description = false) const {
1496 1496 return config_formatter_->to_config(this, default_also, write_description, "");
1497 1497 }
... ... @@ -2336,6 +2336,14 @@ class App {
2336 2336 }
2337 2337 Option *op = get_option_no_throw("--" + item.name);
2338 2338 if(op == nullptr) {
  2339 + if(item.name.size() == 1) {
  2340 + op = get_option_no_throw("-" + item.name);
  2341 + }
  2342 + }
  2343 + if(op == nullptr) {
  2344 + op = get_option_no_throw(item.name);
  2345 + }
  2346 + if(op == nullptr) {
2339 2347 // If the option was not present
2340 2348 if(get_allow_config_extras() == config_extras_mode::capture)
2341 2349 // Should we worry about classifying the extras properly?
... ...
include/CLI/Config.hpp
... ... @@ -282,14 +282,14 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description,
282 282 }
283 283 for(const Option *opt : app->get_options({})) {
284 284  
285   - // Only process option with a long-name and configurable
286   - if(!opt->get_lnames().empty() && opt->get_configurable()) {
  285 + // Only process options that are configurable
  286 + if(opt->get_configurable()) {
287 287 if(opt->get_group() != group) {
288 288 if(!(group == "Options" && opt->get_group().empty())) {
289 289 continue;
290 290 }
291 291 }
292   - std::string name = prefix + opt->get_lnames()[0];
  292 + std::string name = prefix + opt->get_single_name();
293 293 std::string value = detail::ini_join(opt->reduced_results(), arraySeparator, arrayStart, arrayEnd);
294 294  
295 295 if(value.empty() && default_also) {
... ...
include/CLI/Option.hpp
... ... @@ -300,7 +300,7 @@ class Option : public OptionBase&lt;Option&gt; {
300 300 /// @name Other
301 301 ///@{
302 302  
303   - /// Remember the parent app
  303 + /// link back up to the parent App for fallthrough
304 304 App *parent_{nullptr};
305 305  
306 306 /// Options store a callback to do all the work
... ... @@ -681,7 +681,19 @@ class Option : public OptionBase&lt;Option&gt; {
681 681  
682 682 /// Get the flag names with specified default values
683 683 const std::vector<std::string> &get_fnames() const { return fnames_; }
684   -
  684 + /// Get a single name for the option, first of lname, pname, sname, envname
  685 + const std::string &get_single_name() const {
  686 + if(!lnames_.empty()) {
  687 + return lnames_[0];
  688 + }
  689 + if(!pname_.empty()) {
  690 + return pname_;
  691 + }
  692 + if(!snames_.empty()) {
  693 + return snames_[0];
  694 + }
  695 + return envname_;
  696 + }
685 697 /// The number of times the option expects to be included
686 698 int get_expected() const { return expected_min_; }
687 699  
... ... @@ -836,23 +848,33 @@ class Option : public OptionBase&lt;Option&gt; {
836 848 bool operator==(const Option &other) const { return !matching_name(other).empty(); }
837 849  
838 850 /// Check a name. Requires "-" or "--" for short / long, supports positional name
839   - bool check_name(std::string name) const {
  851 + bool check_name(const std::string &name) const {
840 852  
841 853 if(name.length() > 2 && name[0] == '-' && name[1] == '-')
842 854 return check_lname(name.substr(2));
843 855 if(name.length() > 1 && name.front() == '-')
844 856 return check_sname(name.substr(1));
845   -
846   - std::string local_pname = pname_;
847   - if(ignore_underscore_) {
848   - local_pname = detail::remove_underscore(local_pname);
849   - name = detail::remove_underscore(name);
  857 + if(!pname_.empty()) {
  858 + std::string local_pname = pname_;
  859 + std::string local_name = name;
  860 + if(ignore_underscore_) {
  861 + local_pname = detail::remove_underscore(local_pname);
  862 + local_name = detail::remove_underscore(local_name);
  863 + }
  864 + if(ignore_case_) {
  865 + local_pname = detail::to_lower(local_pname);
  866 + local_name = detail::to_lower(local_name);
  867 + }
  868 + if(local_name == local_pname) {
  869 + return true;
  870 + }
850 871 }
851   - if(ignore_case_) {
852   - local_pname = detail::to_lower(local_pname);
853   - name = detail::to_lower(name);
  872 +
  873 + if(!envname_.empty()) {
  874 + // this needs to be the original since envname_ shouldn't match on case insensitivity
  875 + return (name == envname_);
854 876 }
855   - return name == local_pname;
  877 + return false;
856 878 }
857 879  
858 880 /// Requires "-" to be removed from string
... ...
tests/AppTest.cpp
... ... @@ -2131,6 +2131,26 @@ TEST_F(TApp, Env) {
2131 2131 EXPECT_THROW(run(), CLI::RequiredError);
2132 2132 }
2133 2133  
  2134 +// curiously check if an environmental only option works
  2135 +TEST_F(TApp, EnvOnly) {
  2136 +
  2137 + put_env("CLI11_TEST_ENV_TMP", "2");
  2138 +
  2139 + int val{1};
  2140 + CLI::Option *vopt = app.add_option("", val)->envname("CLI11_TEST_ENV_TMP");
  2141 +
  2142 + run();
  2143 +
  2144 + EXPECT_EQ(2, val);
  2145 + EXPECT_EQ(1u, vopt->count());
  2146 +
  2147 + vopt->required();
  2148 + run();
  2149 +
  2150 + unset_env("CLI11_TEST_ENV_TMP");
  2151 + EXPECT_THROW(run(), CLI::RequiredError);
  2152 +}
  2153 +
2134 2154 TEST_F(TApp, RangeInt) {
2135 2155 int x{0};
2136 2156 app.add_option("--one", x)->check(CLI::Range(3, 6));
... ...
tests/ConfigFileTest.cpp
... ... @@ -1207,6 +1207,57 @@ TEST_F(TApp, IniFlagDual) {
1207 1207 EXPECT_THROW(run(), CLI::ConversionError);
1208 1208 }
1209 1209  
  1210 +TEST_F(TApp, IniShort) {
  1211 +
  1212 + TempFile tmpini{"TestIniTmp.ini"};
  1213 +
  1214 + int key{0};
  1215 + app.add_option("--flag,-f", key);
  1216 + app.set_config("--config", tmpini);
  1217 +
  1218 + {
  1219 + std::ofstream out{tmpini};
  1220 + out << "f=3" << std::endl;
  1221 + }
  1222 +
  1223 + ASSERT_NO_THROW(run());
  1224 + EXPECT_EQ(key, 3);
  1225 +}
  1226 +
  1227 +TEST_F(TApp, IniPositional) {
  1228 +
  1229 + TempFile tmpini{"TestIniTmp.ini"};
  1230 +
  1231 + int key{0};
  1232 + app.add_option("key", key);
  1233 + app.set_config("--config", tmpini);
  1234 +
  1235 + {
  1236 + std::ofstream out{tmpini};
  1237 + out << "key=3" << std::endl;
  1238 + }
  1239 +
  1240 + ASSERT_NO_THROW(run());
  1241 + EXPECT_EQ(key, 3);
  1242 +}
  1243 +
  1244 +TEST_F(TApp, IniEnvironmental) {
  1245 +
  1246 + TempFile tmpini{"TestIniTmp.ini"};
  1247 +
  1248 + int key{0};
  1249 + app.add_option("key", key)->envname("CLI11_TEST_ENV_KEY_TMP");
  1250 + app.set_config("--config", tmpini);
  1251 +
  1252 + {
  1253 + std::ofstream out{tmpini};
  1254 + out << "CLI11_TEST_ENV_KEY_TMP=3" << std::endl;
  1255 + }
  1256 +
  1257 + ASSERT_NO_THROW(run());
  1258 + EXPECT_EQ(key, 3);
  1259 +}
  1260 +
1210 1261 TEST_F(TApp, IniFlagText) {
1211 1262  
1212 1263 TempFile tmpini{"TestIniTmp.ini"};
... ... @@ -1376,6 +1427,49 @@ TEST_F(TApp, TomlOutputSimple) {
1376 1427 EXPECT_EQ("simple=3\n", str);
1377 1428 }
1378 1429  
  1430 +TEST_F(TApp, TomlOutputShort) {
  1431 +
  1432 + int v{0};
  1433 + app.add_option("-s", v);
  1434 +
  1435 + args = {"-s3"};
  1436 +
  1437 + run();
  1438 +
  1439 + std::string str = app.config_to_str();
  1440 + EXPECT_EQ("s=3\n", str);
  1441 +}
  1442 +
  1443 +TEST_F(TApp, TomlOutputPositional) {
  1444 +
  1445 + int v{0};
  1446 + app.add_option("pos", v);
  1447 +
  1448 + args = {"3"};
  1449 +
  1450 + run();
  1451 +
  1452 + std::string str = app.config_to_str();
  1453 + EXPECT_EQ("pos=3\n", str);
  1454 +}
  1455 +
  1456 +// try the output with environmental only arguments
  1457 +TEST_F(TApp, TomlOutputEnvironmental) {
  1458 +
  1459 + put_env("CLI11_TEST_ENV_TMP", "2");
  1460 +
  1461 + int val{1};
  1462 + app.add_option(std::string{}, val)->envname("CLI11_TEST_ENV_TMP");
  1463 +
  1464 + run();
  1465 +
  1466 + EXPECT_EQ(2, val);
  1467 + std::string str = app.config_to_str();
  1468 + EXPECT_EQ("CLI11_TEST_ENV_TMP=2\n", str);
  1469 +
  1470 + unset_env("CLI11_TEST_ENV_TMP");
  1471 +}
  1472 +
1379 1473 TEST_F(TApp, TomlOutputNoConfigurable) {
1380 1474  
1381 1475 int v1{0}, v2{0};
... ...