mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-04-29 20:23:55 +00:00
This adds a round trip test for config file generation to the fuzzer. (the next step after this PR will be a fuzzer that verifies that the round trip actually matches the results. This change ended up requiring quite a few minor changes to fix the ambiguities between the config file generation and config file reader. 1). There was a number of potential conflicts between positional names and regular option names that could be triggered in config files, this required a number of additional checks on the positional naming to ensure no conflicts. 2). flag options with disable flag override can produce output results that are not valid by themselves, resolving this required flag input to be able to handle an array and output the original value set of results. 3). strings with non-printable characters could cause all sorts of chaos in the config files. This was resolved by generating a binary string conversion format and handling multiline comments and characters, and handling escaped characters. Note; I think a better solution is to move to fully supporting string formatting and escaping along with the binary strings from TOML now that TOML 1.0 is finalized. That will not be this PR though, maybe the next one. 4). Lot of ambiguities and edge cases in the string splitter, this was reworked 5). handling of comments was not done well, especially comment characters in the name of the option which is allowed. 6). non printable characters in the option naming. This would be weird in practice but it also cause some big holes in the config file generation, so the restricted character set for option naming was expanded. (don't allow spaces or control characters). --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
361 lines
16 KiB
C++
361 lines
16 KiB
C++
// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner
|
|
// under NSF AWARD 1414736 and by the respective contributors.
|
|
// All rights reserved.
|
|
//
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#pragma once
|
|
|
|
// [CLI11:public_includes:set]
|
|
#include <exception>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
// [CLI11:public_includes:end]
|
|
|
|
// CLI library includes
|
|
#include "Macros.hpp"
|
|
#include "StringTools.hpp"
|
|
|
|
namespace CLI {
|
|
// [CLI11:error_hpp:verbatim]
|
|
|
|
// Use one of these on all error classes.
|
|
// These are temporary and are undef'd at the end of this file.
|
|
#define CLI11_ERROR_DEF(parent, name) \
|
|
protected: \
|
|
name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \
|
|
name(std::string ename, std::string msg, ExitCodes exit_code) \
|
|
: parent(std::move(ename), std::move(msg), exit_code) {} \
|
|
\
|
|
public: \
|
|
name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \
|
|
name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {}
|
|
|
|
// This is added after the one above if a class is used directly and builds its own message
|
|
#define CLI11_ERROR_SIMPLE(name) \
|
|
explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {}
|
|
|
|
/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut,
|
|
/// int values from e.get_error_code().
|
|
enum class ExitCodes {
|
|
Success = 0,
|
|
IncorrectConstruction = 100,
|
|
BadNameString,
|
|
OptionAlreadyAdded,
|
|
FileError,
|
|
ConversionError,
|
|
ValidationError,
|
|
RequiredError,
|
|
RequiresError,
|
|
ExcludesError,
|
|
ExtrasError,
|
|
ConfigError,
|
|
InvalidError,
|
|
HorribleError,
|
|
OptionNotFound,
|
|
ArgumentMismatch,
|
|
BaseClass = 127
|
|
};
|
|
|
|
// Error definitions
|
|
|
|
/// @defgroup error_group Errors
|
|
/// @brief Errors thrown by CLI11
|
|
///
|
|
/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors.
|
|
/// @{
|
|
|
|
/// All errors derive from this one
|
|
class Error : public std::runtime_error {
|
|
int actual_exit_code;
|
|
std::string error_name{"Error"};
|
|
|
|
public:
|
|
CLI11_NODISCARD int get_exit_code() const { return actual_exit_code; }
|
|
|
|
CLI11_NODISCARD std::string get_name() const { return error_name; }
|
|
|
|
Error(std::string name, std::string msg, int exit_code = static_cast<int>(ExitCodes::BaseClass))
|
|
: runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {}
|
|
|
|
Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast<int>(exit_code)) {}
|
|
};
|
|
|
|
// Note: Using Error::Error constructors does not work on GCC 4.7
|
|
|
|
/// Construction errors (not in parsing)
|
|
class ConstructionError : public Error {
|
|
CLI11_ERROR_DEF(Error, ConstructionError)
|
|
};
|
|
|
|
/// Thrown when an option is set to conflicting values (non-vector and multi args, for example)
|
|
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 SetFlag(std::string name) {
|
|
return IncorrectConstruction(name + ": Cannot set an expected number for flags");
|
|
}
|
|
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 exact 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 MissingDash(std::string name) {
|
|
return BadNameString("Long names strings require 2 dashes " + name);
|
|
}
|
|
static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); }
|
|
static BadNameString BadPositionalName(std::string name) {
|
|
return BadNameString("Invalid positional 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)
|
|
explicit OptionAlreadyAdded(std::string name)
|
|
: OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {}
|
|
static OptionAlreadyAdded Requires(std::string name, std::string other) {
|
|
return {name + " requires " + other, ExitCodes::OptionAlreadyAdded};
|
|
}
|
|
static OptionAlreadyAdded Excludes(std::string name, std::string other) {
|
|
return {name + " excludes " + other, ExitCodes::OptionAlreadyAdded};
|
|
}
|
|
};
|
|
|
|
// Parsing errors
|
|
|
|
/// Anything that can error in Parse
|
|
class ParseError : public Error {
|
|
CLI11_ERROR_DEF(Error, ParseError)
|
|
};
|
|
|
|
// Not really "errors"
|
|
|
|
/// This is a successful completion on parsing, supposed to exit
|
|
class Success : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, Success)
|
|
Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {}
|
|
};
|
|
|
|
/// -h or --help on command line
|
|
class CallForHelp : public Success {
|
|
CLI11_ERROR_DEF(Success, CallForHelp)
|
|
CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
|
|
};
|
|
|
|
/// Usually something like --help-all on command line
|
|
class CallForAllHelp : public Success {
|
|
CLI11_ERROR_DEF(Success, CallForAllHelp)
|
|
CallForAllHelp()
|
|
: CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
|
|
};
|
|
|
|
/// -v or --version on command line
|
|
class CallForVersion : public Success {
|
|
CLI11_ERROR_DEF(Success, CallForVersion)
|
|
CallForVersion()
|
|
: CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {}
|
|
};
|
|
|
|
/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code.
|
|
class RuntimeError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, RuntimeError)
|
|
explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {}
|
|
};
|
|
|
|
/// Thrown when parsing an INI file and it is missing
|
|
class FileError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, FileError)
|
|
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
|
|
class ConversionError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ConversionError)
|
|
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<std::string> 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
|
|
class ValidationError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ValidationError)
|
|
CLI11_ERROR_SIMPLE(ValidationError)
|
|
explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {}
|
|
};
|
|
|
|
/// Thrown when a required option is missing
|
|
class RequiredError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, RequiredError)
|
|
explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {}
|
|
static RequiredError Subcommand(std::size_t min_subcom) {
|
|
if(min_subcom == 1) {
|
|
return RequiredError("A subcommand");
|
|
}
|
|
return {"Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError};
|
|
}
|
|
static RequiredError
|
|
Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) {
|
|
if((min_option == 1) && (max_option == 1) && (used == 0))
|
|
return RequiredError("Exactly 1 option from [" + option_list + "]");
|
|
if((min_option == 1) && (max_option == 1) && (used > 1)) {
|
|
return {"Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) +
|
|
" were given",
|
|
ExitCodes::RequiredError};
|
|
}
|
|
if((min_option == 1) && (used == 0))
|
|
return RequiredError("At least 1 option from [" + option_list + "]");
|
|
if(used < min_option) {
|
|
return {"Requires at least " + std::to_string(min_option) + " options used and only " +
|
|
std::to_string(used) + "were given from [" + option_list + "]",
|
|
ExitCodes::RequiredError};
|
|
}
|
|
if(max_option == 1)
|
|
return {"Requires at most 1 options be given from [" + option_list + "]", ExitCodes::RequiredError};
|
|
|
|
return {"Requires at most " + std::to_string(max_option) + " options be used and " + std::to_string(used) +
|
|
"were given from [" + option_list + "]",
|
|
ExitCodes::RequiredError};
|
|
}
|
|
};
|
|
|
|
/// Thrown when the wrong number of arguments has been received
|
|
class ArgumentMismatch : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ArgumentMismatch)
|
|
CLI11_ERROR_SIMPLE(ArgumentMismatch)
|
|
ArgumentMismatch(std::string name, int expected, std::size_t received)
|
|
: ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name +
|
|
", got " + std::to_string(received))
|
|
: ("Expected at least " + std::to_string(-expected) + " arguments to " + name +
|
|
", got " + std::to_string(received)),
|
|
ExitCodes::ArgumentMismatch) {}
|
|
|
|
static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) {
|
|
return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " +
|
|
std::to_string(received));
|
|
}
|
|
static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) {
|
|
return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " +
|
|
std::to_string(received));
|
|
}
|
|
static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) {
|
|
return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing");
|
|
}
|
|
static ArgumentMismatch FlagOverride(std::string name) {
|
|
return ArgumentMismatch(name + " was given a disallowed flag override");
|
|
}
|
|
static ArgumentMismatch PartialType(std::string name, int num, std::string type) {
|
|
return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) +
|
|
" required for each element");
|
|
}
|
|
};
|
|
|
|
/// Thrown when a requires option is missing
|
|
class RequiresError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, RequiresError)
|
|
RequiresError(std::string curname, std::string subname)
|
|
: RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {}
|
|
};
|
|
|
|
/// Thrown when an excludes option is present
|
|
class ExcludesError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ExcludesError)
|
|
ExcludesError(std::string curname, std::string subname)
|
|
: ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {}
|
|
};
|
|
|
|
/// Thrown when too many positionals or options are found
|
|
class ExtrasError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ExtrasError)
|
|
explicit ExtrasError(std::vector<std::string> args)
|
|
: ExtrasError((args.size() > 1 ? "The following arguments were not expected: "
|
|
: "The following argument was not expected: ") +
|
|
detail::rjoin(args, " "),
|
|
ExitCodes::ExtrasError) {}
|
|
ExtrasError(const std::string &name, std::vector<std::string> args)
|
|
: ExtrasError(name,
|
|
(args.size() > 1 ? "The following arguments were not expected: "
|
|
: "The following argument was not expected: ") +
|
|
detail::rjoin(args, " "),
|
|
ExitCodes::ExtrasError) {}
|
|
};
|
|
|
|
/// Thrown when extra values are found in an INI file
|
|
class ConfigError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, ConfigError)
|
|
CLI11_ERROR_SIMPLE(ConfigError)
|
|
static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); }
|
|
static ConfigError NotConfigurable(std::string item) {
|
|
return ConfigError(item + ": This option is not allowed in a configuration file");
|
|
}
|
|
};
|
|
|
|
/// Thrown when validation fails before parsing
|
|
class InvalidError : public ParseError {
|
|
CLI11_ERROR_DEF(ParseError, InvalidError)
|
|
explicit InvalidError(std::string name)
|
|
: InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) {
|
|
}
|
|
};
|
|
|
|
/// 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)
|
|
};
|
|
|
|
// After parsing
|
|
|
|
/// Thrown when counting a non-existent option
|
|
class OptionNotFound : public Error {
|
|
CLI11_ERROR_DEF(Error, OptionNotFound)
|
|
explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {}
|
|
};
|
|
|
|
#undef CLI11_ERROR_DEF
|
|
#undef CLI11_ERROR_SIMPLE
|
|
|
|
/// @}
|
|
|
|
// [CLI11:error_hpp:end]
|
|
} // namespace CLI
|