diff --git a/README.md b/README.md index ce7872a..7088de5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ **A single-header, dependency-free C++ command-line argument parser.** Argparse-style ergonomics for C++ — named & positional arguments, type casting, -choices, defaults, auto-generated help, and Python-like keyword arguments in C++20. +choices, value validators, defaults, auto-generated help, and Python-like +keyword arguments in C++20. [![tests](https://github.com/simfeo/FancyArgumentParser/actions/workflows/tests.yml/badge.svg)](https://github.com/simfeo/FancyArgumentParser/actions/workflows/tests.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license) @@ -28,12 +29,17 @@ submodules, no CMake packages, no linking. Everything lives in `argparse.h`. ## Features - 🧩 **Single header, zero dependencies** — just `#include "argparse.h"`. -- 🏷️ **Named and positional arguments**, freely mixed. +- 📦 **C++20 module** — optional `import argparse;` via `argparse.ixx`. +- 🏷️ **Named and positional arguments**, freely mixed in any order. - 🔢 **Typed values** — `int`, `long long`, `double`, `bool`, and `string`. -- 🎚️ **Flexible arity** — fixed counts, `kAnyArgCount` (zero-or-more), - `kFromOneToInfiniteArgCount` (one-or-more), or flags (zero values). -- ✅ **Validation** — required/optional, value `choices`, and defaults. +- 🎚️ **Flexible arity** — fixed counts, or Python-style `'?'` (zero-or-one), + `'*'` (zero-or-more) and `'+'` (one-or-more), plus the matching + `kZeroOrOneArgCount` / `kAnyArgCount` / `kFromOneToInfiniteArgCount` constants. +- ✅ **Validation** — required/optional, value `choices`, numeric `SetRange`, + `SetPositive`, filesystem checks (`SetExistingFile`), and custom + `SetValidator` predicates. - 🔗 **Variable binding** — `BindTo(&var)` writes parsed values straight into your own variables. +- 🎯 **By-name getters** — `obj.GetAsInt("count")` straight off the parsed result. - 📖 **Auto-generated help & usage**, with custom epilogue and overridable usage line. - 🔤 **Long-option abbreviations** (`--verb` → `--verbose` when unambiguous). - ⚙️ **Configurable** — custom prefix characters, ignore-unknown args, custom namespace. @@ -104,6 +110,18 @@ parser.AddArgument(argparse::CreateNamedArgument({ > **Note:** every argument is **required by default** — call `SetRequired(false)` > (or set `required = false`) to make one optional. +Short names may be written as a single **char** (`'n'`) as well as a string +(`"n"`), and arity accepts a Python-style **char** in place of a constant: + +```cpp +// 'c' short name, '?' == zero-or-one value (same as kZeroOrOneArgCount) +parser.AddArgument(argparse::CreateNamedArgument('c', "count", '?') + .SetType(argparse::ArgTypeCast::e_int).SetDefault(1)); + +// '+' == one-or-more, '*' == zero-or-more +parser.AddArgument(argparse::CreatePositionalArgument("files", '+')); +``` + ## Binding values to variables Instead of pulling each value out with `GetArg(name).GetAsX()`, you can bind an @@ -140,6 +158,108 @@ onward. - Bindings are applied **only on a successful parse**; a failed parse never writes through them. +## Validating values + +Beyond `choices`, an argument can carry a validator that each parsed value must +pass; a failing value makes `ParseArgs` return an invalid result with a message. + +```cpp +// Numeric range (also sets the type for you) +parser.AddArgument(argparse::CreateNamedArgument('p', "port") + .SetRange(1, 65535)); + +// Strictly positive +parser.AddArgument(argparse::CreateNamedArgument('r', "ratio") + .SetType(argparse::ArgTypeCast::e_double).SetPositive()); + +// Filesystem checks (available when is, i.e. C++17+) +parser.AddArgument(argparse::CreateNamedArgument('i', "input") + .SetExistingFile()); + +// Any custom predicate, with an optional error message +parser.AddArgument(argparse::CreateNamedArgument('m', "mode") + .SetValidator([](const std::string& v){ return v == "fast" || v == "safe"; }, + "mode must be 'fast' or 'safe'")); +``` + +Built-in validators include `SetRange` (int / long long / double, as `(lo, hi)` +or `(max)`), `SetPositive`, `SetNonNegative`, and the path checks +`SetExistingFile`, `SetExistingDirectory`, `SetExistingPath`, +`SetNonexistentPath`. + +## Mixing positional and named arguments + +Positional and named arguments can be declared and passed in any order — the +parser assigns bare values to positionals left to right while pulling named +options out of the stream. + +```cpp +auto parser = argparse::ArgumentParser("cp"); +parser.AddArgument(argparse::CreatePositionalArgument("src")); +parser.AddArgument(argparse::CreatePositionalArgument("dst")); +parser.AddArgument(argparse::CreateNamedArgument('f', "force", + 0, argparse::ArgTypeCast::e_bool, false)); // a flag + +// all equivalent: a b --force | --force a b | a --force b +auto obj = parser.ParseArgs(std::vector{ "a", "--force", "b" }); +std::string src = obj.GetAsString("src"); // "a" +std::string dst = obj.GetAsString("dst"); // "b" +``` + +## Reading results by name + +After a successful parse you can read values straight off the result by +argument name, without going through `GetArg(...)` first: + +```cpp +int n = obj.GetAsInt("count"); +double ratio = obj.GetAsDouble("ratio"); +std::string name = obj.GetAsString("name"); +const std::vector& nums = obj.GetAsVecInt("numbers"); +``` + +The longer `obj.GetArg("count").GetAsInt()` form still works and is handy when +you want to inspect the `Argument` itself (e.g. `GetArgumentExists()`). + +## Configuring the parser + +`ArgumentParser` can be built from a `ParserSpec` aggregate, which collects the +parser-level options in one place (the counterpart of the keyword-style argument +specs): + +```cpp +auto parser = argparse::ArgumentParser(argparse::ParserSpec{ + .name = "cptool", + .description = "Copy files", + .prefixChars = '-', + .addHelp = true, + .allowAbbrev = false}); +``` + +The designated-initializer form needs C++20; the same struct also works with +ordinary aggregate initialization in C++11/14/17. + +## Using it as a C++20 module + +On toolchains that support C++20 modules (MSVC, GCC ≥ 14) you can consume the +library through `import` instead of `#include`: + +```cpp +import argparse; + +int main(int argc, char** argv) +{ + argparse::ArgumentParser parser("demo"); + // ... same API as the header ... +} +``` + +Add `argparse.ixx` to your build as a module interface unit; it wraps +`argparse.h` and re-exports the public API. The header remains fully usable on +its own, so nothing changes for `#include` users. Define +`ARGPARSE_NAMESPACE_NAME` when building the module to rename the exported +namespace, exactly as with the header. + ## Requirements - A **C++11** compiler or newer. The library is continuously tested on diff --git a/argparse.h b/argparse.h index 37313aa..2dd0d75 100644 --- a/argparse.h +++ b/argparse.h @@ -40,24 +40,57 @@ SOFTWARE. #include #include #include +#include +#include #if __cplusplus > 201402L || _MSVC_LANG > 201402L #include #endif +// std::filesystem is available from C++17. The path-existence validators +// (SetExistingFile / ...) are only compiled when it is present. +#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +#include +#define ARGPARSE_HAS_FILESYSTEM 1 +#endif + /// @brief namespace of argument parser constants and Classes /// can be changed if ARGPARSE_NAMESPACE_NAME macro specified during compilation. /// By default is namespace name is "argparse" namespace ARGPARSE_NAMESPACE_NAME { - /// @brief anonymous namespace for internal usage - namespace + /// @brief internal helpers. A named, inline namespace (rather than an + /// anonymous one) so the symbols have external linkage and get emitted in + /// module consumers; inline keeps #include across multiple TUs valid. +#ifdef __cpp_inline_variables +#define ARGPARSE_DETAIL_CONST inline constexpr +#else +#define ARGPARSE_DETAIL_CONST const +#endif + namespace detail { - const size_t kSizeTypeEnd = static_cast(-1); - const size_t kHelpWidth = 80; - const size_t kHelpNameWidthPercent = 30; + ARGPARSE_DETAIL_CONST size_t kSizeTypeEnd = static_cast(-1); + ARGPARSE_DETAIL_CONST size_t kHelpWidth = 80; + ARGPARSE_DETAIL_CONST size_t kHelpNameWidthPercent = 30; - bool isNumber(const std::string& inStr) + inline bool iEquals(const std::string& a, const std::string& b) + { + if (a.size() != b.size()) + { + return false; + } + for (size_t i = 0; i < a.size(); ++i) + { + if (std::tolower(static_cast(a[i])) + != std::tolower(static_cast(b[i]))) + { + return false; + } + } + return true; + } + + inline bool isNumber(const std::string& inStr) { const bool hasNegSign = inStr.at(0) == '-'; size_t dotPos = 0, expPos = 0; @@ -91,12 +124,14 @@ namespace ARGPARSE_NAMESPACE_NAME return true; } - size_t getStringStreamLength(std::stringstream& showDesc) + inline size_t getStringStreamLength(std::stringstream& showDesc) { showDesc.seekp(0, std::ios::end); return showDesc.tellp(); } } + using namespace detail; +#undef ARGPARSE_DETAIL_CONST /// @brief Supported types for argument @@ -110,12 +145,69 @@ namespace ARGPARSE_NAMESPACE_NAME e_bool }; + // inline (external linkage) where available so the module wrapper can + // export them; plain const (internal linkage) otherwise. Behaviour for + // #include users is identical. +#ifdef __cpp_inline_variables +#define ARGPARSE_CONST inline constexpr +#else +#define ARGPARSE_CONST const +#endif /// @brief constant to indicate arguments with various /// count from 0 to infinite - const int kAnyArgCount = -1; + ARGPARSE_CONST int kAnyArgCount = -1; /// @brief constant to indicate arguments with various /// count from 1 to infinite - const int kFromOneToInfiniteArgCount = -2; + ARGPARSE_CONST int kFromOneToInfiniteArgCount = -2; + /// @brief constant to indicate an argument that takes zero or one value + /// (Python's nargs='?'). + ARGPARSE_CONST int kZeroOrOneArgCount = -3; +#undef ARGPARSE_CONST + + /// @brief Argument count value. Accepts either an integer (an exact count, + /// or one of the k...ArgCount constants) or a Python-style character: + /// '?' (zero-or-one), '*' (zero-or-more), '+' (one-or-more). + /// Implicitly convertible to int so it can be used anywhere a plain count is + /// expected. An invalid character throws std::runtime_error at definition time. + struct NArgs + { + int value; + + NArgs(int n = 1) : value(n) {} + NArgs(char c) : value(FromChar(c)) {} + + operator int() const { return value; } + + static int FromChar(char c) + { + switch (c) + { + case '?': return kZeroOrOneArgCount; + case '*': return kAnyArgCount; + case '+': return kFromOneToInfiniteArgCount; + default: + throw std::runtime_error( + std::string("invalid nargs character '") + c + + "'; expected '?', '*' or '+'"); + } + } + }; + + + /// @brief Argument name value. Accepts a std::string, a string literal, or a + /// single char -- so a short name can be written as 'f' as well as "f". + /// Implicitly converts to std::string, so it works anywhere a name is taken. + struct ArgName + { + std::string value; + + ArgName() {} + ArgName(char c) : value(1, c) {} + ArgName(const char* s) : value(s ? s : "") {} + ArgName(const std::string& s) : value(s) {} + + operator const std::string&() const { return value; } + }; class ArgumentParser; @@ -158,9 +250,9 @@ namespace ARGPARSE_NAMESPACE_NAME /// @param argType type of argument. Defined via enum. Supported types are: int, long long, double and bool and string for all other cases. /// @param required Is argument required. Will fail parsing, if required argument are not present. /// @param help Your own custom help string start. - static Argument CreateNamedArgument(const std::string& shortName = "", - const std::string& longName = "", - const int argsCount = 1, + static Argument CreateNamedArgument(const ArgName& shortName = "", + const ArgName& longName = "", + NArgs argsCount = 1, ArgTypeCast argType = ArgTypeCast::e_String, const bool required = true, const std::string& help = "") @@ -174,8 +266,8 @@ namespace ARGPARSE_NAMESPACE_NAME /// @param argType type of argument. Defined via enum. Supported types are: int, long long, double and bool and string for all other cases. /// @param required Is argument required. Will fail parsing, if required argument are not present. /// @param help Your own custom help string start. - static Argument CreatePositionalArgument(const std::string& positionalName = "", - const int argsCount = 1, + static Argument CreatePositionalArgument(const ArgName& positionalName = "", + NArgs argsCount = 1, ArgTypeCast argType = ArgTypeCast::e_String, const bool required = true, const std::string& help = "") @@ -212,15 +304,24 @@ namespace ARGPARSE_NAMESPACE_NAME int m_nargs = 1; /// @brief setter function for m_nargs with desired amount - /// @param amount int value that indicates amount of argument. - /// Could be "kAnyArgCount" or "kFromOneToInfiniteArgCount", 0 or any other positive integer. + /// @param amount argument count: an integer (exact count or a + /// k...ArgCount constant), or a Python-style character '?' / '*' / '+'. /// @return reference to current argument - Argument& SetNumberOfArguments(int amount) + Argument& SetNumberOfArguments(NArgs amount) { m_nargs = amount; return *this; } + /// @brief Handy setter for an argument that takes zero or one value + /// (Python's nargs='?'). + /// @return reference to current argument + Argument& SetZeroOrOneArgument() + { + m_nargs = kZeroOrOneArgCount; + return *this; + } + /// @brief Handy setter for argument count with self declared name /// @return reference to current argument Argument& SetAnyNumberOfArgumentsButAtLeastOne() @@ -266,7 +367,7 @@ namespace ARGPARSE_NAMESPACE_NAME /// @brief Handy setter for positional argument /// @param name name for positional argument. Empty by default /// @return reference to current argument - Argument& SetPositionalName(const std::string& name) + Argument& SetPositionalName(const ArgName& name) { m_positionalName = name; return *this; @@ -282,7 +383,7 @@ namespace ARGPARSE_NAMESPACE_NAME /// Can be auto-generated if possible when m_allowAbbrev in ArgumentParsed set to true. /// @param name name for positional argument. Empty by default /// @return reference to current argument - Argument& SetShortName(const std::string& name) + Argument& SetShortName(const ArgName& name) { m_shortName = name; return *this; @@ -299,7 +400,7 @@ namespace ARGPARSE_NAMESPACE_NAME /// and m_allowAbbrev in ArgumentParsed is "true". /// @param name name for positional argument. Empty by default /// @return reference to current argument - Argument& SetLongName(const std::string& name) + Argument& SetLongName(const ArgName& name) { m_longName = name; return *this; @@ -321,17 +422,21 @@ namespace ARGPARSE_NAMESPACE_NAME /// @brief vector of strings to validate arguments input data. /// Empty by default. Will fail parsing if string not is in input list std::vector m_choicesString = {}; + /// @brief when true, string choices are matched case-insensitively + bool m_choicesIgnoreCase = false; /// @brief Handy setter of valid choices for arguments with string type /// @param choices vector or initializer list of valid strings + /// @param ignoreCase match case-insensitively (false by default) /// @return reference to current argument - Argument& SetChoices(const std::vector& choices) + Argument& SetChoices(const std::vector& choices, bool ignoreCase = false) { if (m_type != ArgTypeCast::e_String) { throw std::runtime_error("wrong type"); } m_choicesString = choices; + m_choicesIgnoreCase = ignoreCase; return *this; } @@ -339,10 +444,11 @@ namespace ARGPARSE_NAMESPACE_NAME /// SetChoices({"+", "-"}) -- resolves unambiguously to the string /// choices instead of colliding with the int/double/long long overloads. /// @param choices initializer list of string literals + /// @param ignoreCase match case-insensitively (false by default) /// @return reference to current argument - Argument& SetChoices(std::initializer_list choices) + Argument& SetChoices(std::initializer_list choices, bool ignoreCase = false) { - return SetChoices(std::vector(choices.begin(), choices.end())); + return SetChoices(std::vector(choices.begin(), choices.end()), ignoreCase); } /// @brief vector of integers to validate arguments input data. @@ -586,9 +692,155 @@ namespace ARGPARSE_NAMESPACE_NAME } } + /// @brief Install a validator: each parsed value token must satisfy + /// @p predicate, otherwise parsing fails. Runs on the raw value (so it + /// works for any type; convert inside the predicate if needed). + /// @param predicate returns true for an accepted value + /// @param message custom error text (a default is used when empty) + /// @return reference to current argument + Argument& SetValidator(std::function predicate, + const std::string& message = "") + { + m_validator = std::move(predicate); + m_validatorMessage = message; + return *this; + } + + /// @brief Whether a value passes this argument's validator (true if none). + bool RunValidator(const std::string& value) const + { + return !m_validator || m_validator(value); + } + + /// @brief Custom validator error message ("" means use the default). + const std::string& ValidatorMessage() const + { + return m_validatorMessage; + } + + /// @brief Restrict an integer argument to the inclusive range [lo, hi]. + /// Sets the type to e_int and installs a validator. + Argument& SetRange(int lo, int hi) + { + m_type = ArgTypeCast::e_int; + return SetRangeLL(lo, hi); + } + + /// @brief Restrict a long long argument to the inclusive range [lo, hi]. + Argument& SetRange(long long lo, long long hi) + { + m_type = ArgTypeCast::e_longlong; + return SetRangeLL(lo, hi); + } + + /// @brief Restrict a double argument to the inclusive range [lo, hi]. + Argument& SetRange(double lo, double hi) + { + m_type = ArgTypeCast::e_double; + return SetValidator( + [lo, hi](const std::string& s) { + try { double v = std::stod(s); return v >= lo && v <= hi; } + catch (...) { return false; } + }, + "value out of range [" + std::to_string(lo) + ", " + std::to_string(hi) + "]"); + } + + /// @brief Restrict an integer argument to [0, max]. + Argument& SetRange(int max) { return SetRange(0, max); } + /// @brief Restrict a long long argument to [0, max]. + Argument& SetRange(long long max) { return SetRange(0LL, max); } + /// @brief Restrict a double argument to [0, max]. + Argument& SetRange(double max) { return SetRange(0.0, max); } + + /// @brief Require a strictly positive number (> 0). Type-agnostic. + Argument& SetPositive(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + try { return std::stod(s) > 0.0; } catch (...) { return false; } + }, + message.empty() ? "value must be positive" : message); + } + + /// @brief Require a non-negative number (>= 0). Type-agnostic. + Argument& SetNonNegative(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + try { return std::stod(s) >= 0.0; } catch (...) { return false; } + }, + message.empty() ? "value must be non-negative" : message); + } + + /// @brief Require the value to fully match an ECMAScript regular + /// expression. An invalid pattern throws std::regex_error at definition. + Argument& SetPattern(const std::string& pattern, const std::string& message = "") + { + std::regex re(pattern); + return SetValidator( + [re](const std::string& s) { return std::regex_match(s, re); }, + message.empty() ? ("value does not match pattern \"" + pattern + "\"") : message); + } + +#ifdef ARGPARSE_HAS_FILESYSTEM + /// @brief Require the value to name an existing regular file (C++17+). + Argument& SetExistingFile(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + std::error_code ec; return std::filesystem::is_regular_file(s, ec); + }, + message.empty() ? "file does not exist" : message); + } + + /// @brief Require the value to name an existing directory (C++17+). + Argument& SetExistingDirectory(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + std::error_code ec; return std::filesystem::is_directory(s, ec); + }, + message.empty() ? "directory does not exist" : message); + } + + /// @brief Require the value to name an existing path (C++17+). + Argument& SetExistingPath(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + std::error_code ec; return std::filesystem::exists(s, ec); + }, + message.empty() ? "path does not exist" : message); + } + + /// @brief Require the value to name a path that does NOT exist (C++17+). + Argument& SetNonexistentPath(const std::string& message = "") + { + return SetValidator( + [](const std::string& s) { + std::error_code ec; return !std::filesystem::exists(s, ec); + }, + message.empty() ? "path already exists" : message); + } +#endif + private: + /// @brief Shared integer-range validator for SetRange(int) / SetRange(long long). + Argument& SetRangeLL(long long lo, long long hi) + { + return SetValidator( + [lo, hi](const std::string& s) { + try { long long v = std::stoll(s); return v >= lo && v <= hi; } + catch (...) { return false; } + }, + "value out of range [" + std::to_string(lo) + ", " + std::to_string(hi) + "]"); + } + /// @brief type-erased sink installed by BindTo(...); empty when unbound std::function m_binding = nullptr; + /// @brief value predicate installed by SetValidator(...); empty when unset + std::function m_validator = nullptr; + std::string m_validatorMessage = ""; bool m_hasDefault = false; std::vector m_defaultBool = {}; @@ -612,9 +864,9 @@ namespace ARGPARSE_NAMESPACE_NAME /// @return instance of Argument /// @note inline: this is a free function in a header, so it must have /// inline linkage to be safely included in more than one translation unit. - inline Argument CreateNamedArgument(const std::string& shortName = "", - const std::string& longName = "", - const int argsCount = 1, + inline Argument CreateNamedArgument(const ArgName& shortName = "", + const ArgName& longName = "", + NArgs argsCount = 1, ArgTypeCast argType = ArgTypeCast::e_String, const bool required = true, const std::string& help = "") @@ -631,8 +883,8 @@ namespace ARGPARSE_NAMESPACE_NAME /// @param help Initial part of help for current argument in case of auto-generated help. /// @return instance of Argument /// @note inline: see CreateNamedArgument -- required for multi-TU inclusion. - inline Argument CreatePositionalArgument(const std::string& positionalName = "", - const int argsCount = 1, + inline Argument CreatePositionalArgument(const ArgName& positionalName = "", + NArgs argsCount = 1, ArgTypeCast argType = ArgTypeCast::e_String, const bool required = true, const std::string& help = "") @@ -654,9 +906,9 @@ namespace ARGPARSE_NAMESPACE_NAME /// The same struct also works with ordinary aggregate init in C++11/14/17. struct NamedArgSpec { - std::string shortName = ""; - std::string longName = ""; - int nargs = 1; + ArgName shortName = ""; + ArgName longName = ""; + NArgs nargs = 1; ArgTypeCast type = ArgTypeCast::e_String; bool required = true; std::string help = ""; @@ -676,7 +928,7 @@ namespace ARGPARSE_NAMESPACE_NAME struct PositionalArgSpec { std::string name = ""; - int nargs = 1; + NArgs nargs = 1; ArgTypeCast type = ArgTypeCast::e_String; bool required = true; std::string help = ""; @@ -889,10 +1141,8 @@ namespace ARGPARSE_NAMESPACE_NAME friend ArgumentsObject; }; - // ---- Argument::BindTo definitions ------------------------------------- - // Defined out-of-line (but still inline) because they read values through - // ArgumentParsed's typed getters, which only become complete right here. - // Each overload also fixes the argument type to match the bound variable. + // Out-of-line: BindTo needs ArgumentParsed's getters, complete only here. + // Each overload also sets the argument type to match the bound variable. inline Argument& Argument::BindTo(bool* target) { @@ -1006,6 +1256,24 @@ namespace ARGPARSE_NAMESPACE_NAME return argument->second; } + /// @name By-name value shortcuts + /// Equivalent to GetArg(name).GetAsX(). The scalar forms throw + /// std::out_of_range when the argument holds no value, so guard optional + /// arguments with GetArg(name).GetArgumentExists() first. + /// @{ + bool GetAsBool(const std::string& name) { return GetArg(name).GetAsBool(); } + int GetAsInt(const std::string& name) { return GetArg(name).GetAsInt(); } + long long GetAsLongLong(const std::string& name) { return GetArg(name).GetAsLongLong(); } + double GetAsDouble(const std::string& name) { return GetArg(name).GetAsDouble(); } + std::string GetAsString(const std::string& name) { return GetArg(name).GetAsString(); } + + std::vector GetAsVecBool(const std::string& name) { return GetArg(name).GetAsVecBool(); } + std::vector GetAsVecInt(const std::string& name) { return GetArg(name).GetAsVecInt(); } + std::vector GetAsVecLongLong(const std::string& name) { return GetArg(name).GetAsVecLongLong(); } + std::vector GetAsVecDouble(const std::string& name) { return GetArg(name).GetAsVecDouble(); } + std::vector GetAsVecString(const std::string& name) { return GetArg(name).GetAsVecString(); } + /// @} + private: ArgumentsObject() {} @@ -1129,16 +1397,22 @@ namespace ARGPARSE_NAMESPACE_NAME return true; } + if (argObj.m_nargs != 0 && !argObj.RunValidator(token)) + { + return InvalidateArgsValidator(argObj, token); + } + if (argument->second.m_type == ArgTypeCast::e_String) { if (argObj.m_nargs != 0) { if (argObj.m_choicesString.size()) { + const bool ic = argObj.m_choicesIgnoreCase; auto it = std::find_if(argObj.m_choicesString.begin(), argObj.m_choicesString.end(), - [&token](const std::string& str) -> bool + [&token, ic](const std::string& str) -> bool { - return token == str; + return ic ? iEquals(token, str) : token == str; }); if (it == argObj.m_choicesString.end()) { @@ -1337,6 +1611,21 @@ namespace ARGPARSE_NAMESPACE_NAME return false; } + /// @brief Helper that reports a value rejected by a SetValidator predicate + /// @param argObj Argument whose validator rejected the value + /// @param token the rejected value + /// @return false + bool InvalidateArgsValidator(const Argument& argObj, const std::string& token) + { + const std::string& name = argObj.m_longName.empty() ? (argObj.m_shortName.empty() ? argObj.m_positionalName : argObj.m_shortName) : argObj.m_longName; + + SetErrorString(argObj.ValidatorMessage().empty() + ? ("Invalid value \"" + token + "\" for argument \"" + name + "\"") + : argObj.ValidatorMessage()); + + return false; + } + bool m_isValid = false; std::string m_error; std::map m_parsed; @@ -1346,8 +1635,30 @@ namespace ARGPARSE_NAMESPACE_NAME }; + /// @brief Aggregate description of a parser, for keyword-style construction + /// -- the parser-level counterpart of NamedArgSpec / PositionalArgSpec: + /// @code + /// auto parser = argparse::ArgumentParser({ + /// .name = "cptool", + /// .description = "Copy files", + /// .allowAbbrev = false}); + /// @endcode + /// Also works with ordinary aggregate init in C++11/14/17. Field order + /// follows the declaration below. + struct ParserSpec + { + std::string name = ""; + std::string description = ""; + std::string epilogue = ""; + std::string usage = ""; + char prefixChars = '-'; + bool addHelp = true; + bool allowAbbrev = true; + bool ignoreUnknownArgs = false; + }; + /// @brief Main class of argument parser - /// hold all user arguments from code and orchestrate other classes + /// hold all user arguments from code and orchestrate other classes /// in order to parse command line input class ArgumentParser { @@ -1358,6 +1669,19 @@ namespace ARGPARSE_NAMESPACE_NAME : m_name(name) {} + /// @brief Keyword-style constructor. See ParserSpec. + /// @param spec aggregate of the parser's properties + ArgumentParser(const ParserSpec& spec) noexcept + : m_allowAbbrev(spec.allowAbbrev) + , m_addHelp(spec.addHelp) + , m_ignoreUnknownArgs(spec.ignoreUnknownArgs) + , m_prefix(spec.prefixChars) + , m_name(spec.name) + , m_description(spec.description) + , m_epilogue(spec.epilogue) + , m_usage(spec.usage) + {} + /// @brief Overload default description for auto-generated command line /// @param description Text to display before the argument help ("" by default) /// @return reference to current parser @@ -1433,6 +1757,23 @@ namespace ARGPARSE_NAMESPACE_NAME return *this; } + /// @brief Add a named argument straight from its spec, without going + /// through CreateNamedArgument, e.g. + /// AddArgument({.shortName='f', .longName="file", .required=true}); + /// @param spec aggregate of the argument's properties + void AddArgument(const NamedArgSpec& spec) + { + AddArgument(CreateNamedArgument(spec)); + } + + /// @brief Add a positional argument straight from its spec. See the + /// NamedArgSpec overload. + /// @param spec aggregate of the argument's properties + void AddArgument(const PositionalArgSpec& spec) + { + AddArgument(CreatePositionalArgument(spec)); + } + /// @brief Function to add arguments specification to command line parser /// @param arg Argument instance void AddArgument(const Argument& arg) @@ -1524,6 +1865,11 @@ namespace ARGPARSE_NAMESPACE_NAME bool positionalArgsEndFlag = false; size_t currentArgumentObjectIndex = kSizeTypeEnd; + // Value tokens consumed by the active option; caps fixed/'?' nargs. + size_t currentArgConsumed = 0; + // After a satisfied fixed option, extra bare tokens are positionals + // -- unlike tokens after an ignored unknown option, which are dropped. + bool spillToPositional = false; std::vector positionalArgs; ArgumentsObject argObj; for (size_t i =0; i < args.size(); ++i) @@ -1550,6 +1896,8 @@ namespace ARGPARSE_NAMESPACE_NAME if (foundArgObject != m_knownArgumentNamesInternal.end()) { currentArgumentObjectIndex = foundArgObject->second.position; + currentArgConsumed = 0; + spillToPositional = false; Argument& argument = m_arguments[currentArgumentObjectIndex]; if (argument.m_nargs == 0) @@ -1586,12 +1934,16 @@ namespace ARGPARSE_NAMESPACE_NAME } continue; } - else if (el.find(_pref) == 0 || el.find(_doublePref) == 0) + // A negative number (e.g. "-3") is a value, not an option, even + // though it starts with the prefix. + else if ((el.find(_pref) == 0 || el.find(_doublePref) == 0) && !isNumber(el)) { if (!_unknownArgumentHit(argObj, i+1, currentArgumentObjectIndex, positionalArgsEndFlag, el)) { return argObj; } + // Tokens after an ignored unknown option are not positionals. + spillToPositional = false; continue; } @@ -1599,10 +1951,30 @@ namespace ARGPARSE_NAMESPACE_NAME { Argument& argument = m_arguments[currentArgumentObjectIndex]; - if (!argObj.Parse(argument, currentArgumentObjectIndex, el)) + // Bounded options ('?' -> 1, fixed -> nargs) stop once full; + // the rest spill to positionals. '*' / '+' stay greedy. + const bool bounded = argument.m_nargs >= 0 + || argument.m_nargs == kZeroOrOneArgCount; + const size_t boundMax = argument.m_nargs >= 0 + ? static_cast(argument.m_nargs) : 1u; + if (bounded && currentArgConsumed >= boundMax) + { + currentArgumentObjectIndex = kSizeTypeEnd; + spillToPositional = true; + positionalArgs.push_back(el); + } + else if (!argObj.Parse(argument, currentArgumentObjectIndex, el)) { return argObj; } + else + { + ++currentArgConsumed; + } + } + else if (spillToPositional) + { + positionalArgs.push_back(el); // overflow after a satisfied option } } @@ -1613,97 +1985,108 @@ namespace ARGPARSE_NAMESPACE_NAME argObj.SetErrorString("Unknown positional argument:" + positionalArgs.front()); return argObj; } - size_t minimumRequiredPositionalCount = 0; - size_t infiniteRequiredPositionalCount = 0; - size_t optionalPositionalCount = 0; + // Distribute tokens across positionals argparse-style: each takes + // between its min and max, and a variable ('*'/'+') one greedily + // absorbs the slack while reserving the minimums that follow it. + const size_t positionalDefsCount = m_positionalArgumentNames.size(); + const size_t totalTokens = positionalArgs.size(); - for (auto& el : m_positionalArgumentNames) + std::vector minTokens(positionalDefsCount, 0); + std::vector isVariable(positionalDefsCount, false); + size_t sumMin = 0; + size_t sumMaxFixed = 0; + bool anyVariable = false; + + for (size_t k = 0; k < positionalDefsCount; ++k) { - if (m_arguments[el.positionInArguments].m_required) + const Argument& a = m_arguments[m_positionalArgumentNames[k].positionInArguments]; + if (a.m_nargs == kAnyArgCount || a.m_nargs == kFromOneToInfiniteArgCount) { - minimumRequiredPositionalCount += m_arguments[el.positionInArguments].m_nargs == kFromOneToInfiniteArgCount ? 1 : m_arguments[el.positionInArguments].m_nargs; - infiniteRequiredPositionalCount = m_arguments[el.positionInArguments].m_nargs == kFromOneToInfiniteArgCount; + isVariable[k] = true; + anyVariable = true; + minTokens[k] = (a.m_nargs == kFromOneToInfiniteArgCount && a.m_required) ? 1 : 0; + } + else if (a.m_nargs == kZeroOrOneArgCount) + { + // '?' : zero or one + minTokens[k] = 0; + sumMaxFixed += 1; } else { - ++optionalPositionalCount; + const size_t n = static_cast(a.m_nargs); + minTokens[k] = a.m_required ? n : 0; + sumMaxFixed += n; } + sumMin += minTokens[k]; + } + + if (totalTokens < sumMin) + { + argObj.SetErrorString("Too few positional arguments: required " + + std::to_string(sumMin) + " got " + std::to_string(totalTokens)); + return argObj; } - if (minimumRequiredPositionalCount > positionalArgs.size()) + if (!anyVariable && totalTokens > sumMaxFixed) { - argObj.SetErrorString("Too few positional arguments: required " + std::to_string(minimumRequiredPositionalCount) + " got " + std::to_string(positionalArgs.size())); + argObj.SetErrorString("Too many positional arguments!"); return argObj; } - size_t totalTokensForRequiredNargs = 1; - size_t additionalTokensForFirstRequiredNarg = 0; - size_t howMuchOptionalArgsCanBeParsed = positionalArgs.size() - minimumRequiredPositionalCount; - if (howMuchOptionalArgsCanBeParsed > optionalPositionalCount) + size_t currentTokenPosition = 0; + for (size_t k = 0; k < positionalDefsCount; ++k) { - if (infiniteRequiredPositionalCount == 0) + const auto& def = m_positionalArgumentNames[k]; + Argument& argument = m_arguments[def.positionInArguments]; + + size_t reserveAfter = 0; + for (size_t j = k + 1; j < positionalDefsCount; ++j) { - argObj.SetErrorString("Too many positional arguments!"); - return argObj; + reserveAfter += minTokens[j]; + } + const size_t remaining = totalTokens - currentTokenPosition; + const size_t avail = remaining > reserveAfter ? remaining - reserveAfter : 0; + + size_t take; + if (isVariable[k]) + { + take = avail; // greedy: grab the slack + } + else if (argument.m_nargs == kZeroOrOneArgCount) + { + take = (avail >= 1) ? 1 : 0; // '?' : zero or one } else { - totalTokensForRequiredNargs = (howMuchOptionalArgsCanBeParsed - optionalPositionalCount) / infiniteRequiredPositionalCount; - additionalTokensForFirstRequiredNarg = (howMuchOptionalArgsCanBeParsed - optionalPositionalCount) % infiniteRequiredPositionalCount; + // Fixed: all-or-nothing (optional takes N only if available). + const size_t n = static_cast(argument.m_nargs); + take = (avail >= n) ? n : (argument.m_required ? n : 0); } - howMuchOptionalArgsCanBeParsed = optionalPositionalCount; - } - - - size_t currentTokenPosition = 0; - size_t optionalParsed = 0; - - for (auto& el : m_positionalArgumentNames) - { - Argument& argument = m_arguments[el.positionInArguments]; - argObj.CreateParsingStub(argument, el.positionInArguments); - - if (m_arguments[el.positionInArguments].m_required) + if (take > remaining) // defensive: never index past the tokens { - - if (m_arguments[el.positionInArguments].m_nargs != kFromOneToInfiniteArgCount) - { - for (size_t i = 0; i < static_cast(m_arguments[el.positionInArguments].m_nargs); ++i) - { - if (!argObj.Parse(argument, el.positionInArguments, positionalArgs[currentTokenPosition])) - { - return argObj; - } - ++currentTokenPosition; - } - } - else - { - size_t addtionalArg = 0; - if (additionalTokensForFirstRequiredNarg > 0) - { - ++addtionalArg; - --additionalTokensForFirstRequiredNarg; - } - for (size_t i = 0; i < totalTokensForRequiredNargs + addtionalArg; ++i) - { - if (!argObj.Parse(argument, el.positionInArguments, positionalArgs[currentTokenPosition])) - { - return argObj; - } - ++currentTokenPosition; - } - } + take = remaining; + } + if (take == 0) + { + continue; // absent optional positional } - else if (optionalParsed <= howMuchOptionalArgsCanBeParsed) + + argObj.CreateParsingStub(argument, def.positionInArguments); + for (size_t t = 0; t < take; ++t) { - if (!argObj.Parse(argument, el.positionInArguments, positionalArgs[currentTokenPosition])) + if (!argObj.Parse(argument, def.positionInArguments, positionalArgs[currentTokenPosition])) { return argObj; } ++currentTokenPosition; - ++optionalParsed; } } + + if (currentTokenPosition < totalTokens) + { + argObj.SetErrorString("Too many positional arguments!"); + return argObj; + } } for (size_t i = 0; i < m_arguments.size(); ++i) @@ -1717,7 +2100,8 @@ namespace ARGPARSE_NAMESPACE_NAME { if (static_cast(parsedArg.GetArgumentCount()) == el.m_nargs || el.m_nargs == kAnyArgCount - || (el.m_nargs == kFromOneToInfiniteArgCount && parsedArg.GetArgumentCount() >= 1)) + || (el.m_nargs == kFromOneToInfiniteArgCount && parsedArg.GetArgumentCount() >= 1) + || (el.m_nargs == kZeroOrOneArgCount && parsedArg.GetArgumentCount() <= 1)) { continue; } @@ -1731,17 +2115,16 @@ namespace ARGPARSE_NAMESPACE_NAME { argObj.ParseDefault(el, i); } - else if (el.m_required) + // '*' and '?' are satisfied by zero values even if required. + else if (el.m_required && el.m_nargs != kAnyArgCount && el.m_nargs != kZeroOrOneArgCount) { argObj.SetErrorString("Required argument with name \"" + name + "\" does not exist"); return argObj; } } - // Bindings: parsing and validation succeeded, so push values into - // any variables the caller registered with Argument::BindTo(...). - // Optional arguments that are absent (and have no default) do not - // exist here, so their bound variables are left untouched. + // Parsing succeeded: push values into any BindTo(...) variables. + // Absent optionals aren't present here, so their variables stay put. for (size_t i = 0; i < m_arguments.size(); ++i) { const Argument& el = m_arguments[i]; diff --git a/argparse.ixx b/argparse.ixx new file mode 100644 index 0000000..2e04b35 --- /dev/null +++ b/argparse.ixx @@ -0,0 +1,50 @@ +// C++20 module interface for the single-header argparse library. +// +// This is a thin wrapper: argparse.h is included in the global module +// fragment (so it compiles exactly as it does for #include users, with all +// internal helpers kept private), and only the public API is re-exported. +// +// Usage: import argparse; instead of #include "argparse.h" +// +// The header remains fully usable on its own; this file is only needed by +// consumers who want the module form and toolchains that support C++20 +// modules (MSVC, recent GCC/Clang). Define ARGPARSE_NAMESPACE_NAME before +// building this unit to change the exported namespace, same as the header. + +module; + +#include "argparse.h" + +export module argparse; + +#ifndef ARGPARSE_NAMESPACE_NAME +#define ARGPARSE_NAMESPACE_NAME argparse +#endif + +export namespace ARGPARSE_NAMESPACE_NAME +{ + // Sentinel argument-count constants. + using ARGPARSE_NAMESPACE_NAME::kAnyArgCount; + using ARGPARSE_NAMESPACE_NAME::kFromOneToInfiniteArgCount; + using ARGPARSE_NAMESPACE_NAME::kZeroOrOneArgCount; + + // Enums and value types. + using ARGPARSE_NAMESPACE_NAME::ArgTypeCast; + using ARGPARSE_NAMESPACE_NAME::NArgs; + using ARGPARSE_NAMESPACE_NAME::ArgName; + + // Argument specification helpers. + using ARGPARSE_NAMESPACE_NAME::NamedArgSpec; + using ARGPARSE_NAMESPACE_NAME::PositionalArgSpec; + using ARGPARSE_NAMESPACE_NAME::ParserSpec; + + // Core classes. + using ARGPARSE_NAMESPACE_NAME::Argument; + using ARGPARSE_NAMESPACE_NAME::ArgumentParsed; + using ARGPARSE_NAMESPACE_NAME::ArgumentsObject; + using ARGPARSE_NAMESPACE_NAME::ArgumentParser; + + // Free factory functions (all overloads). + using ARGPARSE_NAMESPACE_NAME::CreateNamedArgument; + using ARGPARSE_NAMESPACE_NAME::CreatePositionalArgument; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 117aad2..3dc76b5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,3 +31,44 @@ else() endif() add_test(NAME test_argparse COMMAND test_argparse) + +# --- Optional C++20 module build ------------------------------------------- +# Builds the same library through argparse.ixx (import argparse;) instead of the +# header. Only enabled where the toolchain reliably supports named modules: +# MSVC, or GCC >= 14, with CMake >= 3.28 and C++20 or newer. Everything else +# (e.g. AppleClang) simply skips it, so the header-only path is never affected. +set(_argparse_build_module OFF) +if(NOT CMAKE_VERSION VERSION_LESS "3.28" AND CMAKE_CXX_STANDARD GREATER_EQUAL 20) + if(MSVC) + set(_argparse_build_module ON) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" + AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS "14") + set(_argparse_build_module ON) + endif() +endif() + +if(_argparse_build_module) + # Enable scanning of ordinary sources for `import` (off by default because + # cmake_minimum_required above predates modules); needed so the consumer is + # ordered after the module interface. + if(POLICY CMP0155) + cmake_policy(SET CMP0155 NEW) + endif() + add_executable(test_module test_module.cpp) + target_sources(test_module + PRIVATE + FILE_SET argparse_module TYPE CXX_MODULES + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${CMAKE_CURRENT_SOURCE_DIR}/../argparse.ixx) + target_include_directories(test_module PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + target_compile_features(test_module PRIVATE cxx_std_20) + if(MSVC) + target_compile_options(test_module PRIVATE /W4) + else() + target_compile_options(test_module PRIVATE -Wall -Wextra) + endif() + add_test(NAME test_module COMMAND test_module) + message(STATUS "argparse: C++20 module test enabled") +else() + message(STATUS "argparse: C++20 module test skipped (toolchain/standard)") +endif() diff --git a/tests/test_argparse.cpp b/tests/test_argparse.cpp index 1784d1c..e00391d 100644 --- a/tests/test_argparse.cpp +++ b/tests/test_argparse.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "argparse.h" @@ -588,6 +589,22 @@ static void test_designated_initializers_cpp20() CHECK(path.GetAsString() == "out.txt"); CHECK(obj.GetArg("numbers").GetAsVecInt().size() == 3); } + +// Character nargs works inside the C++20 keyword-style spec structs, e.g. +// .nargs = '?' / '*' / '+'. +static void test_char_nargs_in_spec_cpp20() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreateNamedArgument({ + .shortName = "c", .longName = "color", .nargs = '?', .required = false})); + parser.AddArgument(argparse::CreatePositionalArgument({ + .name = "files", .nargs = '*', .required = false})); + + auto obj = parser.ParseArgs(std::vector{ "a", "b", "--color", "auto" }); + CHECK(obj.IsArgValid()); + CHECK(obj.GetArg("color").GetAsString() == "auto"); + CHECK(obj.GetArg("files").GetAsVecString().size() == 2); +} #endif // BindTo writes parsed scalar values straight into the bound variables. @@ -685,6 +702,680 @@ static void test_bind_not_applied_on_parse_failure() CHECK(bound == -1); // binding not applied on failure } +// Positional with a variable count (the cell earlier tests never crossed). + +// '*' (zero-or-more) positional accepts 0, 1, and many tokens. +static void test_positional_star_counts() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("files") + .SetAnyNumberOfArguments()); // kAnyArgCount, default required + return p; + }; + auto zero = make().ParseArgs(std::vector{}); + CHECK(zero.IsArgValid()); // zero is allowed for '*' + CHECK(!zero.GetArg("files").GetArgumentExists()); + + auto one = make().ParseArgs(std::vector{ "a" }); + CHECK(one.IsArgValid()); + CHECK(one.GetArg("files").GetAsVecString().size() == 1); + + auto many = make().ParseArgs(std::vector{ "a", "b", "c" }); + CHECK(many.IsArgValid()); + CHECK(many.GetArg("files").GetAsVecString().size() == 3); // was the bug +} + +// '+' (one-or-more) positional rejects zero, accepts one and many. +static void test_positional_plus_counts() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("files") + .SetAnyNumberOfArgumentsButAtLeastOne()); // kFromOneToInfinite + return p; + }; + auto zero = make().ParseArgs(std::vector{}); + CHECK(!zero.IsArgValid()); // '+' needs at least one + + auto one = make().ParseArgs(std::vector{ "a" }); + CHECK(one.IsArgValid()); + CHECK(one.GetArg("files").GetAsVecString().size() == 1); + + auto many = make().ParseArgs(std::vector{ "a", "b", "c" }); + CHECK(many.IsArgValid()); + CHECK(many.GetArg("files").GetAsVecString().size() == 3); +} + +// Fixed positional then a variable one: fixed takes its count, variable the rest. +static void test_positional_fixed_then_variable() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreatePositionalArgument("cmd")); // fixed 1 + parser.AddArgument(argparse::CreatePositionalArgument("rest") + .SetAnyNumberOfArguments().SetRequired(false)); + + auto obj = parser.ParseArgs(std::vector{ "run", "a", "b", "c" }); + CHECK(obj.IsArgValid()); + CHECK(obj.GetArg("cmd").GetAsString() == "run"); + CHECK(obj.GetArg("rest").GetAsVecString().size() == 3); +} + +// A single fixed positional given too many tokens is rejected. +static void test_positional_too_many() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreatePositionalArgument("x")); // fixed 1 + + auto obj = parser.ParseArgs(std::vector{ "a", "b" }); + CHECK(!obj.IsArgValid()); + CHECK(!obj.GetErrorString().empty()); +} + +// --- Option/positional ordering (the nargs-bounded consumption fix) --------- + +// A fixed option takes its nargs; leftover tokens are positionals, either order. +static void test_option_positional_ordering() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("v", "verbose", 1) + .SetRequired(false)); + p.AddArgument(argparse::CreatePositionalArgument("file").SetRequired(false)); + return p; + }; + // option first + auto a = make().ParseArgs(std::vector{ "--verbose", "true", "f" }); + CHECK(a.IsArgValid()); + CHECK(a.GetArg("verbose").GetAsString() == "true"); + CHECK(a.GetArg("file").GetAsString() == "f"); + + // positional first + auto b = make().ParseArgs(std::vector{ "f", "--verbose", "true" }); + CHECK(b.IsArgValid()); + CHECK(b.GetArg("verbose").GetAsString() == "true"); + CHECK(b.GetArg("file").GetAsString() == "f"); +} + +// A fixed-count option must not swallow trailing tokens meant for positionals. +static void test_option_does_not_overconsume() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreateNamedArgument("v", "verbose", 1) + .SetRequired(false)); + parser.AddArgument(argparse::CreatePositionalArgument("files") + .SetAnyNumberOfArguments().SetRequired(false)); + + auto obj = parser.ParseArgs(std::vector{ "--verbose", "true", "a", "b" }); + CHECK(obj.IsArgValid()); + CHECK(obj.GetArg("verbose").GetArgumentCount() == 1); // not 3 + CHECK(obj.GetArg("files").GetAsVecString().size() == 2); // a, b +} + +// --- nargs='?' (zero-or-one) and character nargs input ---------------------- + +// A '?' positional takes zero or one token; absent falls back to the default. +static void test_positional_optional_single() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("out") + .SetZeroOrOneArgument().SetRequired(false) + .SetDefault(std::string("a.out"))); + return p; + }; + auto present = make().ParseArgs(std::vector{ "file" }); + CHECK(present.IsArgValid()); + CHECK(present.GetArg("out").GetAsString() == "file"); + + auto absent = make().ParseArgs(std::vector{}); + CHECK(absent.IsArgValid()); + CHECK(absent.GetArg("out").GetAsString() == "a.out"); // default fallback +} + +// A fixed positional then a '?' one; an extra token is rejected. +static void test_fixed_then_optional_single() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("cmd")); + p.AddArgument(argparse::CreatePositionalArgument("out") + .SetZeroOrOneArgument().SetRequired(false)); + return p; + }; + auto one = make().ParseArgs(std::vector{ "run" }); + CHECK(one.IsArgValid()); + CHECK(one.GetArg("cmd").GetAsString() == "run"); + CHECK(!one.GetArg("out").GetArgumentExists()); + + auto two = make().ParseArgs(std::vector{ "run", "x" }); + CHECK(two.IsArgValid()); + CHECK(two.GetArg("out").GetAsString() == "x"); + + auto three = make().ParseArgs(std::vector{ "run", "x", "y" }); + CHECK(!three.IsArgValid()); // too many +} + +// A named '?' option: with a value takes it; bare it is present with 0 values. +static void test_named_optional_single() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("c", "color") + .SetZeroOrOneArgument().SetRequired(false)); + return p; + }; + auto withVal = make().ParseArgs(std::vector{ "--color", "auto" }); + CHECK(withVal.IsArgValid()); + CHECK(withVal.GetArg("color").GetAsString() == "auto"); + + auto bare = make().ParseArgs(std::vector{ "--color" }); + CHECK(bare.IsArgValid()); + CHECK(bare.GetArg("color").GetArgumentExists()); + CHECK(bare.GetArg("color").GetArgumentCount() == 0); +} + +// Character nargs ('*','+','?') are equivalent to the k...ArgCount constants. +static void test_char_nargs_input() +{ + // '*' via char == kAnyArgCount + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("n", "nums", '*', + argparse::ArgTypeCast::e_int, false)); + auto o = p.ParseArgs(std::vector{ "--nums", "1", "2", "3" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("nums").GetAsVecInt().size() == 3); + } + // '+' via SetNumberOfArguments('+') rejects zero + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("files") + .SetNumberOfArguments('+')); + CHECK(!p.ParseArgs(std::vector{}).IsArgValid()); + CHECK(p.ParseArgs(std::vector{ "a", "b" }).IsArgValid()); + } + // '?' via SetNumberOfArguments('?') == kZeroOrOneArgCount (zero or one) + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("out") + .SetNumberOfArguments('?').SetRequired(false)); + CHECK(p.ParseArgs(std::vector{}).IsArgValid()); // zero + CHECK(p.ParseArgs(std::vector{ "x" }).IsArgValid()); // one + CHECK(!p.ParseArgs(std::vector{ "x", "y" }).IsArgValid()); // too many + } + // an invalid nargs character throws at definition time + bool threw = false; + try { argparse::CreateNamedArgument("x", "y", '@'); } + catch (const std::exception&) { threw = true; } + CHECK(threw); +} + +// --- Validators (SetValidator) ---------------------------------------------- + +// A validator rejects a value and reports the custom message. +static void test_validator_custom_message() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreateNamedArgument("f", "file", 1) + .SetRequired(false) + .SetValidator([](const std::string& s) { return !s.empty() && s[0] == '/'; }, + "file must be absolute")); + + auto ok = parser.ParseArgs(std::vector{ "--file", "/etc/hosts" }); + CHECK(ok.IsArgValid()); + + auto bad = parser.ParseArgs(std::vector{ "--file", "rel" }); + CHECK(!bad.IsArgValid()); + CHECK(bad.GetErrorString() == "file must be absolute"); +} + +// Without a message, the default names the value and argument. +static void test_validator_default_message() +{ + auto parser = argparse::ArgumentParser("prog"); + parser.AddArgument(argparse::CreateNamedArgument("m", "mode", 1) + .SetRequired(false) + .SetValidator([](const std::string& s) { return s == "on" || s == "off"; })); + + auto bad = parser.ParseArgs(std::vector{ "--mode", "maybe" }); + CHECK(!bad.IsArgValid()); + CHECK(bad.GetErrorString().find("maybe") != std::string::npos); + CHECK(bad.GetErrorString().find("mode") != std::string::npos); +} + +// A validator on a numeric argument runs on each value (positive values only, +// to avoid the separate negative-number-as-option limitation). +static void test_validator_numeric() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("n", "num", 1, + argparse::ArgTypeCast::e_int, false) + .SetValidator([](const std::string& s) { + try { return std::stoi(s) <= 10; } catch (...) { return false; } }, + "num must be <= 10")); + return p; + }; + auto ok = make().ParseArgs(std::vector{ "--num", "5" }); + CHECK(ok.IsArgValid()); + CHECK(ok.GetArg("num").GetAsInt() == 5); + + auto bad = make().ParseArgs(std::vector{ "--num", "50" }); + CHECK(!bad.IsArgValid()); +} + +// A validator applies to every value of a variable-count positional. +static void test_validator_positional_each() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("letters") + .SetAnyNumberOfArguments() + .SetValidator([](const std::string& s) { return s.size() == 1; })); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "a", "b", "c" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "a", "bb" }).IsArgValid()); +} + +// --- Negative numbers as values, and SetRange ------------------------------- + +// A negative number is a value, not an option, for named and positional args. +static void test_negative_number_values() +{ + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("n", "num", 1, + argparse::ArgTypeCast::e_int, false)); + auto o = p.ParseArgs(std::vector{ "--num", "-3" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("num").GetAsInt() == -3); + } + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreatePositionalArgument("nums") + .SetAnyNumberOfArguments().SetType(argparse::ArgTypeCast::e_int)); + auto o = p.ParseArgs(std::vector{ "-1", "-2", "3" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("nums").GetAsVecInt().size() == 3); + CHECK(o.GetArg("nums").GetAsVecInt().at(0) == -1); + } + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("x", "xval", 1, + argparse::ArgTypeCast::e_double, false)); + auto o = p.ParseArgs(std::vector{ "--xval", "-2.5" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("xval").GetAsDouble() == -2.5); + } +} + +// SetRange restricts int and double arguments to an inclusive range. +static void test_set_range() +{ + { + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("p", "port", 1) + .SetRequired(false).SetRange(1, 65535)); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--port", "8080" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--port", "70000" }).IsArgValid()); + } + { + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("r", "ratio", 1) + .SetRequired(false).SetRange(0.0, 1.0)); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--ratio", "0.5" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--ratio", "2.0" }).IsArgValid()); + } + // negative bounds and a negative value (exercises both features together) + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("t", "temp", 1) + .SetRequired(false).SetRange(-40, 50)); + auto o = p.ParseArgs(std::vector{ "--temp", "-10" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("temp").GetAsInt() == -10); + } +} + +// --- Standard validators: range shorthand, sign, pattern, CI choices -------- + +static void test_range_shorthand_and_signs() +{ + // SetRange(max) == [0, max] + { + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("a", "num", 1) + .SetRequired(false).SetRange(10)); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--num", "5" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--num", "15" }).IsArgValid()); + } + // SetPositive: > 0 + { + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("b", "cnt", 1, + argparse::ArgTypeCast::e_int, false).SetPositive()); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--cnt", "5" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--cnt", "0" }).IsArgValid()); + } + // SetNonNegative: >= 0 (negative value reaches the validator and is rejected) + { + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("c", "lvl", 1, + argparse::ArgTypeCast::e_double, false).SetNonNegative()); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--lvl", "0" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--lvl", "-1" }).IsArgValid()); + } +} + +static void test_pattern_validator() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("e", "email", 1) + .SetRequired(false).SetPattern(R"(^[^@]+@[^@]+\.[^@]+$)")); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--email", "a@b.com" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--email", "nope" }).IsArgValid()); +} + +static void test_choices_case_insensitive() +{ + auto make = []{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("m", "mode", 1) + .SetRequired(false).SetChoices({ "On", "Off" }, true)); + return p; + }; + CHECK(make().ParseArgs(std::vector{ "--mode", "on" }).IsArgValid()); + CHECK(make().ParseArgs(std::vector{ "--mode", "OFF" }).IsArgValid()); + CHECK(!make().ParseArgs(std::vector{ "--mode", "maybe" }).IsArgValid()); +} + +#ifdef ARGPARSE_HAS_FILESYSTEM +static void test_filesystem_validators() +{ + const std::string tmp = "argparse_fs_test.tmp"; + { std::ofstream(tmp) << "x"; } // create a real file + + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("f", "file", 1) + .SetRequired(false).SetExistingFile()); + CHECK(p.ParseArgs(std::vector{ "--file", tmp }).IsArgValid()); + CHECK(!p.ParseArgs(std::vector{ "--file", "no_such_file_xyz" }).IsArgValid()); + } + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("d", "dir", 1) + .SetRequired(false).SetExistingDirectory()); + CHECK(p.ParseArgs(std::vector{ "--dir", "." }).IsArgValid()); + CHECK(!p.ParseArgs(std::vector{ "--dir", tmp }).IsArgValid()); // a file, not dir + } + { + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("n", "np", 1) + .SetRequired(false).SetNonexistentPath()); + CHECK(p.ParseArgs(std::vector{ "--np", "no_such_path_xyz" }).IsArgValid()); + CHECK(!p.ParseArgs(std::vector{ "--np", tmp }).IsArgValid()); + } + + std::remove(tmp.c_str()); +} +#endif + +// --- Ergonomic shortcuts: spec overload, char names, by-name getters -------- + +// A short name may be given as a char as well as a string. +static void test_char_short_name() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument('n', "num", 1, + argparse::ArgTypeCast::e_int, false)); + p.AddArgument(argparse::CreateNamedArgument("s", "str", 1, + argparse::ArgTypeCast::e_String, false)); // string form still works + + auto o = p.ParseArgs(std::vector{ "-n", "7", "--str", "hi" }); + CHECK(o.IsArgValid()); + CHECK(o.GetArg("num").GetAsInt() == 7); + CHECK(o.GetArg("str").GetAsString() == "hi"); +} + +// GetAsX(name) is shorthand for GetArg(name).GetAsX() -- every scalar form. +static void test_by_name_getters_scalar() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("s", "str", 1, + argparse::ArgTypeCast::e_String, false)); + p.AddArgument(argparse::CreateNamedArgument("n", "num", 1, + argparse::ArgTypeCast::e_int, false)); + p.AddArgument(argparse::CreateNamedArgument("l", "big", 1, + argparse::ArgTypeCast::e_longlong, false)); + p.AddArgument(argparse::CreateNamedArgument("d", "ratio", 1, + argparse::ArgTypeCast::e_double, false)); + p.AddArgument(argparse::CreateNamedArgument("b", "flag", 1, + argparse::ArgTypeCast::e_bool, false)); + + auto o = p.ParseArgs(std::vector{ + "--str", "hi", "--num", "3", "--big", "9000000000", + "--ratio", "1.5", "--flag", "true" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsString("str") == "hi"); + CHECK(o.GetAsInt("num") == 3); + CHECK(o.GetAsLongLong("big") == 9000000000LL); + CHECK(o.GetAsDouble("ratio") == 1.5); + CHECK(o.GetAsBool("flag") == true); + // identical to the long form + CHECK(o.GetAsString("str") == o.GetArg("str").GetAsString()); + CHECK(o.GetAsInt("num") == o.GetArg("num").GetAsInt()); +} + +// ... and every vector form. +static void test_by_name_getters_vector() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument("s", "strs", '+', + argparse::ArgTypeCast::e_String, false)); + p.AddArgument(argparse::CreateNamedArgument("n", "nums", '+', + argparse::ArgTypeCast::e_int, false)); + p.AddArgument(argparse::CreateNamedArgument("l", "bigs", '+', + argparse::ArgTypeCast::e_longlong, false)); + p.AddArgument(argparse::CreateNamedArgument("d", "dbls", '+', + argparse::ArgTypeCast::e_double, false)); + p.AddArgument(argparse::CreateNamedArgument("b", "bools", '+', + argparse::ArgTypeCast::e_bool, false)); + + auto o = p.ParseArgs(std::vector{ + "--strs", "a", "b", "--nums", "1", "2", "--bigs", "9000000000", + "--dbls", "1.5", "2.5", "--bools", "true", "false" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsVecString("strs").size() == 2); + CHECK(o.GetAsVecInt("nums").size() == 2); + CHECK(o.GetAsVecInt("nums").at(1) == 2); + CHECK(o.GetAsVecLongLong("bigs").at(0) == 9000000000LL); + CHECK(o.GetAsVecDouble("dbls").at(1) == 2.5); + CHECK(o.GetAsVecBool("bools").size() == 2); + CHECK(o.GetAsVecBool("bools").at(0) == true); +} + +// ParserSpec configures the parser without a fluent chain (aggregate form, +// works in every standard). +static void test_parser_spec_aggregate() +{ + argparse::ParserSpec spec; + spec.name = "tool"; + spec.description = "the description"; + spec.epilogue = "the epilogue"; + spec.allowAbbrev = false; + spec.ignoreUnknownArgs = true; + + argparse::ArgumentParser p(spec); + p.AddArgument(argparse::CreatePositionalArgument("x").SetRequired(false)); + + const std::string help = p.GetHelp(80); + CHECK(help.find("the description") != std::string::npos); + CHECK(help.find("the epilogue") != std::string::npos); + + // ignoreUnknownArgs from the spec took effect + auto o = p.ParseArgs(std::vector{ "v", "--bogus" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsString("x") == "v"); +} + +// The remaining ParserSpec fields each take effect: usage, prefixChars, +// addHelp and allowAbbrev. +static void test_parser_spec_remaining_fields() +{ + // usage overrides the generated usage line + { + argparse::ParserSpec s; + s.name = "t"; + s.usage = "MY CUSTOM USAGE"; + argparse::ArgumentParser p(s); + CHECK(p.GetHelp(80).find("MY CUSTOM USAGE") != std::string::npos); + } + // prefixChars changes the option prefix + { + argparse::ParserSpec s; + s.name = "t"; + s.prefixChars = '+'; + argparse::ArgumentParser p(s); + p.AddArgument(argparse::CreateNamedArgument("n", "num", 1, + argparse::ArgTypeCast::e_int, false)); + auto o = p.ParseArgs(std::vector{ "++num", "5" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsInt("num") == 5); + } + // addHelp = false removes the automatic -h/--help option. + // Note: the help option is registered during ParseArgs, so parse first. + { + argparse::ParserSpec s; + s.name = "t"; + s.addHelp = false; + argparse::ArgumentParser p(s); + p.ParseArgs(std::vector{}); + CHECK(p.GetHelp(80).find("--help") == std::string::npos); + + argparse::ParserSpec s2; + s2.name = "t"; // default addHelp = true + argparse::ArgumentParser p2(s2); + p2.ParseArgs(std::vector{}); + CHECK(p2.GetHelp(80).find("--help") != std::string::npos); + } + // allowAbbrev = false rejects an abbreviated long option + { + argparse::ParserSpec s; + s.name = "t"; + s.allowAbbrev = false; + argparse::ArgumentParser p(s); + p.AddArgument(argparse::CreateNamedArgument("", "verbose", 1, + argparse::ArgTypeCast::e_String, false)); + CHECK(!p.ParseArgs(std::vector{ "--verb", "x" }).IsArgValid()); + CHECK(p.ParseArgs(std::vector{ "--verbose", "x" }).IsArgValid()); + } +} + +// The string constructor still works and is unambiguous alongside ParserSpec. +static void test_parser_string_ctor_still_works() +{ + auto a = argparse::ArgumentParser("plain"); + std::string name = "fromstring"; + auto b = argparse::ArgumentParser(name); + auto c = argparse::ArgumentParser("chained").SetDescription("d"); + CHECK(c.GetHelp(80).find("d") != std::string::npos); + (void)a; (void)b; +} + +// A char short name also works through the fluent setters. +static void test_char_name_setters() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument(argparse::CreateNamedArgument() + .SetShortName('n').SetLongName("num") + .SetType(argparse::ArgTypeCast::e_int).SetRequired(false)); + p.AddArgument(argparse::CreatePositionalArgument().SetPositionalName("path") + .SetRequired(false)); + + auto o = p.ParseArgs(std::vector{ "f.txt", "-n", "7" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsInt("num") == 7); + CHECK(o.GetAsString("path") == "f.txt"); +} + +#if __cplusplus >= 202002L || _MSVC_LANG >= 202002L +// AddArgument takes a spec directly, with a char short name. +static void test_add_argument_spec_overload() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument({.shortName = 'f', .longName = "file", .required = false}); + p.AddArgument(argparse::PositionalArgSpec{.name = "rest", .nargs = '*', .required = false}); + + auto o = p.ParseArgs(std::vector{ "--file", "x.txt", "a", "b" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsString("file") == "x.txt"); + CHECK(o.GetAsVecString("rest").size() == 2); + // the char short name resolves too + auto o2 = p.ParseArgs(std::vector{ "-f", "y.txt" }); + CHECK(o2.IsArgValid()); + CHECK(o2.GetAsString("file") == "y.txt"); +} + +// Keyword-style parser construction with designated initializers. +static void test_parser_spec_designated_cpp20() +{ + auto p = argparse::ArgumentParser({ + .name = "cptool", + .description = "Copy files", + .epilogue = "See docs", + .allowAbbrev = false, + .ignoreUnknownArgs = true}); + p.AddArgument({.name = "source"}); + + const std::string help = p.GetHelp(80); + CHECK(help.find("Copy files") != std::string::npos); + CHECK(help.find("See docs") != std::string::npos); + + auto o = p.ParseArgs(std::vector{ "a.txt", "--unknown" }); + CHECK(o.IsArgValid()); // ignoreUnknownArgs from spec + CHECK(o.GetAsString("source") == "a.txt"); +} + +// Bare braces pick the right spec: .name is unique to PositionalArgSpec and +// .shortName/.longName to NamedArgSpec, so no type name is needed. +static void test_bare_brace_spec_disambiguation() +{ + auto p = argparse::ArgumentParser("prog"); + p.AddArgument({.name = "source", .help = "src"}); + p.AddArgument({.name = "count", .type = argparse::ArgTypeCast::e_int}); + p.AddArgument({.name = "extras", .nargs = '*', .required = false}); + p.AddArgument({.shortName = 'v', .longName = "verbose", .nargs = 0, .required = false}); + + auto o = p.ParseArgs(std::vector{ "a.txt", "3", "x.txt", "-v" }); + CHECK(o.IsArgValid()); + CHECK(o.GetAsString("source") == "a.txt"); + CHECK(o.GetAsInt("count") == 3); + CHECK(o.GetAsVecString("extras").size() == 1); + CHECK(o.GetArg("verbose").GetArgumentExists()); +} +#endif + int main() { RUN(test_named_int_vector); @@ -724,6 +1415,7 @@ int main() RUN(test_spec_struct_named_and_positional); #if __cplusplus >= 202002L || _MSVC_LANG >= 202002L RUN(test_designated_initializers_cpp20); + RUN(test_char_nargs_in_spec_cpp20); #endif RUN(test_bind_scalar_values); RUN(test_bind_infers_type); @@ -731,6 +1423,40 @@ int main() RUN(test_bind_absent_optional_untouched); RUN(test_bind_positional); RUN(test_bind_not_applied_on_parse_failure); + RUN(test_positional_star_counts); + RUN(test_positional_plus_counts); + RUN(test_positional_fixed_then_variable); + RUN(test_positional_too_many); + RUN(test_option_positional_ordering); + RUN(test_option_does_not_overconsume); + RUN(test_positional_optional_single); + RUN(test_fixed_then_optional_single); + RUN(test_named_optional_single); + RUN(test_char_nargs_input); + RUN(test_validator_custom_message); + RUN(test_validator_default_message); + RUN(test_validator_numeric); + RUN(test_validator_positional_each); + RUN(test_negative_number_values); + RUN(test_set_range); + RUN(test_range_shorthand_and_signs); + RUN(test_pattern_validator); + RUN(test_choices_case_insensitive); +#ifdef ARGPARSE_HAS_FILESYSTEM + RUN(test_filesystem_validators); +#endif + RUN(test_char_short_name); + RUN(test_char_name_setters); + RUN(test_parser_spec_aggregate); + RUN(test_parser_spec_remaining_fields); + RUN(test_parser_string_ctor_still_works); + RUN(test_by_name_getters_scalar); + RUN(test_by_name_getters_vector); +#if __cplusplus >= 202002L || _MSVC_LANG >= 202002L + RUN(test_add_argument_spec_overload); + RUN(test_bare_brace_spec_disambiguation); + RUN(test_parser_spec_designated_cpp20); +#endif std::cout << "\n" << (g_checks - g_failures) << "/" << g_checks << " checks passed." << std::endl; diff --git a/tests/test_module.cpp b/tests/test_module.cpp new file mode 100644 index 0000000..205999e --- /dev/null +++ b/tests/test_module.cpp @@ -0,0 +1,46 @@ +// Smoke test for the C++20 module form of the library (argparse.ixx). +// +// Built only where the toolchain supports modules (see tests/CMakeLists.txt); +// it exercises the same public API as the header tests but reaches it through +// `import argparse;` instead of `#include "argparse.h"`. + +import argparse; + +#include +#include +#include + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::cerr << "FAIL: " #cond " (" << __FILE__ << ":" \ + << __LINE__ << ")\n"; \ + ++failures; \ + } \ + } while (0) + +int main() +{ + argparse::ArgumentParser parser("demo"); + parser.AddArgument(argparse::CreateNamedArgument("n", "numbers") + .SetAnyNumberOfArgumentsButAtLeastOne() + .SetType(argparse::ArgTypeCast::e_int)); + + auto obj = parser.ParseArgs(std::vector{ "-n", "1", "2", "3" }); + CHECK(obj.IsArgValid()); + + const std::vector& v = obj.GetArg("numbers").GetAsVecInt(); + CHECK(v.size() == 3); + CHECK(v.at(0) == 1); + CHECK(v.at(2) == 3); + + // Re-exported sentinel constant is visible through the module. + int anyCount = argparse::kAnyArgCount; + CHECK(anyCount == -1); + + if (failures == 0) + std::cout << "module test: all checks passed.\n"; + return failures == 0 ? 0 : 1; +}