diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b10e858..529e156d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * Added `CLI::ArgumentMismatch` [#56](https://github.com/CLIUtils/CLI11/pull/56) and fixed missing failure if one arg expected [#55](https://github.com/CLIUtils/CLI11/issues/55) * Support for minimum unlimited expected arguments [#56](https://github.com/CLIUtils/CLI11/pull/56) * Single internal arg parse function [#56](https://github.com/CLIUtils/CLI11/pull/56) +* Allow options to be disabled from INI file, rename `add_config` to `set_config` [#60](https://github.com/CLIUtils/CLI11/pull/60) ## Version 1.2 diff --git a/README.md b/README.md index 7ccc5a15a..00290c6c7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![License: BSD][license-badge]](./LICENSE) [![DOI][DOI-badge]][DOI-link] -[API Docs][api-docs] • [Tutorial series][GitBook] • [What's new](./CHANGELOG.md) +[Documentation][GitBook] • +[API Reference][api-docs] • +[What's new](./CHANGELOG.md) # CLI11: Command line parser for C++11 @@ -173,6 +175,7 @@ The add commands return a pointer to an internally stored `Option`. If you set t * `->check(CLI::NonexistentPath)`: Requires that the path does not exist. * `->check(CLI::Range(min,max))`: Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0. * `->transform(std::string(std::string))`: Converts the input string into the output string, in-place in the parsed options. +* `->configurable(false)`: Disable this option from being in an ini configuration file. These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. Check takes any function that has the signature `void(const std::string&)`; it should throw a `ValidationError` when validation fails. The help message will have the name of the parent option prepended. Since `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. If you just want to see the unconverted values, use `.results()` to get the `std::vector` of results. @@ -237,13 +240,13 @@ There are several options that are supported on the main app and subcommands. Th ## Configuration file ```cpp -app.add_config(option_name, +app.set_config(option_name="", default_file_name="", help_string="Read an ini file", required=false) ``` -Adding a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format. An example of a file: +If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in `ini` format. An example of a file: ```ini ; Commments are supported, using a ; diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 90b0d18fd..a98352c5c 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -369,7 +369,7 @@ class App { Option *opt = add_option(name, fun, description, false); if(opt->get_positional()) - throw IncorrectConstruction("Flags cannot be positional"); + throw IncorrectConstruction::PositionalFlag(name); opt->set_custom_option("", 0); return opt; } @@ -389,7 +389,7 @@ class App { Option *opt = add_option(name, fun, description, false); if(opt->get_positional()) - throw IncorrectConstruction("Flags cannot be positional"); + throw IncorrectConstruction::PositionalFlag(name); opt->set_custom_option("", 0); return opt; } @@ -409,7 +409,7 @@ class App { Option *opt = add_option(name, fun, description, false); if(opt->get_positional()) - throw IncorrectConstruction("Flags cannot be positional"); + throw IncorrectConstruction::PositionalFlag(name); opt->set_custom_option("", 0); opt->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); return opt; @@ -428,7 +428,7 @@ class App { Option *opt = add_option(name, fun, description, false); if(opt->get_positional()) - throw IncorrectConstruction("Flags cannot be positional"); + throw IncorrectConstruction::PositionalFlag(name); opt->set_custom_option("", 0); return opt; } @@ -578,8 +578,8 @@ class App { return opt; } - /// Add a configuration ini file option - Option *add_config(std::string name = "--config", + /// Set a configuration ini file option, or clear it if no name passed + Option *set_config(std::string name = "", std::string default_filename = "", std::string help = "Read an ini file", bool required = false) { @@ -587,9 +587,14 @@ class App { // Remove existing config if present if(config_ptr_ != nullptr) remove_option(config_ptr_); - config_name_ = default_filename; - config_required_ = required; - config_ptr_ = add_option(name, config_name_, help, !default_filename.empty()); + + // Only add config if option passed + if(!name.empty()) { + config_name_ = default_filename; + config_required_ = required; + config_ptr_ = add_option(name, config_name_, help, !default_filename.empty()); + } + return config_ptr_; } @@ -1100,7 +1105,7 @@ class App { std::vector values = detail::parse_ini(config_name_); while(!values.empty()) { if(!_parse_ini(values)) { - throw ExtrasINIError(values.back().fullname); + throw INIError::Extras(values.back().fullname); } } } catch(const FileError &) { @@ -1149,8 +1154,7 @@ class App { if(opt->get_required() || opt->count() != 0) { // Make sure enough -N arguments parsed (+N is already handled in parsing function) if(opt->get_expected() < 0 && opt->count() < static_cast(-opt->get_expected())) - throw ArgumentMismatch(opt->single_name() + ": At least " + std::to_string(-opt->get_expected()) + - " required"); + throw ArgumentMismatch::AtLeast(opt->single_name(), -opt->get_expected()); // Required but empty if(opt->get_required() && opt->count() == 0) @@ -1167,10 +1171,8 @@ class App { } auto selected_subcommands = get_subcommands(); - if(require_subcommand_min_ > 0 && selected_subcommands.empty()) - throw RequiredError("Subcommand required"); - else if(require_subcommand_min_ > selected_subcommands.size()) - throw RequiredError("Requires at least " + std::to_string(require_subcommand_min_) + " subcommands"); + if(require_subcommand_min_ > selected_subcommands.size()) + throw RequiredError::Subcommand(require_subcommand_min_); // Convert missing (pairs) to extras (string only) if(!(allow_extras_ || prefix_command_)) { @@ -1210,6 +1212,9 @@ class App { // Let's not go crazy with pointer syntax Option_p &op = *op_ptr; + if(!op->get_configurable()) + throw INIError::NotConfigurable(current.fullname); + if(op->results_.empty()) { // Flag parsing if(op->get_expected() == 0) { @@ -1226,10 +1231,10 @@ class App { for(size_t i = 0; i < ui; i++) op->results_.emplace_back(""); } catch(const std::invalid_argument &) { - throw ConversionError(current.fullname + ": Should be true/false or a number"); + throw ConversionError::TrueFalse(current.fullname); } } else - throw ConversionError(current.fullname + ": too many inputs for a flag"); + throw ConversionError::TooManyInputsFlag(current.fullname); } else { op->results_ = current.inputs; op->run_callback(); @@ -1424,8 +1429,7 @@ class App { } if(num > 0) { - throw ArgumentMismatch(op->single_name() + ": " + std::to_string(num) + " required " + - op->get_type_name() + " missing"); + throw ArgumentMismatch::TypedAtLeast(op->single_name(), num, op->get_type_name()); } } diff --git a/include/CLI/Error.hpp b/include/CLI/Error.hpp index c76cba7f6..3eec19fe5 100644 --- a/include/CLI/Error.hpp +++ b/include/CLI/Error.hpp @@ -35,14 +35,14 @@ enum class ExitCodes { IncorrectConstruction = 100, BadNameString, OptionAlreadyAdded, - File, + FileError, ConversionError, ValidationError, RequiredError, RequiresError, ExcludesError, ExtrasError, - ExtrasINIError, + INIError, InvalidError, HorribleError, OptionNotFound, @@ -85,18 +85,52 @@ class ConstructionError : public Error { class IncorrectConstruction : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) CLI11_ERROR_SIMPLE(IncorrectConstruction) + static IncorrectConstruction PositionalFlag(std::string name) { + return IncorrectConstruction(name + ": Flags cannot be positional"); + } + static IncorrectConstruction Set0Opt(std::string name) { + return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); + } + static IncorrectConstruction ChangeNotVector(std::string name) { + return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); + } + static IncorrectConstruction AfterMultiOpt(std::string name) { + return IncorrectConstruction( + name + ": You can't change expected arguments after you've changed the multi option policy!"); + } + static IncorrectConstruction MissingOption(std::string name) { + return IncorrectConstruction("Option " + name + " is not defined"); + } + static IncorrectConstruction MultiOptionPolicy(std::string name) { + return IncorrectConstruction(name + ": multi_option_policy only works for flags and single value options"); + } }; /// Thrown on construction of a bad name class BadNameString : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, BadNameString) CLI11_ERROR_SIMPLE(BadNameString) + static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } + static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } + static BadNameString DashesOnly(std::string name) { + return BadNameString("Must have a name, not just dashes: " + name); + } + static BadNameString MultiPositionalNames(std::string name) { + return BadNameString("Only one positional name allowed, remove: " + name); + } }; /// Thrown when an option already exists class OptionAlreadyAdded : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) - CLI11_ERROR_SIMPLE(OptionAlreadyAdded) + OptionAlreadyAdded(std::string name) + : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} + static OptionAlreadyAdded Requires(std::string name, std::string other) { + return OptionAlreadyAdded(name + " requires " + other, ExitCodes::OptionAlreadyAdded); + } + static OptionAlreadyAdded Excludes(std::string name, std::string other) { + return OptionAlreadyAdded(name + " excludes " + other, ExitCodes::OptionAlreadyAdded); + } }; // Parsing errors @@ -129,7 +163,8 @@ class RuntimeError : public ParseError { /// Thrown when parsing an INI file and it is missing class FileError : public ParseError { CLI11_ERROR_DEF(ParseError, FileError) - FileError(std::string name) : FileError(name + " was not readable (missing?)", ExitCodes::File) {} + CLI11_ERROR_SIMPLE(FileError) + static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } }; /// Thrown when conversion call back fails, such as when an int fails to coerce to a string @@ -138,6 +173,14 @@ class ConversionError : public ParseError { CLI11_ERROR_SIMPLE(ConversionError) ConversionError(std::string member, std::string name) : ConversionError("The value " + member + "is not an allowed value for " + name) {} + ConversionError(std::string name, std::vector results) + : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} + static ConversionError TooManyInputsFlag(std::string name) { + return ConversionError(name + ": too many inputs for a flag"); + } + static ConversionError TrueFalse(std::string name) { + return ConversionError(name + ": Should be true/false or a number"); + } }; /// Thrown when validation of results fails @@ -150,7 +193,14 @@ class ValidationError : public ParseError { /// Thrown when a required option is missing class RequiredError : public ParseError { CLI11_ERROR_DEF(ParseError, RequiredError) - CLI11_ERROR_SIMPLE(RequiredError) + RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} + static RequiredError Subcommand(size_t min_subcom) { + if(min_subcom == 1) + return RequiredError("A subcommand"); + else + return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", + ExitCodes::RequiredError); + } }; /// Thrown when the wrong number of arguments has been received @@ -163,6 +213,13 @@ class ArgumentMismatch : public ParseError { : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + ", got " + std::to_string(recieved)), ExitCodes::ArgumentMismatch) {} + + static ArgumentMismatch AtLeast(std::string name, int num) { + return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required"); + } + static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { + return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); + } }; /// Thrown when a requires option is missing @@ -190,9 +247,13 @@ class ExtrasError : public ParseError { }; /// Thrown when extra values are found in an INI file -class ExtrasINIError : public ParseError { - CLI11_ERROR_DEF(ParseError, ExtrasINIError) - ExtrasINIError(std::string item) : ExtrasINIError("INI was not able to parse " + item, ExitCodes::ExtrasINIError) {} +class INIError : public ParseError { + CLI11_ERROR_DEF(ParseError, INIError) + CLI11_ERROR_SIMPLE(INIError) + static INIError Extras(std::string item) { return INIError("INI was not able to parse " + item); } + static INIError NotConfigurable(std::string item) { + return INIError(item + ": This option is not allowed in a configuration file"); + } }; /// Thrown when validation fails before parsing @@ -204,6 +265,7 @@ class InvalidError : public ParseError { }; /// This is just a safety check to verify selection and parsing match - you should not ever see it +/// Strings are directly added to this error, but again, it should never be seen. class HorribleError : public ParseError { CLI11_ERROR_DEF(ParseError, HorribleError) CLI11_ERROR_SIMPLE(HorribleError) diff --git a/include/CLI/Ini.hpp b/include/CLI/Ini.hpp index bab094d95..f9999fff3 100644 --- a/include/CLI/Ini.hpp +++ b/include/CLI/Ini.hpp @@ -106,7 +106,7 @@ inline std::vector parse_ini(const std::string &name) { std::ifstream input{name}; if(!input.good()) - throw FileError(name); + throw FileError::Missing(name); return parse_ini(input); } diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index 6061b39a5..7bc9a4b9e 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -41,6 +41,9 @@ template class OptionBase { /// Ignore the case when matching (option, not value) bool ignore_case_{false}; + /// Allow this option to be given in a configuration file + bool configurable_{true}; + /// Policy for multiple arguments when `expected_ == 1` (can be set on bool flags, too) MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw}; @@ -48,6 +51,7 @@ template class OptionBase { other->group(group_); other->required(required_); other->ignore_case(ignore_case_); + other->configurable(configurable_); other->multi_option_policy(multi_option_policy_); } @@ -81,6 +85,9 @@ template class OptionBase { /// The status of ignore case bool get_ignore_case() const { return ignore_case_; } + /// The status of configurable + bool get_configurable() const { return configurable_; } + /// The status of the multi option policy MultiOptionPolicy get_multi_option_policy() const { return multi_option_policy_; } @@ -106,6 +113,12 @@ template class OptionBase { self->multi_option_policy(MultiOptionPolicy::Join); return self; } + + /// Allow in a configuration file + CRTP *configurable(bool value = true) { + configurable_ = value; + return static_cast(this); + } }; class OptionDefaults : public OptionBase { @@ -235,12 +248,11 @@ class Option : public OptionBase