From da523f3d6fb1784863693f1a678839df16f5084f Mon Sep 17 00:00:00 2001 From: Stephane Del Pino <stephane.delpino44@gmail.com> Date: Thu, 11 Apr 2019 21:08:34 +0200 Subject: [PATCH] git subrepo commit packages/CLI11 subrepo: subdir: "packages/CLI11" merged: "76d2cde65" upstream: origin: "git@github.com:CLIUtils/CLI11.git" branch: "master" commit: "76d2cde65" git-subrepo: version: "0.4.0" origin: "git@github.com:ingydotnet/git-subrepo.git" commit: "5d6aba9" --- packages/CLI11/.ci/azure-steps.yml | 18 + packages/CLI11/.ci/build_doxygen.sh | 2 +- packages/CLI11/.editorconfig | 9 + packages/CLI11/.gitrepo | 4 +- packages/CLI11/.travis.yml | 17 +- packages/CLI11/CHANGELOG.md | 107 +- packages/CLI11/CMakeLists.txt | 20 +- packages/CLI11/LICENSE | 2 +- packages/CLI11/README.md | 425 +++- packages/CLI11/azure-pipelines.yml | 26 + packages/CLI11/cmake/AddGoogletest.cmake | 17 +- .../CLI11/cmake/CLI11ConfigVersion.cmake.in | 13 + packages/CLI11/conanfile.py | 11 +- packages/CLI11/examples/CMakeLists.txt | 92 +- packages/CLI11/examples/digit_args.cpp | 15 + packages/CLI11/examples/enum.cpp | 24 +- packages/CLI11/examples/option_groups.cpp | 38 + packages/CLI11/examples/positional_arity.cpp | 38 + .../CLI11/examples/positional_validation.cpp | 29 + packages/CLI11/examples/ranges.cpp | 33 + packages/CLI11/examples/shapes.cpp | 48 + .../CLI11/examples/subcom_partitioned.cpp | 37 + packages/CLI11/extern/json | 2 +- packages/CLI11/include/CLI/App.hpp | 2074 +++++++++++++---- packages/CLI11/include/CLI/Config.hpp | 7 +- packages/CLI11/include/CLI/ConfigFwd.hpp | 31 +- packages/CLI11/include/CLI/Error.hpp | 42 +- packages/CLI11/include/CLI/Formatter.hpp | 51 +- packages/CLI11/include/CLI/FormatterFwd.hpp | 7 +- packages/CLI11/include/CLI/Option.hpp | 442 +++- packages/CLI11/include/CLI/Optional.hpp | 8 +- packages/CLI11/include/CLI/Split.hpp | 45 +- packages/CLI11/include/CLI/StringTools.hpp | 211 +- packages/CLI11/include/CLI/Timer.hpp | 2 +- packages/CLI11/include/CLI/TypeTools.hpp | 229 +- packages/CLI11/include/CLI/Validators.hpp | 647 ++++- packages/CLI11/include/CLI/Version.hpp | 6 +- packages/CLI11/scripts/check_style_docker.sh | 13 + packages/CLI11/tests/AppTest.cpp | 1134 +++++++-- packages/CLI11/tests/CMakeLists.txt | 17 +- packages/CLI11/tests/CreationTest.cpp | 342 ++- packages/CLI11/tests/DeprecatedTest.cpp | 339 ++- packages/CLI11/tests/FormatterTest.cpp | 57 + packages/CLI11/tests/HelpTest.cpp | 174 +- packages/CLI11/tests/HelpersTest.cpp | 186 +- packages/CLI11/tests/IniTest.cpp | 191 +- packages/CLI11/tests/NewParseTest.cpp | 162 ++ packages/CLI11/tests/OptionGroupTest.cpp | 720 ++++++ packages/CLI11/tests/OptionalTest.cpp | 14 + packages/CLI11/tests/SetTest.cpp | 703 ++++++ packages/CLI11/tests/StringParseTest.cpp | 75 + packages/CLI11/tests/SubcommandTest.cpp | 722 +++++- packages/CLI11/tests/TransformTest.cpp | 433 ++++ packages/CLI11/tests/TrueFalseTest.cpp | 30 + packages/CLI11/tests/WindowsTest.cpp | 4 +- packages/CLI11/tests/app_helper.hpp | 5 +- 56 files changed, 8909 insertions(+), 1241 deletions(-) create mode 100644 packages/CLI11/.ci/azure-steps.yml create mode 100644 packages/CLI11/.editorconfig create mode 100644 packages/CLI11/azure-pipelines.yml create mode 100644 packages/CLI11/cmake/CLI11ConfigVersion.cmake.in create mode 100644 packages/CLI11/examples/digit_args.cpp create mode 100644 packages/CLI11/examples/option_groups.cpp create mode 100644 packages/CLI11/examples/positional_arity.cpp create mode 100644 packages/CLI11/examples/positional_validation.cpp create mode 100644 packages/CLI11/examples/ranges.cpp create mode 100644 packages/CLI11/examples/shapes.cpp create mode 100644 packages/CLI11/examples/subcom_partitioned.cpp create mode 100755 packages/CLI11/scripts/check_style_docker.sh create mode 100644 packages/CLI11/tests/OptionGroupTest.cpp create mode 100644 packages/CLI11/tests/SetTest.cpp create mode 100644 packages/CLI11/tests/StringParseTest.cpp create mode 100644 packages/CLI11/tests/TransformTest.cpp create mode 100644 packages/CLI11/tests/TrueFalseTest.cpp diff --git a/packages/CLI11/.ci/azure-steps.yml b/packages/CLI11/.ci/azure-steps.yml new file mode 100644 index 000000000..3ac2b35c4 --- /dev/null +++ b/packages/CLI11/.ci/azure-steps.yml @@ -0,0 +1,18 @@ +steps: + +- checkout: self + fetchDepth: 50 + submodules: true + +- task: CMake@1 + inputs: + cmakeArgs: .. -DCLI12_SINGLE_FILE=ON -DCLI11_CXX_STD=14 -DCLI11_SINGLE_FILE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug + displayName: 'Configure' + +- script: cmake --build . -j + displayName: 'Build' + workingDirectory: build + +- script: ctest --output-on-failure -C Debug + displayName: 'Test' + workingDirectory: build diff --git a/packages/CLI11/.ci/build_doxygen.sh b/packages/CLI11/.ci/build_doxygen.sh index 97dd4e1e2..5d139a720 100644 --- a/packages/CLI11/.ci/build_doxygen.sh +++ b/packages/CLI11/.ci/build_doxygen.sh @@ -3,7 +3,7 @@ set -evx -DOXYGEN_URL="ftp://ftp.stack.nl/pub/users/dimitri/doxygen-1.8.13.src.tar.gz" +DOXYGEN_URL="http://doxygen.nl/files/doxygen-1.8.15.src.tar.gz" cd "${DEPS_DIR}" if [[ ! -f "${DEPS_DIR}/doxygen/build/bin/doxygen" ]] ; then diff --git a/packages/CLI11/.editorconfig b/packages/CLI11/.editorconfig new file mode 100644 index 000000000..35463abd0 --- /dev/null +++ b/packages/CLI11/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +end_of_line = lf +trim_trailing_whitespace = true + diff --git a/packages/CLI11/.gitrepo b/packages/CLI11/.gitrepo index 7c4acccc9..71fccd41b 100644 --- a/packages/CLI11/.gitrepo +++ b/packages/CLI11/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:CLIUtils/CLI11.git branch = master - commit = bd4dc911847d0cde7a6b41dfa626a85aab213baf - parent = 5c82607003abda391c299d089e4624664a41de7a + commit = 76d2cde6568c9c8870b728aa9bc64b70b29127fd + parent = ead793c351e37e34227121e50abdce709aeb81af cmdver = 0.4.0 method = merge diff --git a/packages/CLI11/.travis.yml b/packages/CLI11/.travis.yml index 7e5b645ea..b3956ed80 100644 --- a/packages/CLI11/.travis.yml +++ b/packages/CLI11/.travis.yml @@ -1,5 +1,4 @@ language: cpp -sudo: false dist: trusty # Exclude ghpages, @@ -25,7 +24,7 @@ matrix: - .ci/make_and_test.sh 17 # Check style/tidy - - compiler: clang + - compiler: clang env: - CHECK_STYLE=yes script: @@ -105,6 +104,20 @@ matrix: conan upload "*" -c -r origin --all fi + # GCC 4.8 + - compiler: gcc + env: + - GCC_VER=4.8 + addons: + apt: + packages: + - g++-4.8 + install: + - export CC=gcc-4.8 + - export CXX=g++-4.8 + script: + - .ci/make_and_test.sh 11 + # macOS and clang - os: osx compiler: clang diff --git a/packages/CLI11/CHANGELOG.md b/packages/CLI11/CHANGELOG.md index 0b8ade8ea..c2e3e9a70 100644 --- a/packages/CLI11/CHANGELOG.md +++ b/packages/CLI11/CHANGELOG.md @@ -1,3 +1,98 @@ +## Version 1.8: Sets and Flags (IN PROGRESS) + +Set handling has been completely replaced by a new backend that works as a Validator. This provides a single interface instead of the 16 different functions in App. It also allows ordered collections to be used, custom functions for filtering, and better help and error messages. You can also use a collection of pairs (like `std::map`) to transform the match into an output. Also new are inverted flags, which can cancel or reduce the count of flags, and can also support general flag types. A new `add_option_fn` lets you more easily program CLI11 options with the types you choose. Vector options now support a custom separator. Apps can now be composed with unnamed subcommand support. + +* New `CLI::IsMember` validator replaces set validation [#222] +* IsMember also supports container of pairs, transform allows modification of result [#228] +* Much more powerful flags with different values [#211], general types [#235] +* `add_option` now supports bool due to unified bool handling [#211] +* Support for composable unnamed subcommands [#216] +* Custom vector separator using `->delimiter(char)` [#209], [#221], [#240] +* Validators added for IP4 addresses and positive numbers [#210] +* Minimum required Boost for optional Optionals has been corrected to 1.61 [#226] +* Positionals can stop options from being parsed with `app.positionals_at_end()` [#223] +* Validators can be negated with `!` [#230], and now handle tname functions [#228] +* Better enum support and streaming helper [#233] and [#228] +* Cleanup for shadow warnings [#232] + +> ### Converting from CLI11 1.7: +> +> * `app.add_set("--name", value, {"choice1", "choice2"})` should become `app.add_option("--name", value)->check(CLI::IsMember({"choice1", "choice2"}))` +> * The `_ignore_case` version of this can be replaced by adding `CLI::ignore_case` to the argument list in `IsMember` +> * The `_ignore_underscore` version of this can be replaced by adding `CLI::ignore_underscore` to the argument list in `IsMember` +> * The `_ignore_case_underscore` version of this can be replaced by adding both functions listed above to the argument list in `IsMember` +> * If you want an exact match to the original choice after one of the modifier functions matches, use `->transform` instead of `->check` +> * The `_mutable` versions of this can be replaced by passing a pointer or shared pointer into `IsMember` +> * An error with sets now produces a `ValidationError` instead of a `ConversionError` + +[#209]: https://github.com/CLIUtils/CLI11/pull/209 +[#210]: https://github.com/CLIUtils/CLI11/pull/210 +[#211]: https://github.com/CLIUtils/CLI11/pull/211 +[#216]: https://github.com/CLIUtils/CLI11/pull/216 +[#221]: https://github.com/CLIUtils/CLI11/pull/221 +[#222]: https://github.com/CLIUtils/CLI11/pull/222 +[#223]: https://github.com/CLIUtils/CLI11/pull/223 +[#226]: https://github.com/CLIUtils/CLI11/pull/226 +[#228]: https://github.com/CLIUtils/CLI11/pull/228 +[#230]: https://github.com/CLIUtils/CLI11/pull/230 +[#232]: https://github.com/CLIUtils/CLI11/pull/232 +[#233]: https://github.com/CLIUtils/CLI11/pull/233 +[#235]: https://github.com/CLIUtils/CLI11/pull/235 +[#240]: https://github.com/CLIUtils/CLI11/pull/240 + + +## Version 1.7.1: Quick patch + +This version provides a quick patch for a (correct) warning from GCC 8 for the windows options code. + + +* Fix for Windows style option parsing [#201] +* Improve `add_subcommand` when throwing an exception [#204] +* Better metadata for Conan package [#202] + +[#201]: https://github.com/CLIUtils/CLI11/pull/201 +[#202]: https://github.com/CLIUtils/CLI11/pull/202 +[#204]: https://github.com/CLIUtils/CLI11/pull/204 + +## Version 1.7: Parse breakup + +The parsing procedure now maps much more sensibly to complex, nested subcommand structures. Each phase of the parsing happens on all subcommands before moving on with the next phase of the parse. This allows several features, like required environment variables, to work properly even through subcommand boundaries. +Passing the same subcommand multiple times is better supported. Several new features were added as well, including Windows style option support, parsing strings directly, and ignoring underscores in names. Adding a set that you plan to change later must now be done with `add_mutable_set`. + +* Support Windows style options with `->allow_windows_style_options`. [#187] On by default on Windows. [#190] +* Added `parse(string)` to split up and parse a command-line style string directly. [#186] +* Added `ignore_underscore` and related functions, to ignore underscores when matching names. [#185] +* The default INI Config will now add quotes to strings with spaces [#195] +* The default message now will mention the help-all flag also if present [#197] +* Added `->description` to set Option descriptions [#199] +* Mutating sets (introduced in Version 1.6) now have a clear add method, `add_mutable_set*`, since the set reference should not expire [#200] +* Subcommands now track how many times they were parsed in a parsing process. `count()` with no arguments will return the number of times a subcommand was encountered. [#179] +* Parsing is now done in phases: `shortcurcuits`, `ini`, `env`, `callbacks`, and `requirements`; all subcommands complete a phase before moving on. [#179] +* Calling parse multiple times is now officially supported without `clear` (automatic). [#179] +* Dropped the mostly undocumented `short_circuit` property, as help flag parsing is a bit more complex, and the default callback behavior of options now works properly. [#179] +* Use the standard `BUILD_TESTING` over `CLI11_TESTING` if defined (`CLI11_TESTING` may eventually be removed) [#183] +* Cleanup warnings [#191] +* Remove deprecated names: `set_footer`, `set_name`, `set_callback`, and `set_type_name`. Use without the `set_` instead. [#192] + +> ### Converting from CLI11 1.6: +> +> * `->short_circuit()` is no longer needed, just remove it if you were using it - raising an exception will happen in the proper place now without it. +> * `->add_set*` becomes `->add_mutable_set*` if you were using the editable set feature +> * `footer`, `name`, `callback`, and `type_name` must be used instead of the `set_*` versions (deprecated previously). + +[#179]: https://github.com/CLIUtils/CLI11/pull/179 +[#183]: https://github.com/CLIUtils/CLI11/pull/183 +[#185]: https://github.com/CLIUtils/CLI11/pull/185 +[#186]: https://github.com/CLIUtils/CLI11/pull/186 +[#187]: https://github.com/CLIUtils/CLI11/pull/187 +[#190]: https://github.com/CLIUtils/CLI11/pull/190 +[#191]: https://github.com/CLIUtils/CLI11/pull/191 +[#192]: https://github.com/CLIUtils/CLI11/pull/192 +[#197]: https://github.com/CLIUtils/CLI11/pull/197 +[#195]: https://github.com/CLIUtils/CLI11/issues/195 +[#199]: https://github.com/CLIUtils/CLI11/pull/199 +[#200]: https://github.com/CLIUtils/CLI11/pull/200 + ## Version 1.6.2: Help-all This version fixes some formatting bugs with help-all. It also adds fixes for several warnings, including an experimental optional error on Clang 7. Several smaller fixes. @@ -96,7 +191,7 @@ Other changes: Backend and testing changes: -* Internally, `type_name` is now a lambda function; for sets, this reads the set live. [#116] +* Internally, `type_name` is now a lambda function; for sets, this reads the set live. [#116] * Cleaner tests without `app.reset()` (and `reset` is now `clear`). [#141] * Better CMake policy handling. [#110] * Includes are properly sorted. [#120] @@ -192,7 +287,7 @@ This version adds lots of smaller fixes and additions after the refactor in vers * Added `ExistingPath` validator [#73] * `app.allow_ini_extras()` added to allow extras in INI files [#70] * Multiline INI comments now supported -* Descriptions can now be written with `config_to_str` [#66] +* Descriptions can now be written with `config_to_str` [#66] * Double printing of error message fixed [#77] * Renamed `requires` to `needs` to avoid C++20 keyword [#75], [#82] * MakeSingleHeader now works if outside of git [#78] @@ -231,7 +326,7 @@ favorite CLI programs. Error messages and help messages are better and more flex * Footers can be added to help [#42](https://github.com/CLIUtils/CLI11/pull/42) * Help flags are easier to customize [#43](https://github.com/CLIUtils/CLI11/pull/43) * Subcommand now support groups [#46](https://github.com/CLIUtils/CLI11/pull/46) -* `CLI::RuntimeError` added, for easy exit with error codes [#45](https://github.com/CLIUtils/CLI11/pull/45) +* `CLI::RuntimeError` added, for easy exit with error codes [#45](https://github.com/CLIUtils/CLI11/pull/45) * The clang-format script is now no longer "hidden" [#48](https://github.com/CLIUtils/CLI11/pull/48) * The order is now preserved for subcommands (list and callbacks) [#49](https://github.com/CLIUtils/CLI11/pull/49) * Tests now run individually, utilizing CMake 3.10 additions if possible [#50](https://github.com/CLIUtils/CLI11/pull/50) @@ -248,7 +343,7 @@ favorite CLI programs. Error messages and help messages are better and more flex * Allow options to be disabled from INI file, rename `add_config` to `set_config` [#60](https://github.com/CLIUtils/CLI11/pull/60) > ### Converting from CLI11 1.2: -> +> > * `app.parse` no longer returns a vector. Instead, use `app.remaining(true)`. > * `"hidden"` is no longer a special group name, instead use `""` > * Validators API has changed to return an error string; use `.empty()` to get the old bool back @@ -273,8 +368,8 @@ This release focuses on making CLI11 behave properly in corner cases, and with c This release incorporates feedback from the release announcement. The examples are slowly being expanded, some corner cases improved, and some new functionality for tricky parsing situations. * Added simple support for enumerations, allow non-printable objects [#12](https://github.com/CLIUtils/CLI11/issues/12) -* Added `app.parse_order()` with original parse order ([#13](https://github.com/CLIUtils/CLI11/issues/13), [#16](https://github.com/CLIUtils/CLI11/pull/16)) -* Added `prefix_command()`, which is like `allow_extras` but instantly stops and returns. ([#8](https://github.com/CLIUtils/CLI11/issues/8), [#17](https://github.com/CLIUtils/CLI11/pull/17)) +* Added `app.parse_order()` with original parse order ([#13](https://github.com/CLIUtils/CLI11/issues/13), [#16](https://github.com/CLIUtils/CLI11/pull/16)) +* Added `prefix_command()`, which is like `allow_extras` but instantly stops and returns. ([#8](https://github.com/CLIUtils/CLI11/issues/8), [#17](https://github.com/CLIUtils/CLI11/pull/17)) * Removed Windows warning ([#10](https://github.com/CLIUtils/CLI11/issues/10), [#20](https://github.com/CLIUtils/CLI11/pull/20)) * Some improvements to CMake, detect Python and no dependencies on Python 2 (like Python 3) ([#18](https://github.com/CLIUtils/CLI11/issues/18), [#21](https://github.com/CLIUtils/CLI11/pull/21)) diff --git a/packages/CLI11/CMakeLists.txt b/packages/CLI11/CMakeLists.txt index 79404b8de..6dc6df558 100644 --- a/packages/CLI11/CMakeLists.txt +++ b/packages/CLI11/CMakeLists.txt @@ -2,12 +2,12 @@ cmake_minimum_required(VERSION 3.4) # Note: this is a header only library. If you have an older CMake than 3.4, # just add the CLI11/include directory and that's all you need to do. -# Make sure users don't get warnings on a tested (3.4 to 3.12) version +# Make sure users don't get warnings on a tested (3.4 to 3.13) version # of CMake. For most of the policies, the new version is better (hence the change). -if(${CMAKE_VERSION} VERSION_LESS 3.12) +if(${CMAKE_VERSION} VERSION_LESS 3.13) cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) else() - cmake_policy(VERSION 3.12) + cmake_policy(VERSION 3.13) endif() set(VERSION_REGEX "#define CLI11_VERSION[ \t]+\"(.+)\"") @@ -36,7 +36,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) if(MSVC) add_definitions("/W4") else() - add_definitions(-Wall -Wextra -pedantic) + add_definitions(-Wall -Wextra -pedantic -Wshadow) endif() if(CMAKE_VERSION VERSION_GREATER 3.6) @@ -96,12 +96,8 @@ endif() # import Targets.cmake # Add the version in a CMake readable way -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - CLI11ConfigVersion.cmake - VERSION ${CLI11_VERSION} - COMPATIBILITY AnyNewerVersion - ) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/CLI11ConfigVersion.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/CLI11ConfigVersion.cmake" @ONLY) # These installs only make sense for a local project if(CUR_PROJ) @@ -156,6 +152,10 @@ endif() cmake_dependent_option(CLI11_SINGLE_FILE_TESTS "Duplicate all the tests for a single file build" OFF "CLI11_SINGLE_FILE" OFF) cmake_dependent_option(CLI11_TESTING "Build the tests and add them" ON "CUR_PROJ" OFF) +if(DEFINED BUILD_TESTING) + cmake_dependent_option(CLI11_TESTING "" ON "BUILD_TESTING" OFF) + message(STATUS "BUILD_TESTING is defined and it supersedes CLI11_TESTING. Has forced to ${CLI11_TESTING}") +endif() if(CLI11_TESTING) enable_testing() add_subdirectory(tests) diff --git a/packages/CLI11/LICENSE b/packages/CLI11/LICENSE index 6b0077836..2221b918f 100644 --- a/packages/CLI11/LICENSE +++ b/packages/CLI11/LICENSE @@ -1,4 +1,4 @@ -CLI11 1.6 Copyright (c) 2017-2018 University of Cincinnati, developed by Henry +CLI11 1.7 Copyright (c) 2017-2019 University of Cincinnati, developed by Henry Schreiner under NSF AWARD 1414736. All rights reserved. Redistribution and use in source and binary forms of CLI11, with or without diff --git a/packages/CLI11/README.md b/packages/CLI11/README.md index c03b5e904..36eb4fffb 100644 --- a/packages/CLI11/README.md +++ b/packages/CLI11/README.md @@ -4,6 +4,7 @@ [![Build Status Linux and macOS][travis-badge]][travis] [![Build Status Windows][appveyor-badge]][appveyor] +[![Build Status Azure][azure-badge]][azure] [![Code Coverage][codecov-badge]][codecov] [![Codacy Badge][codacy-badge]][codacy-link] [![Join the chat at https://gitter.im/CLI11gitter/Lobby][gitter-badge]][gitter] @@ -11,11 +12,11 @@ [![Latest release][releases-badge]][github releases] [![DOI][doi-badge]][doi-link] [![Conan.io][conan-badge]][conan-link] -[![Try CLI11 1.6 online][wandbox-badge]][wandbox-link] +[![Try CLI11 1.7 online][wandbox-badge]][wandbox-link] +[What's new](./CHANGELOG.md) β’ [Documentation][gitbook] β’ -[API Reference][api-docs] β’ -[What's new](./CHANGELOG.md) +[API Reference][api-docs] CLI11 is a command line parser for C++11 and beyond that provides a rich feature set with a simple and intuitive interface. @@ -30,9 +31,18 @@ CLI11 is a command line parser for C++11 and beyond that provides a rich feature - [Usage](#usage) - [Adding options](#adding-options) - [Option types](#option-types) + - [Example](#example) - [Option options](#option-options) + - [Validators](#validators) π§ + - [Transforming Validators](#transforming-validators)π§ + - [Validator operations](#validator-operations)π§ + - [Custom Validators](#custom-validators)π§ + - [Querying Validators](#querying-validators)π§ + - [Getting Results](#getting-results) π§ - [Subcommands](#subcommands) - [Subcommand options](#subcommand-options) + - [Option groups](#option-groups) π§ + - [Callbacks](#callbacks) - [Configuration file](#configuration-file) - [Inheriting defaults](#inheriting-defaults) - [Formatting](#formatting) @@ -44,15 +54,17 @@ CLI11 is a command line parser for C++11 and beyond that provides a rich feature - [Contribute](#contribute) - [License](#license) +Features that were added in the last released major version are marked with "π". Features only available in master are marked with "π§". + ## Background ### Introduction CLI11 provides all the features you expect in a powerful command line parser, with a beautiful, minimal syntax and no dependencies beyond C++11. It is header only, and comes in a single file form for easy inclusion in projects. It is easy to use for small projects, but powerful enough for complex command line projects, and can be customized for frameworks. -It is tested on [Travis][] and [AppVeyor][], and is being included in the [GooFit GPU fitting framework][goofit]. It was inspired by [`plumbum.cli`][plumbum] for Python. CLI11 has a user friendly introduction in this README, a more in-depth tutorial [GitBook][], as well as [API documentation][api-docs] generated by Travis. +It is tested on [Travis][], [AppVeyor][], and [Azure][], and is being included in the [GooFit GPU fitting framework][goofit]. It was inspired by [`plumbum.cli`][plumbum] for Python. CLI11 has a user friendly introduction in this README, a more in-depth tutorial [GitBook][], as well as [API documentation][api-docs] generated by Travis. See the [changelog](./CHANGELOG.md) or [GitHub Releases][] for details for current and past releases. Also see the [Version 1.0 post][], [Version 1.3 post][], or [Version 1.6 post][] for more information. -You can be notified when new releases are made by subscribing to <https://github.com/CLIUtils/CLI11/releases.atom> on an RSS reader, like Feedly. +You can be notified when new releases are made by subscribing to <https://github.com/CLIUtils/CLI11/releases.atom> on an RSS reader, like Feedly, or use the releases mode of the github watching tool. ### Why write another CLI parser? @@ -60,18 +72,18 @@ An acceptable CLI parser library should be all of the following: - Easy to include (i.e., header only, one file if possible, **no external requirements**). - Short, simple syntax: This is one of the main reasons to use a CLI parser, it should make variables from the command line nearly as easy to define as any other variables. If most of your program is hidden in CLI parsing, this is a problem for readability. -- C++11 or better: Should work with GCC 4.7+ (such as GCC 4.8 on CentOS 7), Clang 3.5+, AppleClang 7+, NVCC 7.0+, or MSVC 2015+. +- C++11 or better: Should work with GCC 4.8+ (default on CentOS/RHEL 7), Clang 3.5+, AppleClang 7+, NVCC 7.0+, or MSVC 2015+. - Work on Linux, macOS, and Windows. -- Well tested using [Travis][] (Linux and macOS) and [AppVeyor][] (Windows). "Well" is defined as having good coverage measured by [CodeCov][]. +- Well tested using [Travis][] (Linux and macOS) and [AppVeyor][] (Windows) or [Azure][] (all three). "Well" is defined as having good coverage measured by [CodeCov][]. - Clear help printing. - Nice error messages. - Standard shell idioms supported naturally, like grouping flags, a positional separator, etc. - Easy to execute, with help, parse errors, etc. providing correct exit and details. - Easy to extend as part of a framework that provides "applications" to users. -- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, and optional fallthrough (explained later). +- Usable subcommand syntax, with support for multiple subcommands, nested subcommands, option groups, and optional fallthrough (explained later). - Ability to add a configuration file (`ini` format), and produce it as well. - Produce real values that can be used directly in code, not something you have pay compute time to look up, for HPC applications. -- Work with standard types, simple custom types, and extendible to exotic types. +- Work with standard types, simple custom types, and extensible to exotic types. - Permissively licensed. ### Other parsers @@ -92,7 +104,7 @@ After I wrote this, I also found the following libraries: | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [GFlags][] | The Google Commandline Flags library. Uses macros heavily, and is limited in scope, missing things like subcommands. It provides a simple syntax and supports config files/env vars. | | [GetOpt][] | Very limited C solution with long, convoluted syntax. Does not support much of anything, like help generation. Always available on UNIX, though (but in different flavors). | -| [ProgramOptions.hxx][] | Intresting library, less powerful and no subcommands. Nice callback system. | +| [ProgramOptions.hxx][] | Interesting library, less powerful and no subcommands. Nice callback system. | | [Args][] | Also interesting, and supports subcommands. I like the optional-like design, but CLI11 is cleaner and provides direct value access, and is less verbose. | | [Argument Aggregator][] | I'm a big fan of the [fmt][] library, and the try-catch statement looks familiar. :thumbsup: Doesn't seem to support subcommands. | | [Clara][] | Simple library built for the excellent [Catch][] testing framework. Unique syntax, limited scope. | @@ -113,7 +125,6 @@ There are some other possible "features" that are intentionally not supported by - Non-standard variations on syntax, like `-long` options. This is non-standard and should be avoided, so that is enforced by this library. - Completion of partial options, such as Python's `argparse` supplies for incomplete arguments. It's better not to guess. Most third party command line parsers for python actually reimplement command line parsing rather than using argparse because of this perceived design flaw. -- In C++14, you could have a set of `callback` methods with differing signatures (tested in a branch). Not deemed worth having a C++14 variation on API and removed. - Autocomplete: This might eventually be added to both Plumbum and CLI11, but it is not supported yet. - Wide strings / unicode: Since this uses the standard library only, it might be hard to properly implement, but I would be open to suggestions in how to do this. @@ -144,12 +155,15 @@ GTEST_COLOR=1 CTEST_OUTPUT_ON_FAILURE=1 make test To set up, add options, and run, your main function will look something like this: ```cpp -CLI::App app{"App description"}; +int main(int charc, char** argv) { + CLI::App app{"App description"}; -std::string filename = "default"; -app.add_option("-f,--file", filename, "A help string"); + std::string filename = "default"; + app.add_option("-f,--file", filename, "A help string"); -CLI11_PARSE(app, argc, argv); + CLI11_PARSE(app, argc, argv); + return 0; +} ``` <details><summary>Note: If you don't like macros, this is what that macro expands to: (click to expand)</summary><p> @@ -162,7 +176,7 @@ try { } ``` -The try/catch block ensures that `-h,--help` or a parse error will exit with the correct return code (selected from `CLI::ExitCodes`). (The return here should be inside `main`). You should not assume that the option values have been set inside the catch block; for example, help flags intentionally short-circuit all other processing for speed and to ensure required options and the like do not interfere. +The try/catch block ensures that `-h,--help` or a parse error will exit with the correct return code (selected from `CLI::ExitCodes`). (The return here should be inside `main`). You should not assume that the option values have been set inside the catch block; for example, help flags intentionally short-circuit all other processing for speed and to ensure required options and the like do not interfere. </p></details> </br> @@ -174,37 +188,80 @@ The initialization is just one line, adding options is just two each. The parse While all options internally are the same type, there are several ways to add an option depending on what you need. The supported values are: ```cpp +// Add options +app.add_option(option_name, help_str="") // π§ + app.add_option(option_name, - variable_to_bind_to, // int, float, vector, or string-like + variable_to_bind_to, // bool, int, float, vector, π§ enum, or string-like, or anything with a defined conversion from a string help_string="", default=false) +app.add_option_function<type>(option_name, + function <void(const type &value)>, // π§ int, bool, float, enum, vector, or string-like, or anything with a defined conversion from a string + help_string="") + app.add_complex(... // Special case: support for complex numbers +// Add flags app.add_flag(option_name, - int_or_bool = nothing, help_string="") -app.add_flag_function(option_name, - function <void(size_t count)>, +app.add_flag(option_name, + variable_to_bind_to, // bool, int, π§ float, π§ vector, π§ enum, or π§ string-like, or π§ anything with a defined conversion from a string help_string="") -app.add_set(option_name, - variable_to_bind_to, - set_of_possible_options, - help_string="", - default=false) +app.add_flag_function(option_name, // π§ + function <void(int64_t count)>, + help_string="") -app.add_set_ignore_case(... // String only +app.add_flag_callback(option_name,function<void(void)>,help_string="") // π§ +// Add subcommands App* subcom = app.add_subcommand(name, description); + +Option_group *app.add_option_group(name,description); // π§ + +// π§ All add_*set* methods deprecated in CLI11 1.8 - use ->transform(CLI::IsMember) instead +-app.add_set(option_name, +- variable_to_bind_to, // Same type as stored by set +- set_of_possible_options, // Set will be copied, ignores changes +- help_string="", +- default=false) +-app.add_mutable_set(... // π Set can change later, keeps reference +-app.add_set_ignore_case(... // String only +-app.add_mutable_set_ignore_case(... // π String only +-app.add_set_ignore_underscore(... // π String only +-app.add_mutable_set_ignore_underscore(... // π String only +-app.add_set_ignore_case_underscore(... // π String only +-app.add_mutable_set_ignore_case_underscore(... // π String only ``` -An option name must start with a alphabetic character or underscore. For long options, anything but an equals sign or a comma is valid after that. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option` or `add_set`. The set options allow your users to pick from a set of predefined options; you can add an existing set if you need to modify the set later, or you can use an initializer list. +An option name must start with a alphabetic character, underscore, a number π§, '?'π§, or '@'π§. For long options, after the first character '.', and '-' are also valid characters. For the `add_flag*` functions '{' has special meaning. Names are given as a comma separated string, with the dash or dashes. An option or flag can have as many names as you want, and afterward, using `count`, you can use any of the names, with dashes as needed, to count the options. One of the names is allowed to be given without proceeding dash(es); if present the option is a positional option, and that name will be used on the help line for its positional form. If you want the default value to print in the help description, pass in `true` for the final parameter for `add_option`. -On a C++14 compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure. +The `add_option_function<type>(...` function will typically require the template parameter be given unless a `std::function` object with an exact match is passed. The type can be any type supported by the `add_option` function. The function should throw an error (`CLI::ConversionError` or `CLI::ValidationError` possibly) if the value is not valid. -On a compiler that supports C++17's `__has_include`, you can also use `std::optional`, `std::experimental::optional`, and `boost::optional` directly in an `add_option` call. If you don't have `__has_include`, you can define `CLI11_BOOST_OPTIONAL 1` before including CLI11 to manually add support (or 0 to remove) for `boost::optional`. See [CLI11 Internals][] for information on how this was done and how you can add your own converters. +π§ Flag options specified through the `add_flag*` functions allow a syntax for the option names to default particular options to a false value or any other value if some flags are passed. For example: + +```cpp +app.add_flag("--flag,!--no-flag,result,"help for flag"); // π§ +``` + +specifies that if `--flag` is passed on the command line result will be true or contain a value of 1. If `--no-flag` is +passed `result` will contain false or -1 if `result` is a signed integer type, or 0 if it is an unsigned type. An +alternative form of the syntax is more explicit: `"--flag,--no-flag{false}"`; this is equivalent to the previous +example. This also works for short form options `"-f,!-n"` or `"-f,-n{false}"`. If `variable_to_bind_to` is anything but an integer value the +default behavior is to take the last value given, while if `variable_to_bind_to` is an integer type the behavior will be to sum +all the given arguments and return the result. This can be modified if needed by changing the `multi_option_policy` on each flag (this is not inherited). +The default value can be any value. For example if you wished to define a numerical flag +```cpp +app.add_flag("-1{1},-2{2},-3{3}",result,"numerical flag") // π§ +``` +using any of those flags on the command line will result in the specified number in the output. Similar things can be done for string values, and enumerations, as long as the default value can be converted to the given type. + + +On a `C++14` compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure. + +On a compiler that supports C++17's `__has_include`, you can also use `std::optional`, `std::experimental::optional`, and `boost::optional` directly in an `add_option` call. If you don't have `__has_include`, you can define `CLI11_BOOST_OPTIONAL 1` before including CLI11 to manually add support (or 0 to remove) for `boost::optional`. See [CLI11 Internals][] for information on how this was done and how you can add your own converters. Optional values are only supported for types that support the `>>` operator. #### Example @@ -212,14 +269,14 @@ On a compiler that supports C++17's `__has_include`, you can also use `std::opti - `"this"` Can only be passed positionally - `"-a,-b,-c"` No limit to the number of non-positional option names -The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used directly to check for the count (`->count()`) after parsing to avoid a string based lookup. +The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used directly to check for the count (`->count()`) after parsing to avoid a string based lookup. #### Option options Before parsing, you can set the following options: - `->required()`: The program will quit if this option is not present. This is `mandatory` in Plumbum, but required options seems to be a more standard term. For compatibility, `->mandatory()` also works. -- `->expected(N)`: Take `N` values instead of as many as possible, only for vector args. If negative, require at least `-N`; end with `--` or another recognized option. +- `->expected(N)`: Take `N` values instead of as many as possible, only for vector args. If negative, require at least `-N`; end with `--` or another recognized option or subcommand. - `->type_name(typename)`: Set the name of an Option's type (`type_name_fn` allows a function instead) - `->type_size(N)`: Set the intrinsic size of an option. The parser will require multiples of this number if negative. - `->needs(opt)`: This option requires another option to also be present, opt is an `Option` pointer. @@ -227,17 +284,20 @@ Before parsing, you can set the following options: - `->envname(name)`: Gets the value from the environment if present and not passed on the command line. - `->group(name)`: The help group to put the option in. No effect for positional options. Defaults to `"Options"`. `""` will not show up in the help print (hidden). - `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments). -- `->multi_option_policy(CLI::MultiOptionPolicy::Throw)`: Set the multi-option policy. Shortcuts available: `->take_last()`, `->take_first()`, and `->join()`. This will only affect options expecting 1 argument or bool flags (which always default to take last). -- `->check(CLI::ExistingFile)`: Requires that the file exists if given. -- `->check(CLI::ExistingDirectory)`: Requires that the directory exists. -- `->check(CLI::ExistingPath)`: Requires that the path (file or directory) exists. -- `->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. -- `->each(void(std::string)>`: Run this function on each value received, as it is received. +- `->ignore_underscore()`: π Ignore any underscores in the options names (also works on subcommands, does not affect arguments). For example "option_one" will match with "optionone". This does not apply to short form options since they only have one character +- `->disable_flag_override()`: π§ from the command line long form flag options can be assigned a value on the command line using the `=` notation `--flag=value`. If this behavior is not desired, the `disable_flag_override()` disables it and will generate an exception if it is done on the command line. The `=` does not work with short form flag options. +- `->delimiter(char)`: π§ allows specification of a custom delimiter for separating single arguments into vector arguments, for example specifying `->delimiter(',')` on an option would result in `--opt=1,2,3` producing 3 elements of a vector and the equivalent of --opt 1 2 3 assuming opt is a vector value. +- `->description(str)`: π Set/change the description. +- `->multi_option_policy(CLI::MultiOptionPolicy::Throw)`: Set the multi-option policy. Shortcuts available: `->take_last()`, `->take_first()`, and `->join()`. This will only affect options expecting 1 argument or bool flags (which do not inherit their default but always start with a specific policy). +- `->check(std::string(const std::string &), validator_name="",validator_description="")`: π§ define a check function. The function should return a non empty string with the error message if the check fails +- `->check(Validator)`:π§ Use a Validator object to do the check see [Validators](#validators) for a description of available Validators and how to create new ones. +- `->transform(std::string(std::string &), validator_name="",validator_description=")`: Converts the input string into the output string, in-place in the parsed options. +- `->transform(Validator)`: uses a Validator object to do the transformation see [Validators](#validators) for a description of available Validators and how to create new ones. +- `->each(void(const std::string &)>`: Run this function on each value received, as it is received. It should throw a `ValidationError` if an error is encountered. - `->configurable(false)`: Disable this option from being in a 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<std::string>` of results. Validate can also be a subclass of `CLI::Validator`, in which case it can also set the type name and can be combined with `&` and `|` (all built-in validators are this sort). + +These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. The `each` function 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 `each`, `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. Operations added through `transform` are executed first in reverse order of addition, and `check` and `each` are run following the transform functions in order of addition. If you just want to see the unconverted values, use `.results()` to get the `std::vector<std::string>` of results. On the command line, options can be given as: @@ -247,9 +307,21 @@ On the command line, options can be given as: - `-ffilename` (no space required) - `-abcf filename` (flags and option can be combined) - `--long` (long flag) +- `--long_flag=true` (long flag with equals to override default value) π§ - `--file filename` (space) - `--file=filename` (equals) +π If `allow_windows_style_options()` is specified in the application or subcommand options can also be given as: +- `/a` (flag) +- `/f filename` (option) +- `/long` (long flag) +- `/file filename` (space) +- `/file:filename` (colon) +- `/long_flag:false` (long flag with : to override the default value) π§ += Windows style options do not allow combining short options or values not separated from the short option like with `-` options + +π§ Long flag options may be given with an `=<value>` to allow specifying a false value, or some other value to the flag. See [config files](#configuration-file) for details on the values supported. NOTE: only the `=` or `:` for windows-style options may be used for this, using a space will result in the argument being interpreted as a positional argument. This syntax can override the default values, and can be disabled by using `disable_flag_override()`. + Extra positional arguments will cause the program to exit, so at least one positional option with a vector is recommended if you want to allow extraneous arguments. If you set `.allow_extras()` on the main `App`, you will not get an error. You can access the missing options using `remaining` (if you have subcommands, `app.remaining(true)` will get all remaining options, subcommands included). @@ -257,13 +329,121 @@ You can access a vector of pointers to the parsed options in the original order If `--` is present in the command line that does not end an unlimited option, then everything after that is positional only. +#### Validators +Validators are structures to check or modify inputs, they can be used to verify that an input meets certain criteria or transform it into another value. They are added through the `check` or `transform` functions. The differences between the two function are that checks do not modify the input whereas transforms can and are executed before any Validators added through `check`. + +CLI11 has several Validators built-in that perform some common checks + +- `CLI::IsMember(...)`: π§ Require an option be a member of a given set. See [Transforming Validators](#transforming-validators) for more details. +- `CLI::Transformer(...)`: π§ Modify the input using a map. See [Transforming Validators](#transforming-validators) for more details. +- `CLI::CheckedTransformer(...)`: π§ Modify the input using a map, and require that the input is either in the set or already one of the outputs of the set. See [Transforming Validators](#transforming-validators) for more details. +- `CLI::ExistingFile`: Requires that the file exists if given. +- `CLI::ExistingDirectory`: Requires that the directory exists. +- `CLI::ExistingPath`: Requires that the path (file or directory) exists. +- `CLI::NonexistentPath`: Requires that the path does not exist. +- `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. +- `CLI::Bounded(min,max)`: π§ Modify the input such that it is always between min and max (make sure to use floating point if needed). Min defaults to 0. Will produce an error if conversion is not possible. +- `CLI::PositiveNumber`: π§ Requires the number be greater or equal to 0 +- `CLI::Number`: π§ Requires the input be a number. +- `CLI::ValidIPV4`: π§ Requires that the option be a valid IPv4 string e.g. `'255.255.255.255'`, `'10.1.1.7'`. + +These Validators can be used by simply passing the name into the `check` or `transform` methods on an option +```cpp +->check(CLI::ExistingFile); +->check(CLI::Range(0,10)); +``` + +Validators can be merged using `&` and `|` and inverted using `!`π§. For example +```cpp +->check(CLI::Range(0,10)|CLI::Range(20,30)); +``` +will produce a check to ensure a value is between 0 and 10 or 20 and 30. +```cpp +->check(!CLI::PositiveNumber); +``` +will produce a check for a number less than 0; + +##### Transforming Validators +There are a few built in Validators that let you transform values if used with the `transform` function. If they also do some checks then they can be used `check` but some may do nothing in that case. + * π§ `CLI::Bounded(min,max)` will bound values between min and max and values outside of that range are limited to min or max, it will fail if the value cannot be converted and produce a `ValidationError` + * π§ The `IsMember` Validator lets you specify a set of predefined options. You can pass any container or copyable pointer (including `std::shared_ptr`) to a container to this validator; the container just needs to be iterable and have a `::value_type`. The key type should be convertible from a string, You can use an initializer list directly if you like. If you need to modify the set later, the pointer form lets you do that; the type message and check will correctly refer to the current version of the set. The container passed in can be a set, vector, or a map like structure. If used in the `transform` method the output value will be the matching key as it could be modified by filters. +After specifying a set of options, you can also specify "filter" functions of the form `T(T)`, where `T` is the type of the values. The most common choices probably will be `CLI::ignore_case` an `CLI::ignore_underscore`, and `CLI::ignore_space`. These all work on strings but it is possible to define functions that work on other types. +Here are some examples +of `IsMember`: + + * `CLI::IsMember({"choice1", "choice2"})`: Select from exact match to choices. + * `CLI::IsMember({"choice1", "choice2"}, CLI::ignore_case, CLI::ignore_underscore)`: Match things like `Choice_1`, too. + * `CLI::IsMember(std::set<int>({2,3,4}))`: Most containers and types work; you just need `std::begin`, `std::end`, and `::value_type`. + * `CLI::IsMember(std::map<std::string, TYPE>({{"one", 1}, {"two", 2}}))`: You can use maps; in `->transform()` these replace the matched value with the matched key. The value member of the map is not used in `IsMember`, so it can be any type. + * `auto p = std::make_shared<std::vector<std::string>>(std::initializer_list<std::string>("one", "two")); CLI::IsMember(p)`: You can modify `p` later. +* π§ The `Transformer` and `CheckedTransformer` Validators transform one value into another. Any container or copyable pointer (including `std::shared_ptr`) to a container that generates pairs of values can be passed to these `Validator's`; the container just needs to be iterable and have a `::value_type` that consists of pairs. The key type should be convertible from a string, and the value type should be convertible to a string You can use an initializer list directly if you like. If you need to modify the map later, the pointer form lets you do that; the description message will correctly refer to the current version of the map. `Transformer` does not do any checking so values not in the map are ignored. `CheckedTransformer` takes an extra step of verifying that the value is either one of the map key values, in which case it is transformed, or one of the expected output values, and if not will generate a `ValidationError`. A Transformer placed using `check` will not do anything. +After specifying a map of options, you can also specify "filter" just like in `CLI::IsMember`. +Here are some examples (`Transformer` and `CheckedTransformer` are interchangeable in the examples) +of `Transformer`: + + * `CLI::Transformer({{"key1", "map1"},{"key2","map2"}})`: Select from key values and produce map values. + + * `CLI::Transformer(std::map<std::string,int>({"two",2},{"three",3},{"four",4}}))`: most maplike containers work, the `::value_type` needs to produce a pair of some kind. + * `CLI::CheckedTransformer(std::map<std::string, int>({{"one", 1}, {"two", 2}}))`: You can use maps; in `->transform()` these replace the matched key with the value. `CheckedTransformer` also requires that the value either match one of the keys or match one of known outputs. + * `auto p = std::make_shared<CLI::TransformPairs<std::string>>(std::initializer_list<std::pair<std::string,std::string>>({"key1", "map1"},{"key2","map2"})); CLI::Transformer(p)`: You can modify `p` later. `TransformPairs<T>` is an alias for `std::vector<std::pair<<std::string,T>>` + +NOTES: If the container used in `IsMember`, `Transformer`, or `CheckedTransformer` has a `find` function like `std::unordered_map` or `std::map` then that function is used to do the searching. If it does not have a `find` function a linear search is performed. If there are filters present, the fast search is performed first, and if that fails a linear search with the filters on the key values is performed. + +##### Validator operationsπ§ +Validators are copyable and have a few operations that can be performed on them to alter settings. Most of the built in Validators have a default description that is displayed in the help. This can be altered via `.description(validator_description)`. +The name of a Validator, which is useful for later reference from the `get_validator(name)` method of an `Option` can be set via `.name(validator_name)` +The operation function of a Validator can be set via +`.operation(std::function<std::string(std::string &>)`. The `.active()` function can activate or deactivate a Validator from the operation. +All the functions return a Validator reference allowing them to be chained. For example + +```cpp +opt->check(CLI::Range(10,20).description("range is limited to sensible values").active(false).name("range")); +``` +will specify a check on an option with a name "range", but deactivate it for the time being. +The check can later be activated through +```cpp +opt->get_validator("range")->active(); +``` + +##### Custom Validatorsπ§ + +A validator object with a custom function can be created via +```cpp +CLI::Validator(std::function<std::string(std::string &)>,validator_description,validator_name=""); +``` +or if the operation function is set later they can be created with +```cpp +CLI::Validator(validator_description); +``` + + It is also possible to create a subclass of `CLI::Validator`, in which case it can also set a custom description function, and operation function. + +##### Querying Validators π§ +Once loaded into an Option, a pointer to a named Validator can be retrieved via +```cpp +opt->get_validator(name); +``` +This will retrieve a Validator with the given name or throw a `CLI::OptionNotFound` error. If no name is given or name is empty the first unnamed Validator will be returned or the first Validator if there is only one. + +Validators have a few functions to query the current values + * `get_description()`:π§ Will return a description string + * `get_name()`:π§ Will return the Validator name + * `get_active()`:π§ Will return the current active state, true if the Validator is active. + * `get_modifying()`: π§ Will return true if the Validator is allowed to modify the input, this can be controlled via the `non_modifying()`π§ method, though it is recommended to let `check` and `transform` option methods manipulate it if needed. + +#### Getting results +In most cases, the fastest and easiest way is to return the results through a callback or variable specified in one of the `add_*` functions. But there are situations where this is not possible or desired. For these cases the results may be obtained through one of the following functions. Please note that these functions will do any type conversions and processing during the call so should not used in performance critical code: + +- `results()`: Retrieves a vector of strings with all the results in the order they were given. +- `results(variable_to_bind_to)`: π§ Gets the results according to the MultiOptionPolicy and converts them just like the `add_option_function` with a variable. +- `Value=as<type>()`: π§ Returns the result or default value directly as the specified type if possible, can be vector to return all results, and a non-vector to get the result according to the MultiOptionPolicy in place. + ### Subcommands -Subcommands are supported, and can be nested infinitely. To add a subcommand, call the `add_subcommand` method with a name and an optional description. This gives a pointer to an `App` that behaves just like the main app, and can take options or further subcommands. Add `->ignore_case()` to a subcommand to allow any variation of caps to also be accepted. Children inherit the current setting from the parent. You cannot add multiple matching subcommand names at the same level (including ignore -case). +Subcommands are supported, and can be nested infinitely. To add a subcommand, call the `add_subcommand` method with a name and an optional description. This gives a pointer to an `App` that behaves just like the main app, and can take options or further subcommands. Add `->ignore_case()` to a subcommand to allow any variation of caps to also be accepted. π `->ignore_underscore()` is similar, but for underscores. Children inherit the current setting from the parent. You cannot add multiple matching subcommand names at the same level (including `ignore_case` and π `ignore_underscore`). If you want to require that at least one subcommand is given, use `.require_subcommand()` on the parent app. You can optionally give an exact number of subcommands to require, as well. If you give two arguments, that sets the min and max number allowed. -0 for the max number allowed will allow an unlimited number of subcommands. As a handy shortcut, a single negative value N will set "up to N" values. Limiting the maximimum number allows you to keep arguments that match a previous +0 for the max number allowed will allow an unlimited number of subcommands. As a handy shortcut, a single negative value N will set "up to N" values. Limiting the maximum number allows you to keep arguments that match a previous subcommand name from matching. If an `App` (main or subcommand) has been parsed on the command line, `->parsed` will be true (or convert directly to bool). @@ -272,38 +452,141 @@ All `App`s have a `get_subcommands()` method, which returns a list of pointers t For many cases, however, using an app's callback may be easier. Every app executes a callback function after it parses; just use a lambda function (with capture to get parsed values) to `.callback`. If you throw `CLI::Success` or `CLI::RuntimeError(return_value)`, you can even exit the program through the callback. The main `App` has a callback slot, as well, but it is generally not as useful. You are allowed to throw `CLI::Success` in the callbacks. -Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved). +Multiple subcommands are allowed, to allow [`Click`][click] like series of commands (order is preserved). The same subcommand can be triggered multiple times but all positional arguments will take precedence over the second and future calls of the subcommand. `->count()` on the subcommand will return the number of times the subcommand was called. The subcommand callback will only be triggered once unless the `.immediate_callback()` flag is set. In which case the callback executes on completion of the subcommand arguments but after the arguments for that subcommand have been parsed, and can be triggered multiple times. + +π§ Subcommands may also have an empty name either by calling `add_subcommand` with an empty string for the name or with no arguments. +Nameless subcommands function a similarly to groups in the main `App`. See [Option groups](#option-groups) to see how this might work. If an option is not defined in the main App, all nameless subcommands are checked as well. This allows for the options to be defined in a composable group. The `add_subcommand` function has an overload for adding a `shared_ptr<App>` so the subcommand(s) could be defined in different components and merged into a main `App`, or possibly multiple `Apps`. Multiple nameless subcommands are allowed. Callbacks for nameless subcommands are only triggered if any options from the subcommand were parsed. #### Subcommand options -There are several options that are supported on the main app and subcommands. These are: +There are several options that are supported on the main app and subcommands and option_groups. These are: - `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`. +- `.ignore_underscore()`: π Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`. +- `.allow_windows_style_options()`: π Allow command line options to be parsed in the form of `/s /long /file:file_name.ext` This option does not change how options are specified in the `add_option` calls or the ability to process options in the form of `-s --long --file=file_name.ext`. - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through. +- `.disable()`: π§ Specify that the subcommand is disabled, if given with a bool value it will enable or disable the subcommand or option group. +- `.disabled_by_default()`:π§ Specify that at the start of parsing the subcommand/option_group should be disabled. This is useful for allowing some Subcommands to trigger others. +- `.enabled_by_default()`: π§ Specify that at the start of each parse the subcommand/option_group should be enabled. This is useful for allowing some Subcommands to disable others. +- `.validate_positionals()`:π§ Specify that positionals should pass validation before matching. Validation is specified through `transform`, `check`, and `each` for an option. If an argument fails validation it is not an error and matching proceeds to the next available positional or extra arguments. +- `.excludes(option_or_subcommand)`: π§ If given an option pointer or pointer to another subcommand, these subcommands cannot be given together. In the case of options, if the option is passed the subcommand cannot be used and will generate an error. +- `.require_option()`: π§ Require 1 or more options or option groups be used. +- `.require_option(N)`: π§ Require `N` options or option groups, if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default to 0 or more. +- `.require_option(min, max)`: π§ Explicitly set min and max allowed options or option groups. Setting `max` to 0 implies unlimited options. - `.require_subcommand()`: Require 1 or more subcommands. -- `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more. +- `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default to 0 or more. - `.require_subcommand(min, max)`: Explicitly set min and max allowed subcommands. Setting `max` to 0 is unlimited. -- `.add_subcommand(name, description="")` Add a subcommand, returns a pointer to the internally stored subcommand. +- `.add_subcommand(name="", description="")`: Add a subcommand, returns a pointer to the internally stored subcommand. +- `.add_subcommand(shared_ptr<App>)`: π§ Add a subcommand by shared_ptr, returns a pointer to the internally stored subcommand. +- `.remove_subcommand(App)`:π§ Remove a subcommand from the app or subcommand. - `.got_subcommand(App_or_name)`: Check to see if a subcommand was received on the command line. -- `.get_subcommands(filter)`: The list of subcommands given on the command line. -- `.get_parent()`: Get the parent App or nullptr if called on master App. -- `.get_option(name)`: Get an option pointer by option name +- `.get_subcommands(filter)`: The list of subcommands that match a particular filter function. +- `.add_option_group(name="", description="")`: π§ Add an [option group](#option-groups) to an App, an option group is specialized subcommand intended for containing groups of options or other groups for controlling how options interact. +- `.get_parent()`: Get the parent App or `nullptr` if called on master App. +- `.get_option(name)`: Get an option pointer by option name will throw if the specified option is not available, nameless subcommands are also searched +- `.get_option_no_throw(name)`: π§ Get an option pointer by option name. This function will return a `nullptr` instead of throwing if the option is not available. - `.get_options(filter)`: Get the list of all defined option pointers (useful for processing the app for custom output formats). - `.parse_order()`: Get the list of option pointers in the order they were parsed (including duplicates). - `.formatter(fmt)`: Set a formatter, with signature `std::string(const App*, std::string, AppFormatMode)`. See Formatting for more details. +- `.description(str)`: π Set/change the description. - `.get_description()`: Access the description. - `.parsed()`: True if this subcommand was given on the command line. +- `.count()`: Returns the number of times the subcommand was called. +- `.count(option_name)`: Returns the number of times a particular option was called. +- `.count_all()`: π§ Returns the total number of arguments a particular subcommand processed, on the master App it returns the total number of processed commands. - `.name(name)`: Add or change the name. -- `.callback(void() function)`: Set the callback that runs at the end of parsing. The options have already run at this point. +- `.callback(void() function)`: Set the callback that runs at the end of parsing. The options have already run at this point. See [Subcommand callbacks](#callbacks) for some additional details. +- `.immediate_callback()`: π§ Specify that the callback for a subcommand should run immediately on completion of a subcommand vs at the completion of all parsing if this option is not used. +- `.pre_parse_callback(void(size_t) function)`: π§ Set a callback that executes after the first argument of an application is processed. See [Subcommand callbacks](#callbacks) for some additional details. - `.allow_extras()`: Do not throw an error if extra arguments are left over. -- `.prefix_command()`: Like `allow_extras`, but stop immediately on the first unrecognised item. It is ideal for allowing your app or subcommand to be a "prefix" to calling another app. +- `.positionals_at_end()`: π§ Specify that positional arguments occur as the last arguments and throw an error if an unexpected positional is encountered. +- `.prefix_command()`: Like `allow_extras`, but stop immediately on the first unrecognized item. It is ideal for allowing your app or subcommand to be a "prefix" to calling another app. - `.footer(message)`: Set text to appear at the bottom of the help string. - `.set_help_flag(name, message)`: Set the help flag name and message, returns a pointer to the created option. - `.set_help_all_flag(name, message)`: Set the help all flag name and message, returns a pointer to the created option. Expands subcommands. - `.failure_message(func)`: Set the failure message function. Two provided: `CLI::FailureMessage::help` and `CLI::FailureMessage::simple` (the default). - `.group(name)`: Set a group name, defaults to `"Subcommands"`. Setting `""` will be hide the subcommand. +- `[option_name]`: π§ retrieve a const pointer to an option given by `option_name` for Example `app["--flag1"]` will get a pointer to the option for the "--flag1" value, `app["--flag1"]->as<bool>()` will get the results of the command line for a flag. + +> Note: if you have a fixed number of required positional options, that will match before subcommand names. `{}` is an empty filter function, and any positional argument will match before repeated subcommand names. + + +#### Callbacks +A subcommand has two optional callbacks that are executed at different stages of processing. The `preparse_callback` π§ is executed once after the first argument of a subcommand or application is processed and gives an argument for the number of remaining arguments to process. For the main app the first argument is considered the program name, for subcommands the first argument is the subcommand name. For Option groups and nameless subcommands the first argument is after the first argument or subcommand is processed from that group. +The second callback is executed after parsing. The behavior depends on the status of the `immediate_callback` flag π§. If true, this runs immediately after the parsing of the subcommand. Or if the flag is false, once after parsing of all arguments. If the `immediate_callback` is set then the callback can be executed multiple times if the subcommand list given multiple times. If the main app or subcommand has a config file, no data from the config file will be reflected in immediate_callback. `immediate_callback()` has no effect on the main app, though it can be inherited. For option_groups `immediate_callback` causes the callback to be run prior to other option groups and options in the main app, effectively giving the options in the group priority. + +For example say an application was set up like + +```cpp +app.callback(ac); +sub1=app.add_subcommand("sub1")->callback(c1)->preparse_callback(pc1)->immediate_callback(); +sub2=app.add_subcommand("sub2")->callback(c2)->preparse_callback(pc2); +app.preparse_callback( pa); + +... A bunch of other options + +``` + +Then the command line is given as + +``` +program --opt1 opt1_val sub1 --sub1opt --sub1optb val sub2 --sub2opt sub1 --sub1opt2 sub2 --sub2opt2 val +``` + +* pa will be called prior to parsing any values with an argument of 13. +* pc1 will be called immediately after processing the sub1 command with a value of 10. +* c1 will be called when the `sub2` command is encountered. +* pc2 will be called with value of 6 after the sub2 command is encountered. +* c1 will be called again after the second sub2 command is encountered. +* c2 will be called once after processing all arguments. +* ac will be called after completing the parse and all lower level callbacks have been executed. + +A subcommand is considered terminated when one of the following conditions are met. +1. There are no more arguments to process +2. Another subcommand is encountered that would not fit in an optional slot of the subcommand +3. The positional_mark(`--`) is encountered and there are no available positional slots in the subcommand. +4. The subcommand_terminator mark(`++`) is encountered + +If the `immediate_callback` flag is set then all contained options are processed and the callback is triggered. If a subcommand with an `immediate_callback` flag is called again, then the contained options are reset, and can be triggered again. + + + +#### Option groups π§ + +The subcommand method + +```cpp +.add_option_group(name,description) +``` + +Will create an option group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range test](./tests/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through + +```cpp +ogroup->add_option(option_pointer); +ogroup->add_options(option_pointer); +ogroup->add_options(option1,option2,option3,...); +``` + +The option pointers used in this function must be options defined in the parent application of the option group otherwise an error will be generated. Subcommands can also be added via + +```cpp +ogroup->add_subcommand(subcom_pointer); +``` + +This results in the subcommand being moved from its parent into the option group. + +Options in an option group are searched for a command line match after any options in the main app, so any positionals in the main app would be matched first. So care must be taken to make sure of the order when using positional arguments and option groups. +Option groups work well with `excludes` and `require_options` methods, as an Application will treat an option group as a single option for the purpose of counting and requirements, and an option group will be considered used if any of the options or subcommands contained in it are used. Option groups allow specifying requirements such as requiring 1 of 3 options in one group and 1 of 3 options in a different group. Option groups can contain other groups as well. Disabling an option group will turn off all options within the group. + +The `CLI::TriggerOn`π§ and `CLI::TriggerOff`π§ methods are helper methods to allow the use of options/subcommands from one group to trigger another group on or off. + +```cpp +CLI::TriggerOn(group1_pointer, triggered_group); +CLI::TriggerOff(group2_pointer, disabled_group); +``` + +These functions make use of `preparse_callback`, `enabled_by_default()` and `disabled_by_default`. The triggered group may be a vector of group pointers. These methods should only be used once per group and will override any previous use of the underlying functions. More complex arrangements can be accomplished using similar methodology with a custom preparse_callback function that does more. -> Note: if you have a fixed number of required positional options, that will match before subcommand names. `{}` is an empty filter function. ### Configuration file @@ -317,7 +600,7 @@ app.set_config(option_name="", 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 by default (other formats can be added by an adept user). An example of a file: ```ini -; Commments are supported, using a ; +; Comments are supported, using a ; ; The default section is [default], case insensitive value = 1 @@ -331,16 +614,16 @@ in_subcommand = Wow sub.subcommand = true ``` -Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`; or `false`, `off`, `0`, `no` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not mean that subcommand was passed, it just sets the "defaults". You cannot set positional-only arguments or force subcommands to be present in the command line. +Spaces before and after the name and argument are ignored. Multiple arguments are separated by spaces. One set of quotes will be removed, preserving spaces (the same way the command line works). Boolean options can be `true`, `on`, `1`, `yes`, π§ `enable`; or `false`, `off`, `0`, `no`, π§ `disable` (case insensitive). Sections (and `.` separated names) are treated as subcommands (note: this does not mean that subcommand was passed, it just sets the "defaults". You cannot set positional-only arguments or force subcommands to be present in the command line. To print a configuration file from the passed arguments, use `.config_to_str(default_also=false, prefix="", write_description=false)`, where `default_also` will also show any defaulted arguments, `prefix` will add a prefix, and `write_description` will include option descriptions. ### Inheriting defaults -Many of the defaults for subcommands and even options are inherited from their creators. The inherited default values for subcommands are `allow_extras`, `prefix_command`, `ignore_case`, `fallthrough`, `group`, `footer`, and maximum number of required subcommands. The help flag existence, name, and description are inherited, as well. +Many of the defaults for subcommands and even options are inherited from their creators. The inherited default values for subcommands are `allow_extras`, `prefix_command`, `ignore_case`, π `ignore_underscore`, `fallthrough`, `group`, `footer`,`immediate_callback` and maximum number of required subcommands. The help flag existence, name, and description are inherited, as well. -Options have defaults for `group`, `required`, `multi_option_policy`, and `ignore_case`. To set these defaults, you should set the `option_defaults()` object, for example: +Options have defaults for `group`, `required`, `multi_option_policy`, `ignore_case`, π `ignore_underscore`, π§ `delimiter`, and π§ `disable_flag_override`. To set these defaults, you should set the `option_defaults()` object, for example: ```cpp app.option_defaults()->required(); @@ -351,7 +634,7 @@ The default settings for options are inherited to subcommands, as well. ### Formatting -The job of formatting help printouts is delegated to a formatter callable object on Apps and Options. You are free to replace either formatter by calling `formatter(fmt)` on an `App`, where fmt is any copyable callable with the correct signature. +The job of formatting help printouts is delegated to a formatter callable object on Apps and Options. You are free to replace either formatter by calling `formatter(fmt)` on an `App`, where fmt is any copyable callable with the correct signature. CLI11 comes with a default App formatter functional, `Formatter`. It is customizable; you can set `label(key, value)` to replace the default labels like `REQUIRED`, and `column_width(n)` to set the width of the columns before you add the functional to the app or option. You can also override almost any stage of the formatting process in a subclass of either formatter. If you want to make a new formatter from scratch, you can do that too; you just need to implement the correct signature. The first argument is a const pointer to the in question. The formatter will get a `std::string` usage name as the second option, and a `AppFormatMode` mode for the final option. It should return a `std::string`. @@ -364,30 +647,32 @@ The App class was designed allow toolkits to subclass it, to provide preset defa but before run behavior, while still giving the user freedom to `callback` on the main app. -The most important parse function is `parse(std::vector<std::string>)`, which takes a reversed list of arguments (so that `pop_back` processes the args in the correct order). `get_help_ptr` and `get_config_ptr` give you access to the help/config option pointers. The standard `parse` manually sets the name from the first argument, so it should not be in this vector. +The most important parse function is `parse(std::vector<std::string>)`, which takes a reversed list of arguments (so that `pop_back` processes the args in the correct order). `get_help_ptr` and `get_config_ptr` give you access to the help/config option pointers. The standard `parse` manually sets the name from the first argument, so it should not be in this vector. π You can also use `parse(string, bool)` to split up and parse a string; the optional bool should be set to true if you are +including the program name in the string, and false otherwise. Also, in a related note, the `App` you get a pointer to is stored in the parent `App` in a `unique_ptr`s (like `Option`s) and are deleted when the main `App` goes out of scope. ### How it works -Every `add_` option you have seen so far depends on one method that takes a lambda function. Each of these methods is just making a different lambda function with capture to populate the option. The function has full access to the vector of strings, so it knows how many times an option was passed or how many arguments it received (flags add empty strings to keep the counts correct). The lambda returns `true` if it could validate the option strings, and +Every `add_` option you have seen so far depends on one method that takes a lambda function. Each of these methods is just making a different lambda function with capture to populate the option. The function has full access to the vector of strings, so it knows how many times an option was passed or how many arguments it received. The lambda returns `true` if it could validate the option strings, and `false` if it failed. -Other values can be added as long as they support `operator>>` (and defaults can be printed if they support `operator<<`). To add an enum, for example, provide a custom `operator>>` with an `istream` (inside the CLI namespace is fine if you don't want to interfere with an existing `operator>>`). +Other values can be added as long as they support `operator>>` (and defaults can be printed if they support `operator<<`). To add a new type, for example, provide a custom `operator>>` with an `istream` (inside the CLI namespace is fine if you don't want to interfere with an existing `operator>>`). -If you wanted to extend this to support a completely new type, just use a lambda. An example of a new parser for `complex<double>` that supports all of the features of a standard `add_options` call is in [one of the tests](./tests/NewParseTest.cpp). A simpler example is shown below: +If you wanted to extend this to support a completely new type, use a lambda or add a specialization of the `lexical_cast` function template in the namespace `CLI::detail` with the type you need to convert to. Some examples of some new parsers for `complex<double>` that support all of the features of a standard `add_options` call are in [one of the tests](./tests/NewParseTest.cpp). A simpler example is shown below: #### Example ```cpp app.add_option("--fancy-count", [](std::vector<std::string> val){ - std::cout << "This option was given " << val.size() << " times." << std::endl + std::cout << "This option was given " << val.size() << " times." << std::endl; + return true; }); ``` ### Utilities -There are a few other utilities that are often useful in CLI programming. These are in separate headers, and do not appear in `CLI11.hpp`, but are completely independent and can be used as needed. The `Timer`/`AutoTimer` class allows you to easily time a block of code, with custom print output. +There are a few other utilities that are often useful in CLI programming. These are in separate headers, and do not appear in `CLI11.hpp`, but are completely independent and can be used as needed. The `Timer`/`AutoTimer` class allows you to easily time a block of code, with custom print output. ```cpp { @@ -460,6 +745,12 @@ Significant features and/or improvements to the code were contributed by: - [StΓ©phane Del Pino](https://github.com/delpinux) - [Mak Kolybabi](https://github.com/mogigoma) - [PaweΕ Bylica](https://github.com/chfast) +- [Philip Top](https://github.com/phlptp) <!-- Major features in 1.7 and 1.8 --> +- [almikhayl](https://github.com/almikhayl) +- [nurelin](https://github.com/nurelin) <!-- help_all in message --> +- [ncihneg](https://github.com/ncihneg) <!-- Quoting strings in INI generation --> +- [Fred HelmesjΓΆ](https://github.com/helmesjo) <!-- `->description()` --> +- [Rafi Wiener](https://github.com/rafiw) <!-- INI, +ive validators and vector separators --> ## License @@ -470,6 +761,8 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi [doi-badge]: https://zenodo.org/badge/80064252.svg [doi-link]: https://zenodo.org/badge/latestdoi/80064252 +[azure-badge]: https://dev.azure.com/CLIUtils/CLI11/_apis/build/status/CLIUtils.CLI11?branchName=master +[azure]: https://dev.azure.com/CLIUtils/CLI11/_build/|latest?definitionId=1&branchName=master [travis-badge]: https://img.shields.io/travis/CLIUtils/CLI11/master.svg?label=Linux/macOS [travis]: https://travis-ci.org/CLIUtils/CLI11 [appveyor-badge]: https://img.shields.io/appveyor/ci/HenrySchreiner/cli11/master.svg?label=Windows @@ -511,8 +804,8 @@ CLI11 was developed at the [University of Cincinnati][] to support of the [GooFi [version 1.0 post]: https://iscinumpy.gitlab.io/post/announcing-cli11-10/ [version 1.3 post]: https://iscinumpy.gitlab.io/post/announcing-cli11-13/ [version 1.6 post]: https://iscinumpy.gitlab.io/post/announcing-cli11-16/ -[wandbox-badge]: https://img.shields.io/badge/try-online-blue.svg -[wandbox-link]: https://wandbox.org/permlink/95DStlyHsquYH3k6 +[wandbox-badge]: https://img.shields.io/badge/try_1.7-online-blue.svg +[wandbox-link]: https://wandbox.org/permlink/k1BzlRwOZ07ieEuD [releases-badge]: https://img.shields.io/github/release/CLIUtils/CLI11.svg [cli11-po-compare]: https://iscinumpy.gitlab.io/post/comparing-cli11-and-boostpo/ [diana slides]: https://indico.cern.ch/event/619465/contributions/2507949/attachments/1448567/2232649/20170424-diana-2.pdf diff --git a/packages/CLI11/azure-pipelines.yml b/packages/CLI11/azure-pipelines.yml new file mode 100644 index 000000000..328bbc3e3 --- /dev/null +++ b/packages/CLI11/azure-pipelines.yml @@ -0,0 +1,26 @@ +# C/C++ with GCC +# Build your C/C++ project with GCC using make. +# Add steps that publish test results, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/apps/c-cpp/gcc + +trigger: +- master + +jobs: +- job: Linux + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: .ci/azure-steps.yml + +- job: macOS + pool: + vmImage: 'macOS-10.13' + steps: + - template: .ci/azure-steps.yml + +- job: Windows + pool: + vmImage: 'vs2017-win2016' + steps: + - template: .ci/azure-steps.yml diff --git a/packages/CLI11/cmake/AddGoogletest.cmake b/packages/CLI11/cmake/AddGoogletest.cmake index fb603e269..12eb621df 100644 --- a/packages/CLI11/cmake/AddGoogletest.cmake +++ b/packages/CLI11/cmake/AddGoogletest.cmake @@ -6,7 +6,10 @@ # set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) set(BUILD_SHARED_LIBS OFF) - +# older version of google tests doesn't support MSYS so needs this flag to compile +if (MSYS) + set(gtest_disable_pthreads ON CACHE BOOL "" FORCE) +endif() set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE BOOL "") add_subdirectory("${CLI11_SOURCE_DIR}/extern/googletest" "${CLI11_BINARY_DIR}/extern/googletest" EXCLUDE_FROM_ALL) @@ -56,9 +59,11 @@ BUILD_GTEST set_target_properties(gtest gtest_main gmock gmock_main PROPERTIES FOLDER "Extern") -if(MSVC AND MSVC_VERSION GREATER_EQUAL 1900) - target_compile_definitions(gtest PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) - target_compile_definitions(gtest_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) - target_compile_definitions(gmock PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) - target_compile_definitions(gmock_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) +if(MSVC) + if (MSVC_VERSION GREATER_EQUAL 1900) + target_compile_definitions(gtest PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gtest_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gmock PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + target_compile_definitions(gmock_main PUBLIC _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) + endif() endif() diff --git a/packages/CLI11/cmake/CLI11ConfigVersion.cmake.in b/packages/CLI11/cmake/CLI11ConfigVersion.cmake.in new file mode 100644 index 000000000..49faee579 --- /dev/null +++ b/packages/CLI11/cmake/CLI11ConfigVersion.cmake.in @@ -0,0 +1,13 @@ +# Adapted from write_basic_package_version_file(... COMPATIBILITY AnyNewerVersion) output +# ARCH_INDEPENDENT is only present in cmake 3.14 and onwards + +set(PACKAGE_VERSION "@VERSION_STRING@") + +if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() diff --git a/packages/CLI11/conanfile.py b/packages/CLI11/conanfile.py index efbaaca2e..13fc77aca 100644 --- a/packages/CLI11/conanfile.py +++ b/packages/CLI11/conanfile.py @@ -10,14 +10,17 @@ def get_version(): except Exception: return None -class HelloConan(ConanFile): +class CLI11Conan(ConanFile): name = "CLI11" version = get_version() - url = "https://github.com/CLIUtils/CLI11" - settings = "os", "compiler", "arch", "build_type" - license = "BSD 3 clause" description = "Command Line Interface toolkit for C++11" + topics = ("cli", "c++11", "parser", "cli11") + url = "https://github.com/CLIUtils/CLI11" + homepage = "https://github.com/CLIUtils/CLI11" + author = "Henry Schreiner <hschrein@cern.ch>" + license = "BSD-3-Clause" + settings = "os", "compiler", "arch", "build_type" exports_sources = "LICENSE", "README.md", "include/*", "extern/*", "cmake/*", "CMakeLists.txt", "tests/*" def build(self): # this is not building a library, just tests diff --git a/packages/CLI11/examples/CMakeLists.txt b/packages/CLI11/examples/CMakeLists.txt index 56a58e87c..c1731f67d 100644 --- a/packages/CLI11/examples/CMakeLists.txt +++ b/packages/CLI11/examples/CMakeLists.txt @@ -63,11 +63,92 @@ set_property(TEST subcommands_all PROPERTY PASS_REGULAR_EXPRESSION "Subcommand: start" "Subcommand: stop") +add_cli_exe(subcom_partitioned subcom_partitioned.cpp) +add_test(NAME subcom_partitioned_none COMMAND subcom_partitioned) +set_property(TEST subcom_partitioned_none PROPERTY PASS_REGULAR_EXPRESSION + "This is a timer:" + "--file is required" + "Run with --help for more information.") + +add_test(NAME subcom_partitioned_all COMMAND subcom_partitioned --file this --count --count -d 1.2) +set_property(TEST subcom_partitioned_all PROPERTY PASS_REGULAR_EXPRESSION + "This is a timer:" + "Working on file: this, direct count: 1, opt count: 1" + "Working on count: 2, direct count: 2, opt count: 2" + "Some value: 1.2") + # test shows that the help prints out for unnamed subcommands +add_test(NAME subcom_partitioned_help COMMAND subcom_partitioned --help) +set_property(TEST subcom_partitioned_help PROPERTY PASS_REGULAR_EXPRESSION + "-f,--file TEXT REQUIRED" + "-d,--double FLOAT") + +add_cli_exe(option_groups option_groups.cpp) +add_test(NAME option_groups_missing COMMAND option_groups ) +set_property(TEST option_groups_missing PROPERTY PASS_REGULAR_EXPRESSION + "Exactly 1 option from" + "is required") +add_test(NAME option_groups_extra COMMAND option_groups --csv --binary) +set_property(TEST option_groups_extra PROPERTY PASS_REGULAR_EXPRESSION + "and 2 were given") +add_test(NAME option_groups_extra2 COMMAND option_groups --csv --address "192.168.1.1" -o "test.out") +set_property(TEST option_groups_extra2 PROPERTY PASS_REGULAR_EXPRESSION + "at most 1") + +add_cli_exe(positional_arity positional_arity.cpp) +add_test(NAME positional_arity1 COMMAND positional_arity one ) +set_property(TEST positional_arity1 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one") +add_test(NAME positional_arity2 COMMAND positional_arity one two ) +set_property(TEST positional_arity2 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one" + "File 2 = two") +add_test(NAME positional_arity3 COMMAND positional_arity 1 2 one) +set_property(TEST positional_arity3 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one") +add_test(NAME positional_arity_fail COMMAND positional_arity 1 one two) +set_property(TEST positional_arity_fail PROPERTY PASS_REGULAR_EXPRESSION + "Could not convert") + + add_cli_exe(positional_validation positional_validation.cpp) +add_test(NAME positional_validation1 COMMAND positional_validation one ) +set_property(TEST positional_validation1 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one") +add_test(NAME positional_validation2 COMMAND positional_validation one 1 2 two ) +set_property(TEST positional_validation2 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one" + "File 2 = two") +add_test(NAME positional_validation3 COMMAND positional_validation 1 2 one) +set_property(TEST positional_validation3 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one") +add_test(NAME positional_validation4 COMMAND positional_validation 1 one two 2) +set_property(TEST positional_validation4 PROPERTY PASS_REGULAR_EXPRESSION + "File 1 = one" + "File 2 = two") + +add_cli_exe(shapes shapes.cpp) +add_test(NAME shapes_all COMMAND shapes circle 4.4 circle 10.7 rectangle 4 4 circle 2.3 triangle 4.5 ++ rectangle 2.1 ++ circle 234.675) +set_property(TEST shapes_all PROPERTY PASS_REGULAR_EXPRESSION + "circle2" + "circle4" + "rectangle2 with edges [2.1,2.1]" + "triangel1 with sides [4.5]") + +add_cli_exe(ranges ranges.cpp) +add_test(NAME ranges_range COMMAND ranges --range 1 2 3) +set_property(TEST ranges_range PROPERTY PASS_REGULAR_EXPRESSION + "[2:1:3]") +add_test(NAME ranges_minmax COMMAND ranges --min 2 --max 3) +set_property(TEST ranges_minmax PROPERTY PASS_REGULAR_EXPRESSION + "[2:1:3]") +add_test(NAME ranges_error COMMAND ranges --min 2 --max 3 --step 1 --range 1 2 3) +set_property(TEST ranges_error PROPERTY PASS_REGULAR_EXPRESSION + "Exactly 1 option from") + add_cli_exe(validators validators.cpp) add_test(NAME validators_help COMMAND validators --help) set_property(TEST validators_help PROPERTY PASS_REGULAR_EXPRESSION - " -f,--file FILE File name" - " -v,--value INT in [3 - 6] Value in range") + " -f,--file TEXT:FILE[\\r\\n\\t ]+File name" + " -v,--value INT:INT in [3 - 6][\\r\\n\\t ]+Value in range") add_test(NAME validators_file COMMAND validators --file nonex.xxx) set_property(TEST validators_file PROPERTY PASS_REGULAR_EXPRESSION "--file: File does not exist: nonex.xxx" @@ -112,7 +193,12 @@ add_cli_exe(enum enum.cpp) add_test(NAME enum_pass COMMAND enum -l 1) add_test(NAME enum_fail COMMAND enum -l 4) set_property(TEST enum_fail PROPERTY PASS_REGULAR_EXPRESSION - "Could not convert: --level = 4") + "--level: Check 4 value in {" "FAILED") + +add_cli_exe(digit_args digit_args.cpp) +add_test(NAME digit_args COMMAND digit_args -h) +set_property(TEST digit_args PROPERTY PASS_REGULAR_EXPRESSION + "-3{3}") add_cli_exe(modhelp modhelp.cpp) add_test(NAME modhelp COMMAND modhelp -a test -h) diff --git a/packages/CLI11/examples/digit_args.cpp b/packages/CLI11/examples/digit_args.cpp new file mode 100644 index 000000000..c2193c875 --- /dev/null +++ b/packages/CLI11/examples/digit_args.cpp @@ -0,0 +1,15 @@ +#include <CLI/CLI.hpp> +#include <iostream> + +int main(int argc, char **argv) { + CLI::App app; + + int val; + // add a set of flags with default values associate with them + app.add_flag("-1{1},-2{2},-3{3},-4{4},-5{5},-6{6}, -7{7}, -8{8}, -9{9}", val, "compression level"); + + CLI11_PARSE(app, argc, argv); + + std::cout << "value = " << val << std::endl; + return 0; +} diff --git a/packages/CLI11/examples/enum.cpp b/packages/CLI11/examples/enum.cpp index 5d404579a..6e0d69b27 100644 --- a/packages/CLI11/examples/enum.cpp +++ b/packages/CLI11/examples/enum.cpp @@ -1,25 +1,25 @@ #include <CLI/CLI.hpp> -#include <sstream> enum class Level : int { High, Medium, Low }; -std::istream &operator>>(std::istream &in, Level &level) { - int i; - in >> i; - level = static_cast<Level>(i); - return in; -} - -std::ostream &operator<<(std::ostream &in, const Level &level) { return in << static_cast<int>(level); } - int main(int argc, char **argv) { CLI::App app; Level level; - app.add_set("-l,--level", level, {Level::High, Level::Medium, Level::Low}, "Level settings") - ->type_name("enum/Level in {High=0, Medium=1, Low=2}"); + // specify string->value mappings + std::vector<std::pair<std::string, Level>> map{ + {"high", Level::High}, {"medium", Level::Medium}, {"low", Level::Low}}; + // checked Transform does the translation and checks the results are either in one of the strings or one of the + // translations already + app.add_option("-l,--level", level, "Level settings") + ->required() + ->transform(CLI::CheckedTransformer(map, CLI::ignore_case)); CLI11_PARSE(app, argc, argv); + // CLI11's built in enum streaming can be used outside CLI11 like this: + using namespace CLI::enums; + std::cout << "Enum received: " << level << std::endl; + return 0; } diff --git a/packages/CLI11/examples/option_groups.cpp b/packages/CLI11/examples/option_groups.cpp new file mode 100644 index 000000000..e3cf30139 --- /dev/null +++ b/packages/CLI11/examples/option_groups.cpp @@ -0,0 +1,38 @@ +#include "CLI/CLI.hpp" + +int main(int argc, char **argv) { + + CLI::App app("data output specification"); + app.set_help_all_flag("--help-all", "Expand all help"); + + auto format = app.add_option_group("output_format", "formatting type for output"); + auto target = app.add_option_group("output target", "target location for the output"); + bool csv = false; + bool human = false; + bool binary = false; + format->add_flag("--csv", csv, "specify the output in csv format"); + format->add_flag("--human", human, "specify the output in human readable text format"); + format->add_flag("--binary", binary, "specify the output in binary format"); + // require one of the options to be selected + format->require_option(1); + std::string fileLoc; + std::string networkAddress; + target->add_option("-o,--file", fileLoc, "specify the file location of the output"); + target->add_option("--address", networkAddress, "specify a network address to send the file"); + + // require at most one of the target options + target->require_option(0, 1); + CLI11_PARSE(app, argc, argv); + + std::string format_type = (csv) ? std::string("CSV") : ((human) ? "human readable" : "binary"); + std::cout << "Selected " << format_type << "format" << std::endl; + if(fileLoc.empty()) { + std::cout << " sent to file " << fileLoc << std::endl; + } else if(networkAddress.empty()) { + std::cout << " sent over network to " << networkAddress << std::endl; + } else { + std::cout << " sent to std::cout" << std::endl; + } + + return 0; +} diff --git a/packages/CLI11/examples/positional_arity.cpp b/packages/CLI11/examples/positional_arity.cpp new file mode 100644 index 000000000..0b390df25 --- /dev/null +++ b/packages/CLI11/examples/positional_arity.cpp @@ -0,0 +1,38 @@ +#include "CLI/CLI.hpp" + +int main(int argc, char **argv) { + + CLI::App app("test for positional arity"); + + auto numbers = app.add_option_group("numbers", "specify key numbers"); + auto files = app.add_option_group("files", "specify files"); + int num1 = -1, num2 = -1; + numbers->add_option("num1", num1, "first number"); + numbers->add_option("num2", num2, "second number"); + std::string file1, file2; + files->add_option("file1", file1, "first file")->required(); + files->add_option("file2", file2, "second file"); + // set a pre parse callback that turns the numbers group on or off depending on the number of arguments + app.preparse_callback([numbers](size_t arity) { + if(arity <= 2) { + numbers->disabled(); + } else { + numbers->disabled(false); + } + }); + + CLI11_PARSE(app, argc, argv); + + if(num1 != -1) + std::cout << "Num1 = " << num1 << '\n'; + + if(num2 != -1) + std::cout << "Num2 = " << num2 << '\n'; + + std::cout << "File 1 = " << file1 << '\n'; + if(!file2.empty()) { + std::cout << "File 2 = " << file2 << '\n'; + } + + return 0; +} diff --git a/packages/CLI11/examples/positional_validation.cpp b/packages/CLI11/examples/positional_validation.cpp new file mode 100644 index 000000000..63ff498c1 --- /dev/null +++ b/packages/CLI11/examples/positional_validation.cpp @@ -0,0 +1,29 @@ +#include "CLI/CLI.hpp" + +int main(int argc, char **argv) { + + CLI::App app("test for positional validation"); + + int num1 = -1, num2 = -1; + app.add_option("num1", num1, "first number")->check(CLI::Number); + app.add_option("num2", num2, "second number")->check(CLI::Number); + std::string file1, file2; + app.add_option("file1", file1, "first file")->required(); + app.add_option("file2", file2, "second file"); + app.validate_positionals(); + + CLI11_PARSE(app, argc, argv); + + if(num1 != -1) + std::cout << "Num1 = " << num1 << '\n'; + + if(num2 != -1) + std::cout << "Num2 = " << num2 << '\n'; + + std::cout << "File 1 = " << file1 << '\n'; + if(!file2.empty()) { + std::cout << "File 2 = " << file2 << '\n'; + } + + return 0; +} diff --git a/packages/CLI11/examples/ranges.cpp b/packages/CLI11/examples/ranges.cpp new file mode 100644 index 000000000..ace778fb8 --- /dev/null +++ b/packages/CLI11/examples/ranges.cpp @@ -0,0 +1,33 @@ +#include "CLI/CLI.hpp" + +int main(int argc, char **argv) { + + CLI::App app{"App to demonstrate exclusionary option groups."}; + + std::vector<int> range; + app.add_option("--range,-R", range, "A range")->expected(-2); + + auto ogroup = app.add_option_group("min_max_step", "set the min max and step"); + int min, max, step = 1; + ogroup->add_option("--min,-m", min, "The minimum")->required(); + ogroup->add_option("--max,-M", max, "The maximum")->required(); + ogroup->add_option("--step,-s", step, "The step", true); + + app.require_option(1); + + CLI11_PARSE(app, argc, argv); + + if(!range.empty()) { + if(range.size() == 2) { + min = range[0]; + max = range[1]; + } + if(range.size() >= 3) { + step = range[0]; + min = range[1]; + max = range[2]; + } + } + std::cout << "range is [" << min << ':' << step << ':' << max << "]\n"; + return 0; +} diff --git a/packages/CLI11/examples/shapes.cpp b/packages/CLI11/examples/shapes.cpp new file mode 100644 index 000000000..1fcf76961 --- /dev/null +++ b/packages/CLI11/examples/shapes.cpp @@ -0,0 +1,48 @@ +#include "CLI/CLI.hpp" + +int main(int argc, char **argv) { + + CLI::App app("load shapes"); + + app.set_help_all_flag("--help-all"); + auto circle = app.add_subcommand("circle", "draw a circle")->immediate_callback(); + double radius{0.0}; + int circle_counter = 0; + circle->callback([&radius, &circle_counter] { + ++circle_counter; + std::cout << "circle" << circle_counter << " with radius " << radius << std::endl; + }); + + circle->add_option("radius", radius, "the radius of the circle")->required(); + + auto rect = app.add_subcommand("rectangle", "draw a rectangle")->immediate_callback(); + double edge1{0.0}; + double edge2{0.0}; + int rect_counter = 0; + rect->callback([&edge1, &edge2, &rect_counter] { + ++rect_counter; + if(edge2 == 0) { + edge2 = edge1; + } + std::cout << "rectangle" << rect_counter << " with edges [" << edge1 << ',' << edge2 << "]" << std::endl; + edge2 = 0; + }); + + rect->add_option("edge1", edge1, "the first edge length of the rectangle")->required(); + rect->add_option("edge2", edge2, "the second edge length of the rectangle"); + + auto tri = app.add_subcommand("triangle", "draw a rectangle")->immediate_callback(); + std::vector<double> sides; + int tri_counter = 0; + tri->callback([&sides, &tri_counter] { + ++tri_counter; + + std::cout << "triangle" << tri_counter << " with sides [" << CLI::detail::join(sides) << "]" << std::endl; + }); + + tri->add_option("sides", sides, "the side lengths of the triangle"); + + CLI11_PARSE(app, argc, argv); + + return 0; +} diff --git a/packages/CLI11/examples/subcom_partitioned.cpp b/packages/CLI11/examples/subcom_partitioned.cpp new file mode 100644 index 000000000..d3afdc598 --- /dev/null +++ b/packages/CLI11/examples/subcom_partitioned.cpp @@ -0,0 +1,37 @@ +#include "CLI/CLI.hpp" +#include "CLI/Timer.hpp" + +int main(int argc, char **argv) { + CLI::AutoTimer("This is a timer"); + + CLI::App app("K3Pi goofit fitter"); + + CLI::App_p impOpt = std::make_shared<CLI::App>("Important"); + std::string file; + CLI::Option *opt = impOpt->add_option("-f,--file,file", file, "File name")->required(); + + int count; + CLI::Option *copt = impOpt->add_flag("-c,--count", count, "Counter")->required(); + + CLI::App_p otherOpt = std::make_shared<CLI::App>("Other"); + double value; // = 3.14; + otherOpt->add_option("-d,--double", value, "Some Value"); + + // add the subapps to the main one + app.add_subcommand(impOpt); + app.add_subcommand(otherOpt); + + try { + app.parse(argc, argv); + } catch(const CLI::ParseError &e) { + return app.exit(e); + } + + std::cout << "Working on file: " << file << ", direct count: " << impOpt->count("--file") + << ", opt count: " << opt->count() << std::endl; + std::cout << "Working on count: " << count << ", direct count: " << impOpt->count("--count") + << ", opt count: " << copt->count() << std::endl; + std::cout << "Some value: " << value << std::endl; + + return 0; +} diff --git a/packages/CLI11/extern/json b/packages/CLI11/extern/json index f1768a540..db53bdac1 160000 --- a/packages/CLI11/extern/json +++ b/packages/CLI11/extern/json @@ -1 +1 @@ -Subproject commit f1768a540a7b7c5cc30cdcd6be9e9ef91083719b +Subproject commit db53bdac1926d1baebcb459b685dcd2e4608c355 diff --git a/packages/CLI11/include/CLI/App.hpp b/packages/CLI11/include/CLI/App.hpp index 4b207e133..8443711d8 100644 --- a/packages/CLI11/include/CLI/App.hpp +++ b/packages/CLI11/include/CLI/App.hpp @@ -38,7 +38,7 @@ namespace CLI { #endif namespace detail { -enum class Classifer { NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND }; +enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS, SUBCOMMAND, SUBCOMMAND_TERMINATOR }; struct AppFriend; } // namespace detail @@ -49,8 +49,9 @@ std::string help(const App *app, const Error &e); class App; -using App_p = std::unique_ptr<App>; +using App_p = std::shared_ptr<App>; +class Option_group; /// Creates a command line program, with very few defaults. /** To use, create a new `Program()` instance with `argc`, `argv`, and a help description. The templated * add_option methods make it easy to prepare options. Remember to call `.start` before starting your @@ -77,9 +78,28 @@ class App { /// If true, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE bool allow_config_extras_{false}; - /// If true, return immediately on an unrecognised option (implies allow_extras) INHERITABLE + /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE bool prefix_command_{false}; + /// If set to true the name was automatically generated from the command line vs a user set name + bool has_automatic_name_{false}; + + /// If set to true the subcommand is required to be processed and used, ignored for main app + bool required_{false}; + + /// If set to true the subcommand is disabled and cannot be used, ignored for main app + bool disabled_{false}; + + /// Flag indicating that the pre_parse_callback has been triggered + bool pre_parse_called_{false}; + + /// Flag indicating that the callback for the subcommand should be executed immediately on parse completion which is + /// before help or ini files are processed. INHERITABLE + bool immediate_callback_{false}; + + /// This is a function that runs prior to the start of parsing + std::function<void(size_t)> pre_parse_callback_; + /// This is a function that runs when complete. Great for subcommands. Can throw. std::function<void()> callback_; @@ -116,7 +136,7 @@ class App { /// @name Parsing ///@{ - using missing_t = std::vector<std::pair<detail::Classifer, std::string>>; + using missing_t = std::vector<std::pair<detail::Classifier, std::string>>; /// Pair of classifier, string for missing options. (extra detail is removed on returning from parse) /// @@ -129,6 +149,13 @@ class App { /// This is a list of the subcommands collected, in order std::vector<App *> parsed_subcommands_; + /// this is a list of subcommands that are exclusionary to this one + std::set<App *> exclude_subcommands_; + + /// This is a list of options which are exclusionary to this App, if the options were used this subcommand should + /// not be + std::set<Option *> exclude_options_; + ///@} /// @name Subcommands ///@{ @@ -139,14 +166,34 @@ class App { /// If true, the program name is not case sensitive INHERITABLE bool ignore_case_{false}; + /// If true, the program should ignore underscores INHERITABLE + bool ignore_underscore_{false}; + /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE bool fallthrough_{false}; + /// Allow '/' for options for Windows like options. Defaults to true on Windows, false otherwise. INHERITABLE + bool allow_windows_style_options_{ +#ifdef _WIN32 + true +#else + false +#endif + }; + /// specify that positional arguments come at the end of the argument sequence not inheritable + bool positionals_at_end_{false}; + + /// If set to true the subcommand will start each parse disabled + bool disabled_by_default_{false}; + /// If set to true the subcommand will be reenabled at the start of each parse + bool enabled_by_default_{false}; + /// If set to true positional options are validated before assigning INHERITABLE + bool validate_positionals_{false}; /// A pointer to the parent if this is a subcommand App *parent_{nullptr}; - /// True if this command/subcommand was parsed - bool parsed_{false}; + /// Counts the number of times this command/subcommand was parsed + size_t parsed_ = 0; /// Minimum required subcommands (not inheritable!) size_t require_subcommand_min_ = 0; @@ -154,6 +201,12 @@ class App { /// Max number of subcommands allowed (parsing stops after this number). 0 is unlimited INHERITABLE size_t require_subcommand_max_ = 0; + /// Minimum required options (not inheritable!) + size_t require_option_min_ = 0; + + /// Max number of options allowed. 0 is unlimited (not inheritable) + size_t require_option_max_ = 0; + /// The group membership INHERITABLE std::string group_{"Subcommands"}; @@ -176,8 +229,8 @@ class App { ///@} /// Special private constructor for subcommand - App(std::string description_, std::string name, App *parent) - : name_(std::move(name)), description_(std::move(description_)), parent_(parent) { + App(std::string app_description, std::string app_name, App *parent) + : name_(std::move(app_name)), description_(std::move(app_description)), parent_(parent) { // Inherit if not from a nullptr if(parent_ != nullptr) { if(parent_->help_ptr_ != nullptr) @@ -194,8 +247,12 @@ class App { allow_extras_ = parent_->allow_extras_; allow_config_extras_ = parent_->allow_config_extras_; prefix_command_ = parent_->prefix_command_; + immediate_callback_ = parent_->immediate_callback_; ignore_case_ = parent_->ignore_case_; + ignore_underscore_ = parent_->ignore_underscore_; fallthrough_ = parent_->fallthrough_; + validate_positionals_ = parent_->validate_positionals_; + allow_windows_style_options_ = parent_->allow_windows_style_options_; group_ = parent_->group_; footer_ = parent_->footer_; formatter_ = parent_->formatter_; @@ -209,7 +266,8 @@ class App { ///@{ /// Create a new program. Pass in the same arguments as main(), along with a help string. - explicit App(std::string description_ = "", std::string name = "") : App(description_, name, nullptr) { + explicit App(std::string app_description = "", std::string app_name = "") + : App(app_description, app_name, nullptr) { set_help_flag("-h,--help", "Print this help message and exit"); } @@ -222,14 +280,22 @@ class App { /// it is not possible to overload on std::function (fixed in c++14 /// and backported to c++11 on newer compilers). Use capture by reference /// to get a pointer to App if needed. - App *callback(std::function<void()> callback) { - callback_ = callback; + App *callback(std::function<void()> app_callback) { + callback_ = std::move(app_callback); + return this; + } + + /// Set a callback to execute prior to parsing. + /// + App *preparse_callback(std::function<void(size_t)> pp_callback) { + pre_parse_callback_ = std::move(pp_callback); return this; } /// Set a name for the app (empty will use parser to set the name) - App *name(std::string name = "") { - name_ = name; + App *name(std::string app_name = "") { + name_ = app_name; + has_automatic_name_ = false; return this; } @@ -239,6 +305,43 @@ class App { return this; } + /// Remove the error when extras are left over on the command line. + App *required(bool require = true) { + required_ = require; + return this; + } + + /// Disable the subcommand or option group + App *disabled(bool disable = true) { + disabled_ = disable; + return this; + } + + /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled + App *disabled_by_default(bool disable = true) { + disabled_by_default_ = disable; + return this; + } + + /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not + /// disabled) + App *enabled_by_default(bool enable = true) { + enabled_by_default_ = enable; + return this; + } + + /// Set the subcommand callback to be executed immediately on subcommand completion + App *immediate_callback(bool immediate = true) { + immediate_callback_ = immediate; + return this; + } + + /// Set the subcommand to validate positional arguments before assigning + App *validate_positionals(bool validate = true) { + validate_positionals_ = validate; + return this; + } + /// Remove the error when extras are left over on the command line. /// Will also call App::allow_extras(). App *allow_config_extras(bool allow = true) { @@ -247,16 +350,40 @@ class App { return this; } - /// Do not parse anything after the first unrecognised option and return + /// Do not parse anything after the first unrecognized option and return App *prefix_command(bool allow = true) { prefix_command_ = allow; return this; } - /// Ignore case. Subcommand inherit value. + /// Ignore case. Subcommands inherit value. App *ignore_case(bool value = true) { ignore_case_ = value; - if(parent_ != nullptr) { + if(parent_ != nullptr && !name_.empty()) { + for(const auto &subc : parent_->subcommands_) { + if(subc.get() != this && (this->check_name(subc->name_) || subc->check_name(this->name_))) + throw OptionAlreadyAdded(subc->name_); + } + } + return this; + } + + /// Allow windows style options, such as `/opt`. First matching short or long name used. Subcommands inherit value. + App *allow_windows_style_options(bool value = true) { + allow_windows_style_options_ = value; + return this; + } + + /// Specify that the positional arguments are only at the end of the sequence + App *positionals_at_end(bool value = true) { + positionals_at_end_ = value; + return this; + } + + /// Ignore underscore. Subcommands inherit value. + App *ignore_underscore(bool value = true) { + ignore_underscore_ = value; + if(parent_ != nullptr && !name_.empty()) { for(const auto &subc : parent_->subcommands_) { if(subc.get() != this && (this->check_name(subc->name_) || subc->check_name(this->name_))) throw OptionAlreadyAdded(subc->name_); @@ -284,7 +411,7 @@ class App { } /// Check to see if this subcommand was parsed, true only if received on command line. - bool parsed() const { return parsed_; } + bool parsed() const { return parsed_ > 0; } /// Get the OptionDefault object, to set option defaults OptionDefaults *option_defaults() { return &option_defaults_; } @@ -307,15 +434,18 @@ class App { /// std::string filename; /// program.add_option("filename", filename, "description of filename"); /// - Option *add_option(std::string name, callback_t callback, std::string description = "", bool defaulted = false) { - Option myopt{name, description, callback, defaulted, this}; + Option *add_option(std::string option_name, + callback_t option_callback, + std::string option_description = "", + bool defaulted = false) { + Option myopt{option_name, option_description, option_callback, defaulted, this}; if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; }) == std::end(options_)) { options_.emplace_back(); Option_p &option = options_.back(); - option.reset(new Option(name, description, callback, defaulted, this)); + option.reset(new Option(option_name, option_description, option_callback, defaulted, this)); option_defaults_.copy_to(option.get()); return option.get(); } else @@ -323,29 +453,63 @@ class App { } /// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`) - template <typename T, enable_if_t<!is_vector<T>::value, detail::enabler> = detail::dummy> - Option *add_option(std::string name, + template <typename T, enable_if_t<!is_vector<T>::value & !std::is_const<T>::value, detail::enabler> = detail::dummy> + Option *add_option(std::string option_name, T &variable, ///< The variable to set - std::string description = "") { + std::string option_description = "") { CLI::callback_t fun = [&variable](CLI::results_t res) { return detail::lexical_cast(res[0], variable); }; - Option *opt = add_option(name, fun, description, false); + Option *opt = add_option(option_name, fun, option_description, false); opt->type_name(detail::type_name<T>()); return opt; } - /// Add option for non-vectors with a default print + /// Add option for a callback of a specific type template <typename T, enable_if_t<!is_vector<T>::value, detail::enabler> = detail::dummy> - Option *add_option(std::string name, + Option *add_option_function(std::string option_name, + const std::function<void(const T &)> &func, ///< the callback to execute + std::string option_description = "") { + + CLI::callback_t fun = [func](CLI::results_t res) { + T variable; + bool result = detail::lexical_cast(res[0], variable); + if(result) { + func(variable); + } + return result; + }; + + Option *opt = add_option(option_name, std::move(fun), option_description, false); + opt->type_name(detail::type_name<T>()); + return opt; + } + /// Add option with no description or variable assignment + Option *add_option(std::string option_name) { + return add_option(option_name, CLI::callback_t(), std::string{}, false); + } + + /// Add option with description but with no variable assignment or callback + template <typename T, + enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> = + detail::dummy> + Option *add_option(std::string option_name, T &option_description) { + return add_option(option_name, CLI::callback_t(), option_description, false); + } + + /// Add option for non-vectors with a default print, allow template to specify conversion type + template <typename T, + typename XC = T, + enable_if_t<!is_vector<XC>::value && !std::is_const<XC>::value, detail::enabler> = detail::dummy> + Option *add_option(std::string option_name, T &variable, ///< The variable to set - std::string description, + std::string option_description, bool defaulted) { + static_assert(std::is_constructible<T, XC>::value, "assign type must be assignable from conversion type"); + CLI::callback_t fun = [&variable](CLI::results_t res) { return detail::lexical_cast<XC>(res[0], variable); }; - CLI::callback_t fun = [&variable](CLI::results_t res) { return detail::lexical_cast(res[0], variable); }; - - Option *opt = add_option(name, fun, description, defaulted); - opt->type_name(detail::type_name<T>()); + Option *opt = add_option(option_name, fun, option_description, defaulted); + opt->type_name(detail::type_name<XC>()); if(defaulted) { std::stringstream out; out << variable; @@ -354,62 +518,92 @@ class App { return opt; } - /// Add option for vectors (no default) + /// Add option for vectors template <typename T> - Option *add_option(std::string name, + Option *add_option(std::string option_name, std::vector<T> &variable, ///< The variable vector to set - std::string description = "") { + std::string option_description = "") { CLI::callback_t fun = [&variable](CLI::results_t res) { bool retval = true; variable.clear(); - for(const auto &a : res) { + variable.reserve(res.size()); + for(const auto &elem : res) { + variable.emplace_back(); - retval &= detail::lexical_cast(a, variable.back()); + retval &= detail::lexical_cast(elem, variable.back()); } return (!variable.empty()) && retval; }; - Option *opt = add_option(name, fun, description, false); + Option *opt = add_option(option_name, fun, option_description, false); opt->type_name(detail::type_name<T>())->type_size(-1); return opt; } - /// Add option for vectors + /// Add option for vectors with defaulted argument template <typename T> - Option *add_option(std::string name, + Option *add_option(std::string option_name, std::vector<T> &variable, ///< The variable vector to set - std::string description, + std::string option_description, bool defaulted) { CLI::callback_t fun = [&variable](CLI::results_t res) { bool retval = true; variable.clear(); - for(const auto &a : res) { + variable.reserve(res.size()); + for(const auto &elem : res) { + variable.emplace_back(); - retval &= detail::lexical_cast(a, variable.back()); + retval &= detail::lexical_cast(elem, variable.back()); } return (!variable.empty()) && retval; }; - Option *opt = add_option(name, fun, description, defaulted); + Option *opt = add_option(option_name, fun, option_description, defaulted); opt->type_name(detail::type_name<T>())->type_size(-1); + if(defaulted) opt->default_str("[" + detail::join(variable) + "]"); return opt; } + /// Add option for a vector callback of a specific type + template <typename T, enable_if_t<is_vector<T>::value, detail::enabler> = detail::dummy> + Option *add_option_function(std::string option_name, + const std::function<void(const T &)> &func, ///< the callback to execute + std::string option_description = "") { + + CLI::callback_t fun = [func](CLI::results_t res) { + T values; + bool retval = true; + values.reserve(res.size()); + for(const auto &elem : res) { + values.emplace_back(); + retval &= detail::lexical_cast(elem, values.back()); + } + if(retval) { + func(values); + } + return retval; + }; + + Option *opt = add_option(option_name, std::move(fun), std::move(option_description), false); + opt->type_name(detail::type_name<T>())->type_size(-1); + return opt; + } + /// Set a help flag, replace the existing one if present - Option *set_help_flag(std::string name = "", std::string description = "") { + Option *set_help_flag(std::string flag_name = "", const std::string &help_description = "") { + // take flag_description by const reference otherwise add_flag tries to assign to help_description if(help_ptr_ != nullptr) { remove_option(help_ptr_); help_ptr_ = nullptr; } // Empty name will simply remove the help flag - if(!name.empty()) { - help_ptr_ = add_flag_function(name, [](size_t) -> void { throw CallForHelp(); }, description); - help_ptr_->short_circuit(true); + if(!flag_name.empty()) { + help_ptr_ = add_flag(flag_name, help_description); help_ptr_->configurable(false); } @@ -417,327 +611,380 @@ class App { } /// Set a help all flag, replaced the existing one if present - Option *set_help_all_flag(std::string name = "", std::string description = "") { + Option *set_help_all_flag(std::string help_name = "", const std::string &help_description = "") { + // take flag_description by const reference otherwise add_flag tries to assign to flag_description if(help_all_ptr_ != nullptr) { remove_option(help_all_ptr_); help_all_ptr_ = nullptr; } // Empty name will simply remove the help all flag - if(!name.empty()) { - help_all_ptr_ = add_flag_function(name, [](size_t) -> void { throw CallForAllHelp(); }, description); - help_all_ptr_->short_circuit(true); + if(!help_name.empty()) { + help_all_ptr_ = add_flag(help_name, help_description); help_all_ptr_->configurable(false); } return help_all_ptr_; } - /// Add option for flag - Option *add_flag(std::string name, std::string description = "") { - CLI::callback_t fun = [](CLI::results_t) { return true; }; + private: + /// Internal function for adding a flag + Option *_add_flag_internal(std::string flag_name, CLI::callback_t fun, std::string flag_description) { + Option *opt; + if(detail::has_default_flag_values(flag_name)) { + // check for default values and if it has them + auto flag_defaults = detail::get_default_flag_values(flag_name); + detail::remove_default_flag_values(flag_name); + opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false); + for(const auto &fname : flag_defaults) + opt->fnames_.push_back(fname.first); + opt->default_flag_values_ = std::move(flag_defaults); + } else { + opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false); + } + // flags cannot have positional values + if(opt->get_positional()) { + auto pos_name = opt->get_name(true); + remove_option(opt); + throw IncorrectConstruction::PositionalFlag(pos_name); + } - Option *opt = add_option(name, fun, description, false); - if(opt->get_positional()) - throw IncorrectConstruction::PositionalFlag(name); opt->type_size(0); return opt; } - /// Add option for flag integer + public: + /// Add a flag with no description or variable assignment + Option *add_flag(std::string flag_name) { return _add_flag_internal(flag_name, CLI::callback_t(), std::string{}); } + + /// Add flag with description but with no variable assignment or callback + /// takes a constant string, if a variable string is passed that variable will be assigned the results from the + /// flag + template <typename T, + enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> = + detail::dummy> + Option *add_flag(std::string flag_name, T &flag_description) { + return _add_flag_internal(flag_name, CLI::callback_t(), flag_description); + } + + /// Add option for flag with integer result - defaults to allowing multiple passings, but can be forced to one if + /// `multi_option_policy(CLI::MultiOptionPolicy::Throw)` is used. template <typename T, enable_if_t<std::is_integral<T>::value && !is_bool<T>::value, detail::enabler> = detail::dummy> - Option *add_flag(std::string name, - T &count, ///< A variable holding the count - std::string description = "") { - - count = 0; - CLI::callback_t fun = [&count](CLI::results_t res) { - count = static_cast<T>(res.size()); + Option *add_flag(std::string flag_name, + T &flag_count, ///< A variable holding the count + std::string flag_description = "") { + flag_count = 0; + CLI::callback_t fun = [&flag_count](CLI::results_t res) { + try { + detail::sum_flag_vector(res, flag_count); + } catch(const std::invalid_argument &) { + return false; + } return true; }; + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + } - Option *opt = add_option(name, fun, description, false); - if(opt->get_positional()) - throw IncorrectConstruction::PositionalFlag(name); - opt->type_size(0); + /// Other type version accepts all other types that are not vectors such as bool, enum, string or other classes that + /// can be converted from a string + template <typename T, + enable_if_t<!is_vector<T>::value && !std::is_const<T>::value && + (!std::is_integral<T>::value || is_bool<T>::value) && + !std::is_constructible<std::function<void(int)>, T>::value, + detail::enabler> = detail::dummy> + Option *add_flag(std::string flag_name, + T &flag_result, ///< A variable holding true if passed + std::string flag_description = "") { + + CLI::callback_t fun = [&flag_result](CLI::results_t res) { + if(res.size() != 1) { + return false; + } + return CLI::detail::lexical_cast(res[0], flag_result); + }; + Option *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + opt->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); return opt; } - /// Bool version - defaults to allowing multiple passings, but can be forced to one if - /// `multi_option_policy(CLI::MultiOptionPolicy::Throw)` is used. - template <typename T, enable_if_t<is_bool<T>::value, detail::enabler> = detail::dummy> - Option *add_flag(std::string name, - T &count, ///< A variable holding true if passed - std::string description = "") { - - count = false; - CLI::callback_t fun = [&count](CLI::results_t res) { - count = true; - return res.size() == 1; + /// Vector version to capture multiple flags. + template <typename T, + enable_if_t<!std::is_assignable<std::function<void(int64_t)>, T>::value, detail::enabler> = detail::dummy> + Option *add_flag(std::string flag_name, + std::vector<T> &flag_results, ///< A vector of values with the flag results + std::string flag_description = "") { + CLI::callback_t fun = [&flag_results](CLI::results_t res) { + bool retval = true; + for(const auto &elem : res) { + flag_results.emplace_back(); + retval &= detail::lexical_cast(elem, flag_results.back()); + } + return retval; }; + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); + } - Option *opt = add_option(name, fun, description, false); - if(opt->get_positional()) - throw IncorrectConstruction::PositionalFlag(name); - opt->type_size(0); + /// Add option for callback that is triggered with a true flag and takes no arguments + Option *add_flag_callback(std::string flag_name, + std::function<void(void)> function, ///< A function to call, void(void) + std::string flag_description = "") { + + CLI::callback_t fun = [function](CLI::results_t res) { + if(res.size() != 1) { + return false; + } + bool trigger; + auto result = CLI::detail::lexical_cast(res[0], trigger); + if(trigger) + function(); + return result; + }; + Option *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); opt->multi_option_policy(CLI::MultiOptionPolicy::TakeLast); return opt; } - /// Add option for callback - Option *add_flag_function(std::string name, - std::function<void(size_t)> function, ///< A function to call, void(size_t) - std::string description = "") { + /// Add option for callback with an integer value + Option *add_flag_function(std::string flag_name, + std::function<void(int64_t)> function, ///< A function to call, void(int) + std::string flag_description = "") { CLI::callback_t fun = [function](CLI::results_t res) { - auto count = static_cast<size_t>(res.size()); - function(count); + int64_t flag_count = 0; + detail::sum_flag_vector(res, flag_count); + function(flag_count); return true; }; - - Option *opt = add_option(name, fun, description, false); - if(opt->get_positional()) - throw IncorrectConstruction::PositionalFlag(name); - opt->type_size(0); - return opt; + return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description)); } #ifdef CLI11_CPP14 /// Add option for callback (C++14 or better only) - Option *add_flag(std::string name, - std::function<void(size_t)> function, ///< A function to call, void(size_t) - std::string description = "") { - return add_flag_function(name, function, description); + Option *add_flag(std::string flag_name, + std::function<void(int64_t)> function, ///< A function to call, void(int64_t) + std::string flag_description = "") { + return add_flag_function(std::move(flag_name), std::move(function), std::move(flag_description)); } #endif - /// Add set of options (No default, temp refernce, such as an inline set) + /// Add set of options (No default, temp reference, such as an inline set) DEPRECATED template <typename T> - Option *add_set(std::string name, - T &member, ///< The selected member of the set - const std::set<T> &&options, ///< The set of possibilities - std::string description = "") { - - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) { - bool retval = detail::lexical_cast(res[0], member); - if(!retval) - throw ConversionError(res[0], simple_name); - return std::find(std::begin(options), std::end(options), member) != std::end(options); - }; + Option *add_set(std::string option_name, + T &member, ///< The selected member of the set + std::set<T> options, ///< The set of possibilities + std::string option_description = "") { - Option *opt = add_option(name, fun, description, false); - std::string typeval = detail::type_name<T>(); - typeval += " in {" + detail::join(options) + "}"; - opt->type_name(typeval); + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->check(IsMember{options}); return opt; } - /// Add set of options (No default, non-temp refernce, such as an existing set) + /// Add set of options (No default, set can be changed afterwards - do not destroy the set) DEPRECATED template <typename T> - Option *add_set(std::string name, - T &member, ///< The selected member of the set - const std::set<T> &options, ///< The set of possibilities - std::string description = "") { - - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { - bool retval = detail::lexical_cast(res[0], member); - if(!retval) - throw ConversionError(res[0], simple_name); - return std::find(std::begin(options), std::end(options), member) != std::end(options); - }; - - Option *opt = add_option(name, fun, description, false); - opt->type_name_fn( - [&options]() { return std::string(detail::type_name<T>()) + " in {" + detail::join(options) + "}"; }); + Option *add_mutable_set(std::string option_name, + T &member, ///< The selected member of the set + const std::set<T> &options, ///< The set of possibilities + std::string option_description = "") { + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->check(IsMember{&options}); return opt; } - /// Add set of options (with default, R value, such as an inline set) + /// Add set of options (with default, static set, such as an inline set) DEPRECATED template <typename T> - Option *add_set(std::string name, - T &member, ///< The selected member of the set - const std::set<T> &&options, ///< The set of posibilities - std::string description, + Option *add_set(std::string option_name, + T &member, ///< The selected member of the set + std::set<T> options, ///< The set of possibilities + std::string option_description, bool defaulted) { - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) { - bool retval = detail::lexical_cast(res[0], member); - if(!retval) - throw ConversionError(res[0], simple_name); - return std::find(std::begin(options), std::end(options), member) != std::end(options); - }; - - Option *opt = add_option(name, fun, description, defaulted); - std::string typeval = detail::type_name<T>(); - typeval += " in {" + detail::join(options) + "}"; - opt->type_name(typeval); - if(defaulted) { - std::stringstream out; - out << member; - opt->default_str(out.str()); - } + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->check(IsMember{options}); return opt; } - /// Add set of options (with default, L value refernce, such as an existing set) + /// Add set of options (with default, set can be changed afterwards - do not destroy the set) DEPRECATED template <typename T> - Option *add_set(std::string name, - T &member, ///< The selected member of the set - const std::set<T> &options, ///< The set of posibilities - std::string description, - bool defaulted) { + Option *add_mutable_set(std::string option_name, + T &member, ///< The selected member of the set + const std::set<T> &options, ///< The set of possibilities + std::string option_description, + bool defaulted) { + + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->check(IsMember{&options}); + return opt; + } - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { - bool retval = detail::lexical_cast(res[0], member); - if(!retval) - throw ConversionError(res[0], simple_name); - return std::find(std::begin(options), std::end(options), member) != std::end(options); - }; + /// Add set of options, string only, ignore case (no default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_case)) instead") + Option *add_set_ignore_case(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description = "") { - Option *opt = add_option(name, fun, description, defaulted); - opt->type_name_fn( - [&options]() { return std::string(detail::type_name<T>()) + " in {" + detail::join(options) + "}"; }); - if(defaulted) { - std::stringstream out; - out << member; - opt->default_str(out.str()); - } + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{options, CLI::ignore_case}); return opt; } - /// Add set of options, string only, ignore case (no default, R value) - Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - const std::set<std::string> &&options, ///< The set of possibilities - std::string description = "") { + /// Add set of options, string only, ignore case (no default, set can be changed afterwards - do not destroy the + /// set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_case)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_case(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description = "") { - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) { - member = detail::to_lower(res[0]); - auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { - return detail::to_lower(val) == member; - }); - if(iter == std::end(options)) - throw ConversionError(member, simple_name); - else { - member = *iter; - return true; - } - }; + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{&options, CLI::ignore_case}); + return opt; + } - Option *opt = add_option(name, fun, description, false); - std::string typeval = detail::type_name<std::string>(); - typeval += " in {" + detail::join(options) + "}"; - opt->type_name(typeval); + /// Add set of options, string only, ignore case (default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_case)) instead") + Option *add_set_ignore_case(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description, + bool defaulted) { + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{options, CLI::ignore_case}); return opt; } - /// Add set of options, string only, ignore case (no default, L value) - Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - const std::set<std::string> &options, ///< The set of possibilities - std::string description = "") { + /// Add set of options, string only, ignore case (default, set can be changed afterwards - do not destroy the set) + /// DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(...)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_case(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description, + bool defaulted) { - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { - member = detail::to_lower(res[0]); - auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { - return detail::to_lower(val) == member; - }); - if(iter == std::end(options)) - throw ConversionError(member, simple_name); - else { - member = *iter; - return true; - } - }; + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{&options, CLI::ignore_case}); + return opt; + } - Option *opt = add_option(name, fun, description, false); - opt->type_name_fn([&options]() { - return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}"; - }); + /// Add set of options, string only, ignore underscore (no default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_underscore)) instead") + Option *add_set_ignore_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description = "") { + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{options, CLI::ignore_underscore}); return opt; } - /// Add set of options, string only, ignore case (default, R value) - Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - const std::set<std::string> &&options, ///< The set of posibilities - std::string description, - bool defaulted) { + /// Add set of options, string only, ignore underscore (no default, set can be changed afterwards - do not destroy + /// the set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_underscore)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description = "") { - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, options, simple_name](CLI::results_t res) { - member = detail::to_lower(res[0]); - auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { - return detail::to_lower(val) == member; - }); - if(iter == std::end(options)) - throw ConversionError(member, simple_name); - else { - member = *iter; - return true; - } - }; + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{options, CLI::ignore_underscore}); + return opt; + } - Option *opt = add_option(name, fun, description, defaulted); - std::string typeval = detail::type_name<std::string>(); - typeval += " in {" + detail::join(options) + "}"; - opt->type_name(typeval); - if(defaulted) { - opt->default_str(member); - } + /// Add set of options, string only, ignore underscore (default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_underscore)) instead") + Option *add_set_ignore_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description, + bool defaulted) { + + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{options, CLI::ignore_underscore}); return opt; } - /// Add set of options, string only, ignore case (default, L value) - Option *add_set_ignore_case(std::string name, - std::string &member, ///< The selected member of the set - const std::set<std::string> &options, ///< The set of posibilities - std::string description, - bool defaulted) { + /// Add set of options, string only, ignore underscore (default, set can be changed afterwards - do not destroy the + /// set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_underscore)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description, + bool defaulted) { - std::string simple_name = CLI::detail::split(name, ',').at(0); - CLI::callback_t fun = [&member, &options, simple_name](CLI::results_t res) { - member = detail::to_lower(res[0]); - auto iter = std::find_if(std::begin(options), std::end(options), [&member](std::string val) { - return detail::to_lower(val) == member; - }); - if(iter == std::end(options)) - throw ConversionError(member, simple_name); - else { - member = *iter; - return true; - } - }; + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{&options, CLI::ignore_underscore}); + return opt; + } - Option *opt = add_option(name, fun, description, defaulted); - opt->type_name_fn([&options]() { - return std::string(detail::type_name<std::string>()) + " in {" + detail::join(options) + "}"; - }); - if(defaulted) { - opt->default_str(member); - } + /// Add set of options, string only, ignore underscore and case (no default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_case, CLI::ignore_underscore)) instead") + Option *add_set_ignore_case_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description = "") { + + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{options, CLI::ignore_underscore, CLI::ignore_case}); + return opt; + } + + /// Add set of options, string only, ignore underscore and case (no default, set can be changed afterwards - do not + /// destroy the set) DEPRECATED + CLI11_DEPRECATED( + "Use ->transform(CLI::IsMember(..., CLI::ignore_case, CLI::ignore_underscore)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_case_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description = "") { + + Option *opt = add_option(option_name, member, std::move(option_description)); + opt->transform(IsMember{&options, CLI::ignore_underscore, CLI::ignore_case}); + return opt; + } + + /// Add set of options, string only, ignore underscore and case (default, static set) DEPRECATED + CLI11_DEPRECATED("Use ->transform(CLI::IsMember(..., CLI::ignore_case, CLI::ignore_underscore)) instead") + Option *add_set_ignore_case_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + std::set<std::string> options, ///< The set of possibilities + std::string option_description, + bool defaulted) { + + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{options, CLI::ignore_underscore, CLI::ignore_case}); + return opt; + } + + /// Add set of options, string only, ignore underscore and case (default, set can be changed afterwards - do not + /// destroy the set) DEPRECATED + CLI11_DEPRECATED( + "Use ->transform(CLI::IsMember(..., CLI::ignore_case, CLI::ignore_underscore)) with a (shared) pointer instead") + Option *add_mutable_set_ignore_case_underscore(std::string option_name, + std::string &member, ///< The selected member of the set + const std::set<std::string> &options, ///< The set of possibilities + std::string option_description, + bool defaulted) { + + Option *opt = add_option(option_name, member, std::move(option_description), defaulted); + opt->transform(IsMember{&options, CLI::ignore_underscore, CLI::ignore_case}); return opt; } /// Add a complex number template <typename T> - Option *add_complex(std::string name, + Option *add_complex(std::string option_name, T &variable, - std::string description = "", + std::string option_description = "", bool defaulted = false, std::string label = "COMPLEX") { - std::string simple_name = CLI::detail::split(name, ',').at(0); + std::string simple_name = CLI::detail::split(option_name, ',').at(0); CLI::callback_t fun = [&variable, simple_name, label](results_t res) { if(res[1].back() == 'i') res[1].pop_back(); @@ -748,7 +995,7 @@ class App { return worked; }; - CLI::Option *opt = add_option(name, fun, description, defaulted); + CLI::Option *opt = add_option(option_name, std::move(fun), std::move(option_description), defaulted); opt->type_name(label)->type_size(2); if(defaulted) { std::stringstream out; @@ -759,20 +1006,20 @@ class App { } /// Set a configuration ini file option, or clear it if no name passed - Option *set_config(std::string name = "", + Option *set_config(std::string option_name = "", std::string default_filename = "", - std::string help = "Read an ini file", - bool required = false) { + std::string help_message = "Read an ini file", + bool config_required = false) { // Remove existing config if present if(config_ptr_ != nullptr) remove_option(config_ptr_); // Only add config if option passed - if(!name.empty()) { + if(!option_name.empty()) { config_name_ = default_filename; - config_required_ = required; - config_ptr_ = add_option(name, config_name_, help, !default_filename.empty()); + config_required_ = config_required; + config_ptr_ = add_option(option_name, config_name_, help_message, !default_filename.empty()); config_ptr_->configurable(false); } @@ -787,6 +1034,11 @@ class App { op->remove_excludes(opt); } + if(help_ptr_ == opt) + help_ptr_ = nullptr; + if(help_all_ptr_ == opt) + help_all_ptr_ = nullptr; + auto iterator = std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; }); if(iterator != std::end(options_)) { @@ -796,22 +1048,61 @@ class App { return false; } + /// creates an option group as part of the given app + template <typename T = Option_group> + T *add_option_group(std::string group_name, std::string group_description = "") { + auto option_group = std::make_shared<T>(std::move(group_description), group_name, nullptr); + auto ptr = option_group.get(); + // move to App_p for overload resolution on older gcc versions + App_p app_ptr = std::dynamic_pointer_cast<App>(option_group); + add_subcommand(std::move(app_ptr)); + return ptr; + } + ///@} /// @name Subcommmands ///@{ /// Add a subcommand. Inherits INHERITABLE and OptionDefaults, and help flag - App *add_subcommand(std::string name, std::string description = "") { - subcommands_.emplace_back(new App(description, name, this)); - for(const auto &subc : subcommands_) - if(subc.get() != subcommands_.back().get()) - if(subc->check_name(subcommands_.back()->name_) || subcommands_.back()->check_name(subc->name_)) + App *add_subcommand(std::string subcommand_name = "", std::string subcommand_description = "") { + CLI::App_p subcom = std::shared_ptr<App>(new App(std::move(subcommand_description), subcommand_name, this)); + return add_subcommand(std::move(subcom)); + } + + /// Add a previously created app as a subcommand + App *add_subcommand(CLI::App_p subcom) { + if(!subcom) + throw IncorrectConstruction("passed App is not valid"); + if(!subcom->name_.empty()) { + for(const auto &subc : subcommands_) + if(subc->check_name(subcom->name_) || subcom->check_name(subc->name_)) throw OptionAlreadyAdded(subc->name_); + } + subcom->parent_ = this; + subcommands_.push_back(std::move(subcom)); return subcommands_.back().get(); } + /// Removes a subcommand from the App. Takes a subcommand pointer. Returns true if found and removed. + bool remove_subcommand(App *subcom) { + // Make sure no links exist + for(App_p &sub : subcommands_) { + sub->remove_excludes(subcom); + } + + auto iterator = std::find_if( + std::begin(subcommands_), std::end(subcommands_), [subcom](const App_p &v) { return v.get() == subcom; }); + if(iterator != std::end(subcommands_)) { + subcommands_.erase(iterator); + return true; + } + return false; + } /// Check to see if a subcommand is part of this command (doesn't have to be in command line) + /// returns the first subcommand if passed a nullptr App *get_subcommand(App *subcom) const { + if(subcom == nullptr) + throw OptionNotFound("nullptr passed"); for(const App_p &subcomptr : subcommands_) if(subcomptr.get() == subcom) return subcom; @@ -820,15 +1111,77 @@ class App { /// Check to see if a subcommand is part of this command (text version) App *get_subcommand(std::string subcom) const { + auto subc = _find_subcommand(subcom, false, false); + if(subc == nullptr) + throw OptionNotFound(subcom); + return subc; + } + /// Get a pointer to subcommand by index + App *get_subcommand(int index = 0) const { + if((index >= 0) && (index < static_cast<int>(subcommands_.size()))) + return subcommands_[index].get(); + throw OptionNotFound(std::to_string(index)); + } + + /// Check to see if a subcommand is part of this command and get a shared_ptr to it + CLI::App_p get_subcommand_ptr(App *subcom) const { + if(subcom == nullptr) + throw OptionNotFound("nullptr passed"); + for(const App_p &subcomptr : subcommands_) + if(subcomptr.get() == subcom) + return subcomptr; + throw OptionNotFound(subcom->get_name()); + } + + /// Check to see if a subcommand is part of this command (text version) + CLI::App_p get_subcommand_ptr(std::string subcom) const { for(const App_p &subcomptr : subcommands_) if(subcomptr->check_name(subcom)) - return subcomptr.get(); + return subcomptr; throw OptionNotFound(subcom); } + /// Get an owning pointer to subcommand by index + CLI::App_p get_subcommand_ptr(int index = 0) const { + if((index >= 0) && (index < static_cast<int>(subcommands_.size()))) + return subcommands_[index]; + throw OptionNotFound(std::to_string(index)); + } + + /// Check to see if an option group is part of this App + App *get_option_group(std::string group_name) const { + for(const App_p &app : subcommands_) { + if(app->name_.empty() && app->group_ == group_name) { + return app.get(); + } + } + throw OptionNotFound(group_name); + } + + /// No argument version of count counts the number of times this subcommand was + /// passed in. The main app will return 1. Unnamed subcommands will also return 1 unless + /// otherwise modified in a callback + size_t count() const { return parsed_; } + + /// Get a count of all the arguments processed in options and subcommands, this excludes arguments which were + /// treated as extras. + size_t count_all() const { + size_t cnt{0}; + for(auto &opt : options_) { + cnt += opt->count(); + } + for(auto &sub : subcommands_) { + cnt += sub->count_all(); + } + if(!get_name().empty()) { // for named subcommands add the number of times the subcommand was called + cnt += parsed_; + } + return cnt; + } + /// Changes the group membership - App *group(std::string name) { - group_ = name; + App *group(std::string group_name) { + group_ = group_name; return this; } @@ -861,6 +1214,35 @@ class App { return this; } + /// The argumentless form of require option requires 1 or more options be used + App *require_option() { + require_option_min_ = 1; + require_option_max_ = 0; + return this; + } + + /// Require an option to be given (does not affect help call) + /// The number required can be given. Negative values indicate maximum + /// number allowed (0 for any number). + App *require_option(int value) { + if(value < 0) { + require_option_min_ = 0; + require_option_max_ = static_cast<size_t>(-value); + } else { + require_option_min_ = static_cast<size_t>(value); + require_option_max_ = static_cast<size_t>(value); + } + return this; + } + + /// Explicitly control the number of options required. Setting 0 + /// for the max means unlimited number allowed. Max number inheritable. + App *require_option(size_t min, size_t max) { + require_option_min_ = min; + require_option_max_ = max; + return this; + } + /// Stop subcommand fallthrough, so that parent commands cannot collect commands after subcommand. /// Default from parent, usually set on parent. App *fallthrough(bool value = true) { @@ -870,7 +1252,7 @@ class App { /// Check to see if this subcommand was parsed, true only if received on command line. /// This allows the subcommand to be directly checked. - operator bool() const { return parsed_; } + operator bool() const { return parsed_ > 0; } ///@} /// @name Extras for subclassing @@ -888,24 +1270,27 @@ class App { /// Reset the parsed data void clear() { - parsed_ = false; + parsed_ = 0; + pre_parse_called_ = false; + missing_.clear(); parsed_subcommands_.clear(); - for(const Option_p &opt : options_) { opt->clear(); } - for(const App_p &app : subcommands_) { - app->clear(); + for(const App_p &subc : subcommands_) { + subc->clear(); } } - /// Parses the command line - throws errors + /// Parses the command line - throws errors. /// This must be called after the options are in but before the rest of the program. void parse(int argc, const char *const *argv) { // If the name is not set, read from command line - if(name_.empty()) + if((name_.empty()) || (has_automatic_name_)) { + has_automatic_name_ = true; name_ = argv[0]; + } std::vector<std::string> args; for(int i = argc - 1; i > 0; i--) @@ -913,19 +1298,53 @@ class App { parse(args); } + /// Parse a single string as if it contained command line arguments. + /// This function splits the string into arguments then calls parse(std::vector<std::string> &) + /// the function takes an optional boolean argument specifying if the programName is included in the string to + /// process + void parse(std::string commandline, bool program_name_included = false) { + + if(program_name_included) { + auto nstr = detail::split_program_name(commandline); + if((name_.empty()) || (has_automatic_name_)) { + has_automatic_name_ = true; + name_ = nstr.first; + } + commandline = std::move(nstr.second); + } else + detail::trim(commandline); + // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations + if(!commandline.empty()) { + commandline = detail::find_and_modify(commandline, "=", detail::escape_detect); + if(allow_windows_style_options_) + commandline = detail::find_and_modify(commandline, ":", detail::escape_detect); + } + + auto args = detail::split_up(std::move(commandline)); + // remove all empty strings + args.erase(std::remove(args.begin(), args.end(), std::string{}), args.end()); + std::reverse(args.begin(), args.end()); + + parse(args); + } + /// The real work is done here. Expects a reversed vector. /// Changes the vector to the remaining options. void parse(std::vector<std::string> &args) { // Clear if parsed - if(parsed_) + if(parsed_ > 0) clear(); - // Redundant (set by _parse on commands/subcommands) + // parsed_ is incremented in commands/subcommands, // but placed here to make sure this is cleared when - // running parse after an error is thrown, even by _validate. - parsed_ = true; - + // running parse after an error is thrown, even by _validate or _configure. + parsed_ = 1; _validate(); + _configure(); + // set the parent as nullptr as this object should be the top now + parent_ = nullptr; + parsed_ = 0; + _parse(args); run_callback(); } @@ -965,16 +1384,9 @@ class App { ///@{ /// Counts the number of times the given option was passed. - size_t count(std::string name) const { - for(const Option_p &opt : options_) { - if(opt->check_name(name)) { - return opt->count(); - } - } - throw OptionNotFound(name); - } + size_t count(std::string option_name) const { return get_option(option_name)->count(); } - /// Get a subcommand pointer list to the currently selected subcommands (after parsing by by default, in command + /// Get a subcommand pointer list to the currently selected subcommands (after parsing by default, in command /// line order; use parsed = false to get the original definition list.) std::vector<App *> get_subcommands() const { return parsed_subcommands_; } @@ -1016,24 +1428,70 @@ class App { /// Check to see if given subcommand was selected bool got_subcommand(App *subcom) const { // get subcom needed to verify that this was a real subcommand - return get_subcommand(subcom)->parsed_; + return get_subcommand(subcom)->parsed_ > 0; } /// Check with name instead of pointer to see if subcommand was selected - bool got_subcommand(std::string name) const { return get_subcommand(name)->parsed_; } + bool got_subcommand(std::string subcommand_name) const { return get_subcommand(subcommand_name)->parsed_ > 0; } - ///@} - /// @name Help - ///@{ + /// Sets excluded options for the subcommand + App *excludes(Option *opt) { + if(opt == nullptr) { + throw OptionNotFound("nullptr passed"); + } + exclude_options_.insert(opt); + return this; + } - /// Set footer. - App *footer(std::string footer) { - footer_ = footer; + /// Sets excluded subcommands for the subcommand + App *excludes(App *app) { + if((app == this) || (app == nullptr)) { + throw OptionNotFound("nullptr passed"); + } + auto res = exclude_subcommands_.insert(app); + // subcommand exclusion should be symmetric + if(res.second) { + app->exclude_subcommands_.insert(this); + } return this; } - /// Produce a string that could be read in as a config of the current values of the App. Set default_also to include - /// default arguments. Prefix will add a string to the beginning of each option. + /// Removes an option from the excludes list of this subcommand + bool remove_excludes(Option *opt) { + auto iterator = std::find(std::begin(exclude_options_), std::end(exclude_options_), opt); + if(iterator != std::end(exclude_options_)) { + exclude_options_.erase(iterator); + return true; + } else { + return false; + } + } + + /// Removes a subcommand from this excludes list of this subcommand + bool remove_excludes(App *app) { + auto iterator = std::find(std::begin(exclude_subcommands_), std::end(exclude_subcommands_), app); + if(iterator != std::end(exclude_subcommands_)) { + auto other_app = *iterator; + exclude_subcommands_.erase(iterator); + other_app->remove_excludes(this); + return true; + } else { + return false; + } + } + + ///@} + /// @name Help + ///@{ + + /// Set footer. + App *footer(std::string footer_string) { + footer_ = std::move(footer_string); + return this; + } + + /// Produce a string that could be read in as a config of the current values of the App. Set default_also to + /// include default arguments. Prefix will add a string to the beginning of each option. std::string config_to_str(bool default_also = false, bool write_description = false) const { return config_formatter_->to_config(this, default_also, write_description, ""); } @@ -1054,18 +1512,6 @@ class App { return formatter_->make_help(this, prev, mode); } - /// Provided for backwards compatibility \deprecated - CLI11_DEPRECATED("Please use footer instead") - App *set_footer(std::string msg) { return footer(msg); } - - /// Provided for backwards compatibility \deprecated - CLI11_DEPRECATED("Please use name instead") - App *set_name(std::string msg) { return name(msg); } - - /// Provided for backwards compatibility \deprecated - CLI11_DEPRECATED("Please use callback instead") - App *set_callback(std::function<void()> fn) { return callback(fn); } - ///@} /// @name Getters ///@{ @@ -1079,6 +1525,12 @@ class App { /// Get the app or subcommand description std::string get_description() const { return description_; } + /// Set the description of the app + App *description(std::string app_description) { + description_ = std::move(app_description); + return this; + } + /// Get the list of options (user facing function, so returns raw pointers), has optional filter function std::vector<const Option *> get_options(const std::function<bool(const Option *)> filter = {}) const { std::vector<const Option *> options(options_.size()); @@ -1096,37 +1548,88 @@ class App { return options; } - /// Get an option by name - const Option *get_option(std::string name) const { - for(const Option_p &opt : options_) { - if(opt->check_name(name)) { + /// Get an option by name (noexcept non-const version) + Option *get_option_no_throw(std::string option_name) noexcept { + for(Option_p &opt : options_) { + if(opt->check_name(option_name)) { return opt.get(); } } - throw OptionNotFound(name); + for(auto &subc : subcommands_) { + // also check down into nameless subcommands + if(subc->get_name().empty()) { + auto opt = subc->get_option_no_throw(option_name); + if(opt != nullptr) { + return opt; + } + } + } + return nullptr; } - /// Get an option by name (non-const version) - Option *get_option(std::string name) { - for(Option_p &opt : options_) { - if(opt->check_name(name)) { + /// Get an option by name (noexcept const version) + const Option *get_option_no_throw(std::string option_name) const noexcept { + for(const Option_p &opt : options_) { + if(opt->check_name(option_name)) { return opt.get(); } } - throw OptionNotFound(name); + for(const auto &subc : subcommands_) { + // also check down into nameless subcommands + if(subc->get_name().empty()) { + auto opt = subc->get_option_no_throw(option_name); + if(opt != nullptr) { + return opt; + } + } + } + return nullptr; + } + + /// Get an option by name + const Option *get_option(std::string option_name) const { + auto opt = get_option_no_throw(option_name); + if(opt == nullptr) { + throw OptionNotFound(option_name); + } + return opt; + } + + /// Get an option by name (non-const version) + Option *get_option(std::string option_name) { + auto opt = get_option_no_throw(option_name); + if(opt == nullptr) { + throw OptionNotFound(option_name); + } + return opt; } + /// Shortcut bracket operator for getting a pointer to an option + const Option *operator[](const std::string &option_name) const { return get_option(option_name); } + + /// Shortcut bracket operator for getting a pointer to an option + const Option *operator[](const char *option_name) const { return get_option(option_name); } + /// Check the status of ignore_case bool get_ignore_case() const { return ignore_case_; } + /// Check the status of ignore_underscore + bool get_ignore_underscore() const { return ignore_underscore_; } + /// Check the status of fallthrough bool get_fallthrough() const { return fallthrough_; } + /// Check the status of the allow windows style options + bool get_allow_windows_style_options() const { return allow_windows_style_options_; } + + /// Check the status of the allow windows style options + bool get_positionals_at_end() const { return positionals_at_end_; } + /// Get the group of this subcommand const std::string &get_group() const { return group_; } /// Get footer. - std::string get_footer() const { return footer_; } + const std::string &get_footer() const { return footer_; } /// Get the required min subcommand value size_t get_require_subcommand_min() const { return require_subcommand_min_; } @@ -1134,12 +1637,35 @@ class App { /// Get the required max subcommand value size_t get_require_subcommand_max() const { return require_subcommand_max_; } + /// Get the required min option value + size_t get_require_option_min() const { return require_option_min_; } + + /// Get the required max option value + size_t get_require_option_max() const { return require_option_max_; } + /// Get the prefix command status bool get_prefix_command() const { return prefix_command_; } /// Get the status of allow extras bool get_allow_extras() const { return allow_extras_; } + /// Get the status of required + bool get_required() const { return required_; } + + /// Get the status of disabled + bool get_disabled() const { return disabled_; } + + /// Get the status of disabled + bool get_immediate_callback() const { return immediate_callback_; } + + /// Get the status of disabled by default + bool get_disabled_by_default() const { return disabled_by_default_; } + + /// Get the status of disabled by default + bool get_enabled_by_default() const { return enabled_by_default_; } + /// Get the status of validating positionals + bool get_validate_positionals() const { return validate_positionals_; } + /// Get the status of allow extras bool get_allow_config_extras() const { return allow_config_extras_; } @@ -1167,9 +1693,16 @@ class App { /// Get the name of the current app std::string get_name() const { return name_; } - /// Check the name, case insensitive if set + /// Get a display name for an app + std::string get_display_name() const { return (!name_.empty()) ? name_ : "[Option Group: " + get_group() + "]"; } + + /// Check the name, case insensitive and underscore insensitive if set bool check_name(std::string name_to_check) const { std::string local_name = name_; + if(ignore_underscore_) { + local_name = detail::remove_underscore(name_); + name_to_check = detail::remove_underscore(name_to_check); + } if(ignore_case_) { local_name = detail::to_lower(name_); name_to_check = detail::to_lower(name_to_check); @@ -1198,12 +1731,22 @@ class App { /// This returns the missing options from the current subcommand std::vector<std::string> remaining(bool recurse = false) const { std::vector<std::string> miss_list; - for(const std::pair<detail::Classifer, std::string> &miss : missing_) { + for(const std::pair<detail::Classifier, std::string> &miss : missing_) { miss_list.push_back(std::get<1>(miss)); } - - // Recurse into subcommands + // Get from a subcommand that may allow extras if(recurse) { + if(!allow_extras_) { + for(const auto &sub : subcommands_) { + if(sub->name_.empty() && !sub->missing_.empty()) { + for(const std::pair<detail::Classifier, std::string> &miss : sub->missing_) { + miss_list.push_back(std::get<1>(miss)); + } + } + } + } + // Recurse into subcommands + for(const App *sub : parsed_subcommands_) { std::vector<std::string> output = sub->remaining(recurse); std::copy(std::begin(output), std::end(output), std::back_inserter(miss_list)); @@ -1212,18 +1755,19 @@ class App { return miss_list; } - /// This returns the number of remaining options, minus the -- seperator + /// This returns the number of remaining options, minus the -- separator size_t remaining_size(bool recurse = false) const { - auto count = static_cast<size_t>(std::count_if( - std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifer, std::string> &val) { - return val.first != detail::Classifer::POSITIONAL_MARK; + auto remaining_options = static_cast<size_t>(std::count_if( + std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifier, std::string> &val) { + return val.first != detail::Classifier::POSITIONAL_MARK; })); + if(recurse) { for(const App_p &sub : subcommands_) { - count += sub->remaining_size(recurse); + remaining_options += sub->remaining_size(recurse); } } - return count; + return remaining_options; } ///@} @@ -1231,70 +1775,118 @@ class App { protected: /// Check the options to make sure there are no conflicts. /// - /// Currently checks to see if multiple positionals exist with -1 args + /// Currently checks to see if multiple positionals exist with -1 args and checks if the min and max options are + /// feasible void _validate() const { - auto count = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) { + auto pcount = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) { return opt->get_items_expected() < 0 && opt->get_positional(); }); - if(count > 1) + if(pcount > 1) throw InvalidError(name_); - for(const App_p &app : subcommands_) + + size_t nameless_subs{0}; + for(const App_p &app : subcommands_) { app->_validate(); + if(app->get_name().empty()) + ++nameless_subs; + } + + if(require_option_min_ > 0) { + if(require_option_max_ > 0) { + if(require_option_max_ < require_option_min_) { + throw(InvalidError("Required min options greater than required max options", + ExitCodes::InvalidError)); + } + } + if(require_option_min_ > (options_.size() + nameless_subs)) { + throw(InvalidError("Required min options greater than number of available options", + ExitCodes::InvalidError)); + } + } } - /// Internal function to run (App) callback, top down + /// configure subcommands to enable parsing through the current object + /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable + /// makes sure parent is set correctly + void _configure() { + if(disabled_by_default_) { + disabled_ = true; + } + if(enabled_by_default_) { + disabled_ = false; + } + for(const App_p &app : subcommands_) { + if(app->has_automatic_name_) { + app->name_.clear(); + } + if(app->name_.empty()) { + app->fallthrough_ = false; // make sure fallthrough_ is false to prevent infinite loop + app->prefix_command_ = false; + } + // make sure the parent is set to be this object in preparation for parse + app->parent_ = this; + app->_configure(); + } + } + /// Internal function to run (App) callback, bottom up void run_callback() { pre_callback(); - if(callback_) - callback_(); + // run the callbacks for the received subcommands for(App *subc : get_subcommands()) { - subc->run_callback(); + if(!subc->immediate_callback_) + subc->run_callback(); + } + // now run callbacks for option_groups + for(auto &subc : subcommands_) { + if(!subc->immediate_callback_ && subc->name_.empty() && subc->count_all() > 0) { + subc->run_callback(); + } + } + // finally run the main callback + if(callback_ && (parsed_ > 0)) { + if(!name_.empty() || count_all() > 0) { + callback_(); + } } } /// Check to see if a subcommand is valid. Give up immediately if subcommand max has been reached. - bool _valid_subcommand(const std::string ¤t) const { + bool _valid_subcommand(const std::string ¤t, bool ignore_used = true) const { // Don't match if max has been reached - but still check parents if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) { - return parent_ != nullptr && parent_->_valid_subcommand(current); + return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used); + } + auto com = _find_subcommand(current, true, ignore_used); + if(com != nullptr) { + return true; } - - for(const App_p &com : subcommands_) - if(com->check_name(current) && !*com) - return true; - // Check parent if exists, else return false - return parent_ != nullptr && parent_->_valid_subcommand(current); + return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used); } /// Selects a Classifier enum based on the type of the current argument - detail::Classifer _recognize(const std::string ¤t) const { + detail::Classifier _recognize(const std::string ¤t, bool ignore_used_subcommands = true) const { std::string dummy1, dummy2; if(current == "--") - return detail::Classifer::POSITIONAL_MARK; - if(_valid_subcommand(current)) - return detail::Classifer::SUBCOMMAND; + return detail::Classifier::POSITIONAL_MARK; + if(_valid_subcommand(current, ignore_used_subcommands)) + return detail::Classifier::SUBCOMMAND; if(detail::split_long(current, dummy1, dummy2)) - return detail::Classifer::LONG; + return detail::Classifier::LONG; if(detail::split_short(current, dummy1, dummy2)) - return detail::Classifer::SHORT; - return detail::Classifer::NONE; + return detail::Classifier::SHORT; + if((allow_windows_style_options_) && (detail::split_windows_style(current, dummy1, dummy2))) + return detail::Classifier::WINDOWS; + if((current == "++") && !name_.empty()) + return detail::Classifier::SUBCOMMAND_TERMINATOR; + return detail::Classifier::NONE; } - /// Internal parse function - void _parse(std::vector<std::string> &args) { - parsed_ = true; - bool positional_only = false; - - while(!args.empty()) { - _parse_single(args, positional_only); - } - - for(const Option_p &opt : options_) - if(opt->get_short_circuit() && opt->count() > 0) - opt->run_callback(); + // The parse function is now broken into several parts, and part of process + /// Read and process an ini file (main app only) + void _process_ini() { // Process an INI file if(config_ptr_ != nullptr) { if(*config_ptr_) { @@ -1311,8 +1903,10 @@ class App { } } } + } - // Get envname options if not yet passed + /// Get envname options if not yet passed. Runs on *all* subcommands. + void _process_env() { for(const Option_p &opt : options_) { if(opt->count() == 0 && !opt->envname_.empty()) { char *buffer = nullptr; @@ -1338,19 +1932,93 @@ class App { } } - // Process callbacks + for(App_p &sub : subcommands_) { + if(sub->get_name().empty() || !sub->immediate_callback_) + sub->_process_env(); + } + } + + /// Process callbacks. Runs on *all* subcommands. + void _process_callbacks() { + + for(App_p &sub : subcommands_) { + // process the priority option_groups first + if(sub->get_name().empty() && sub->immediate_callback_) { + if(sub->count_all() > 0) { + sub->_process_callbacks(); + sub->run_callback(); + } + } + } + for(const Option_p &opt : options_) { if(opt->count() > 0 && !opt->get_callback_run()) { opt->run_callback(); } } - // Verify required options + for(App_p &sub : subcommands_) { + if(!sub->immediate_callback_) { + sub->_process_callbacks(); + } + } + } + + /// Run help flag processing if any are found. + /// + /// The flags allow recursive calls to remember if there was a help flag on a parent. + void _process_help_flags(bool trigger_help = false, bool trigger_all_help = false) const { + const Option *help_ptr = get_help_ptr(); + const Option *help_all_ptr = get_help_all_ptr(); + + if(help_ptr != nullptr && help_ptr->count() > 0) + trigger_help = true; + if(help_all_ptr != nullptr && help_all_ptr->count() > 0) + trigger_all_help = true; + + // If there were parsed subcommands, call those. First subcommand wins if there are multiple ones. + if(!parsed_subcommands_.empty()) { + for(const App *sub : parsed_subcommands_) + sub->_process_help_flags(trigger_help, trigger_all_help); + + // Only the final subcommand should call for help. All help wins over help. + } else if(trigger_all_help) { + throw CallForAllHelp(); + } else if(trigger_help) { + throw CallForHelp(); + } + } + + /// Verify required options and cross requirements. Subcommands too (only if selected). + void _process_requirements() { + // check excludes + bool excluded{false}; + std::string excluder; + for(auto &opt : exclude_options_) { + if(opt->count() > 0) { + excluded = true; + excluder = opt->get_name(); + } + } + for(auto &subc : exclude_subcommands_) { + if(subc->count_all() > 0) { + excluded = true; + excluder = subc->get_display_name(); + } + } + if(excluded) { + if(count_all() > 0) { + throw ExcludesError(get_display_name(), excluder); + } + // if we are excluded but didn't receive anything, just return + return; + } + size_t used_options = 0; for(const Option_p &opt : options_) { - // Exit if a help flag was passed (requirements not required in that case) - if(_any_help_flag()) - break; + if(opt->count() != 0) { + ++used_options; + } // Required or partially filled if(opt->get_required() || opt->count() != 0) { // Make sure enough -N arguments parsed (+N is already handled in parsing function) @@ -1370,12 +2038,77 @@ class App { if(opt->count() > 0 && opt_ex->count() != 0) throw ExcludesError(opt->get_name(), opt_ex->get_name()); } + // check for the required number of subcommands + if(require_subcommand_min_ > 0) { + auto selected_subcommands = get_subcommands(); + if(require_subcommand_min_ > selected_subcommands.size()) + throw RequiredError::Subcommand(require_subcommand_min_); + } - auto selected_subcommands = get_subcommands(); - if(require_subcommand_min_ > selected_subcommands.size()) - throw RequiredError::Subcommand(require_subcommand_min_); + // Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item. - // Convert missing (pairs) to extras (string only) + // run this loop to check how many unnamed subcommands were actually used since they are considered options from + // the perspective of an App + for(App_p &sub : subcommands_) { + if(sub->disabled_) + continue; + if(sub->name_.empty() && sub->count_all() > 0) { + ++used_options; + } + } + + if(require_option_min_ > used_options || (require_option_max_ > 0 && require_option_max_ < used_options)) { + auto option_list = detail::join(options_, [](const Option_p &ptr) { return ptr->get_name(false, true); }); + if(option_list.compare(0, 10, "-h,--help,") == 0) { + option_list.erase(0, 10); + } + auto subc_list = get_subcommands([](App *app) { return ((app->get_name().empty()) && (!app->disabled_)); }); + if(!subc_list.empty()) { + option_list += "," + detail::join(subc_list, [](const App *app) { return app->get_display_name(); }); + } + throw RequiredError::Option(require_option_min_, require_option_max_, used_options, option_list); + } + + // now process the requirements for subcommands if needed + for(App_p &sub : subcommands_) { + if(sub->disabled_) + continue; + if(sub->name_.empty() && sub->required_ == false) { + if(sub->count_all() == 0) { + if(require_option_min_ > 0 && require_option_min_ <= used_options) { + continue; + // if we have met the requirement and there is nothing in this option group skip checking + // requirements + } + if(require_option_max_ > 0 && used_options >= require_option_min_) { + continue; + // if we have met the requirement and there is nothing in this option group skip checking + // requirements + } + } + } + if(sub->count() > 0 || sub->name_.empty()) { + sub->_process_requirements(); + } + + if(sub->required_ && sub->count_all() == 0) { + throw(CLI::RequiredError(sub->get_display_name())); + } + } + } + + /// Process callbacks and such. + void _process() { + _process_ini(); + _process_env(); + _process_callbacks(); + _process_help_flags(); + _process_requirements(); + } + + /// Throw an error if anything is left over and should not be. + /// Modifies the args to fill in the missing items before throwing. + void _process_extras(std::vector<std::string> &args) { if(!(allow_extras_ || prefix_command_)) { size_t num_left_over = remaining_size(); if(num_left_over > 0) { @@ -1384,24 +2117,47 @@ class App { } } - if(parent_ == nullptr) { - args = remaining(false); + for(App_p &sub : subcommands_) { + if(sub->count() > 0) + sub->_process_extras(args); } } - /// Return True if a help flag detected (checks all parents) (only run if help called before subcommand) - bool _any_help_flag() const { - bool result = false; - const Option *help_ptr = get_help_ptr(); - const Option *help_all_ptr = get_help_all_ptr(); - if(help_ptr != nullptr && help_ptr->count() > 0) - result = true; - if(help_all_ptr != nullptr && help_all_ptr->count() > 0) - result = true; - if(parent_ != nullptr) - return result || parent_->_any_help_flag(); - else - return result; + /// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands + void increment_parsed() { + ++parsed_; + for(App_p &sub : subcommands_) { + if(sub->get_name().empty()) + sub->increment_parsed(); + } + } + /// Internal parse function + void _parse(std::vector<std::string> &args) { + increment_parsed(); + _trigger_pre_parse(args.size()); + bool positional_only = false; + + while(!args.empty()) { + if(!_parse_single(args, positional_only)) { + break; + } + } + + if(parent_ == nullptr) { + _process(); + + // Throw error if any items are left over (depending on settings) + _process_extras(args); + + // Convert missing (pairs) to extras (string only) + args = remaining(false); + } else if(immediate_callback_) { + _process_env(); + _process_callbacks(); + _process_help_flags(); + _process_requirements(); + run_callback(); + } } /// Parse one config param, return false if not found in any subcommand, remove if it is @@ -1418,23 +2174,20 @@ class App { /// Fill in a single config option bool _parse_single_config(const ConfigItem &item, size_t level = 0) { if(level < item.parents.size()) { - App *subcom; try { - subcom = get_subcommand(item.parents.at(level)); + auto subcom = get_subcommand(item.parents.at(level)); + return subcom->_parse_single_config(item, level + 1); } catch(const OptionNotFound &) { return false; } - return subcom->_parse_single_config(item, level + 1); } - Option *op; - try { - op = get_option("--" + item.name); - } catch(const OptionNotFound &) { + Option *op = get_option_no_throw("--" + item.name); + if(op == nullptr) { // If the option was not present if(get_allow_config_extras()) // Should we worry about classifying the extras properly? - missing_.emplace_back(detail::Classifer::NONE, item.fullname()); + missing_.emplace_back(detail::Classifier::NONE, item.fullname()); return false; } @@ -1444,9 +2197,13 @@ class App { if(op->empty()) { // Flag parsing if(op->get_type_size() == 0) { - op->set_results(config_formatter_->to_flag(item)); + auto res = config_formatter_->to_flag(item); + res = op->get_flag_value(item.name, res); + + op->add_result(res); + } else { - op->set_results(item.inputs); + op->add_result(item.inputs); op->run_callback(); } } @@ -1454,132 +2211,254 @@ class App { return true; } - /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing from - /// master - void _parse_single(std::vector<std::string> &args, bool &positional_only) { - - detail::Classifer classifer = positional_only ? detail::Classifer::NONE : _recognize(args.back()); - switch(classifer) { - case detail::Classifer::POSITIONAL_MARK: - missing_.emplace_back(classifer, args.back()); + /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing + /// from master return false if the parse has failed and needs to return to parent + bool _parse_single(std::vector<std::string> &args, bool &positional_only) { + bool retval = true; + detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back()); + switch(classifier) { + case detail::Classifier::POSITIONAL_MARK: args.pop_back(); positional_only = true; + if((!_has_remaining_positionals()) && (parent_ != nullptr)) { + retval = false; + } else { + _move_to_missing(classifier, "--"); + } break; - case detail::Classifer::SUBCOMMAND: - _parse_subcommand(args); + case detail::Classifier::SUBCOMMAND_TERMINATOR: + args.pop_back(); + retval = false; break; - case detail::Classifer::LONG: - // If already parsed a subcommand, don't accept options_ - _parse_arg(args, true); + case detail::Classifier::SUBCOMMAND: + retval = _parse_subcommand(args); break; - case detail::Classifer::SHORT: + case detail::Classifier::LONG: + case detail::Classifier::SHORT: + case detail::Classifier::WINDOWS: // If already parsed a subcommand, don't accept options_ - _parse_arg(args, false); + _parse_arg(args, classifier); break; - case detail::Classifer::NONE: + case detail::Classifier::NONE: // Probably a positional or something for a parent (sub)command - _parse_positional(args); + retval = _parse_positional(args); + if(retval && positionals_at_end_) { + positional_only = true; + } + break; + + // LCOV_EXCL_START + default: + HorribleError("unrecognized classifier (you should not see this!)"); + // LCOV_EXCL_END } + return retval; } /// Count the required remaining positional arguments - size_t _count_remaining_positionals(bool required = false) const { + size_t _count_remaining_positionals(bool required_only = false) const { size_t retval = 0; for(const Option_p &opt : options_) - if(opt->get_positional() && (!required || opt->get_required()) && opt->get_items_expected() > 0 && + if(opt->get_positional() && (!required_only || opt->get_required()) && opt->get_items_expected() > 0 && static_cast<int>(opt->count()) < opt->get_items_expected()) retval = static_cast<size_t>(opt->get_items_expected()) - opt->count(); return retval; } + /// Count the required remaining positional arguments + bool _has_remaining_positionals() const { + for(const Option_p &opt : options_) + if(opt->get_positional() && + ((opt->get_items_expected() < 0) || ((static_cast<int>(opt->count()) < opt->get_items_expected())))) + return true; + + return false; + } + /// Parse a positional, go up the tree to check - void _parse_positional(std::vector<std::string> &args) { + /// Return true if the positional was used false otherwise + bool _parse_positional(std::vector<std::string> &args) { - std::string positional = args.back(); + const std::string &positional = args.back(); for(const Option_p &opt : options_) { // Eat options, one by one, until done if(opt->get_positional() && (static_cast<int>(opt->count()) < opt->get_items_expected() || opt->get_items_expected() < 0)) { - + if(validate_positionals_) { + std::string pos = positional; + pos = opt->_validate(pos); + if(!pos.empty()) { + continue; + } + } opt->add_result(positional); parse_order_.push_back(opt.get()); args.pop_back(); - return; + return true; } } + for(auto &subc : subcommands_) { + if((subc->name_.empty()) && (!subc->disabled_)) { + if(subc->_parse_positional(args)) { + if(!subc->pre_parse_called_) { + subc->_trigger_pre_parse(args.size()); + } + return true; + } + } + } + // let the parent deal with it if possible if(parent_ != nullptr && fallthrough_) - return parent_->_parse_positional(args); - else { + return _get_fallthrough_parent()->_parse_positional(args); + + /// Try to find a local subcommand that is repeated + auto com = _find_subcommand(args.back(), true, false); + if(com != nullptr && (require_subcommand_max_ == 0 || require_subcommand_max_ > parsed_subcommands_.size())) { args.pop_back(); - missing_.emplace_back(detail::Classifer::NONE, positional); + com->_parse(args); + return true; + } + /// now try one last gasp at subcommands that have been executed before, go to root app and try to find a + /// subcommand in a broader way, if one exists let the parent deal with it + auto parent_app = (parent_ != nullptr) ? _get_fallthrough_parent() : this; + com = parent_app->_find_subcommand(args.back(), true, false); + if(com != nullptr && (com->parent_->require_subcommand_max_ == 0 || + com->parent_->require_subcommand_max_ > com->parent_->parsed_subcommands_.size())) { + return false; + } - if(prefix_command_) { - while(!args.empty()) { - missing_.emplace_back(detail::Classifer::NONE, args.back()); - args.pop_back(); + if(positionals_at_end_) { + throw CLI::ExtrasError(args); + } + /// If this is an option group don't deal with it + if(parent_ != nullptr && name_.empty()) { + return false; + } + /// We are out of other options this goes to missing + _move_to_missing(detail::Classifier::NONE, positional); + args.pop_back(); + if(prefix_command_) { + while(!args.empty()) { + _move_to_missing(detail::Classifier::NONE, args.back()); + args.pop_back(); + } + } + + return true; + } + + /// Locate a subcommand by name with two conditions, should disabled subcommands be ignored, and should used + /// subcommands be ignored + App *_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept { + for(const App_p &com : subcommands_) { + if(com->disabled_ && ignore_disabled) + continue; + if(com->get_name().empty()) { + auto subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used); + if(subc != nullptr) { + return subc; } + } else if(com->check_name(subc_name)) { + if((!*com) || !ignore_used) + return com.get(); } } + return nullptr; } /// Parse a subcommand, modify args and continue /// /// Unlike the others, this one will always allow fallthrough - void _parse_subcommand(std::vector<std::string> &args) { - if(_count_remaining_positionals(/* required */ true) > 0) - return _parse_positional(args); - for(const App_p &com : subcommands_) { - if(com->check_name(args.back())) { - args.pop_back(); - if(std::find(std::begin(parsed_subcommands_), std::end(parsed_subcommands_), com.get()) == - std::end(parsed_subcommands_)) - parsed_subcommands_.push_back(com.get()); - com->_parse(args); - return; + /// return true if the subcommand was processed false otherwise + bool _parse_subcommand(std::vector<std::string> &args) { + if(_count_remaining_positionals(/* required */ true) > 0) { + _parse_positional(args); + return true; + } + auto com = _find_subcommand(args.back(), true, true); + if(com != nullptr) { + args.pop_back(); + parsed_subcommands_.push_back(com); + com->_parse(args); + auto parent_app = com->parent_; + while(parent_app != this) { + parent_app->_trigger_pre_parse(args.size()); + parent_app->parsed_subcommands_.push_back(com); + parent_app = parent_app->parent_; } + return true; } - if(parent_ != nullptr) - return parent_->_parse_subcommand(args); - else + + if(parent_ == nullptr) throw HorribleError("Subcommand " + args.back() + " missing"); + return false; } /// Parse a short (false) or long (true) argument, must be at the top of the list - void _parse_arg(std::vector<std::string> &args, bool second_dash) { - - detail::Classifer current_type = second_dash ? detail::Classifer::LONG : detail::Classifer::SHORT; + /// return true if the argument was processed or false if nothing was done + bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type) { std::string current = args.back(); - std::string name; + std::string arg_name; std::string value; std::string rest; - if(second_dash) { - if(!detail::split_long(current, name, value)) + switch(current_type) { + case detail::Classifier::LONG: + if(!detail::split_long(current, arg_name, value)) throw HorribleError("Long parsed but missing (you should not see this):" + args.back()); - } else { - if(!detail::split_short(current, name, rest)) + break; + case detail::Classifier::SHORT: + if(!detail::split_short(current, arg_name, rest)) throw HorribleError("Short parsed but missing! You should not see this"); + break; + case detail::Classifier::WINDOWS: + if(!detail::split_windows_style(current, arg_name, value)) + throw HorribleError("windows option parsed but missing! You should not see this"); + break; + case detail::Classifier::SUBCOMMAND: + case detail::Classifier::POSITIONAL_MARK: + case detail::Classifier::NONE: + default: + throw HorribleError("parsing got called with invalid option! You should not see this"); } - auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, second_dash](const Option_p &opt) { - return second_dash ? opt->check_lname(name) : opt->check_sname(name); - }); + auto op_ptr = + std::find_if(std::begin(options_), std::end(options_), [arg_name, current_type](const Option_p &opt) { + if(current_type == detail::Classifier::LONG) + return opt->check_lname(arg_name); + if(current_type == detail::Classifier::SHORT) + return opt->check_sname(arg_name); + // this will only get called for detail::Classifier::WINDOWS + return opt->check_lname(arg_name) || opt->check_sname(arg_name); + }); // Option not found if(op_ptr == std::end(options_)) { + for(auto &subc : subcommands_) { + if(subc->name_.empty() && !subc->disabled_) { + if(subc->_parse_arg(args, current_type)) { + if(!subc->pre_parse_called_) { + subc->_trigger_pre_parse(args.size()); + } + return true; + } + } + } // If a subcommand, try the master command if(parent_ != nullptr && fallthrough_) - return parent_->_parse_arg(args, second_dash); - // Otherwise, add to missing - else { - args.pop_back(); - missing_.emplace_back(current_type, current); - return; + return _get_fallthrough_parent()->_parse_arg(args, current_type); + // don't capture missing if this is a nameless subcommand + if(parent_ != nullptr && name_.empty()) { + return false; } + // Otherwise, add to missing + args.pop_back(); + _move_to_missing(current_type, current); + return true; } args.pop_back(); @@ -1591,31 +2470,36 @@ class App { // Make sure we always eat the minimum for unlimited vectors int collected = 0; - + int result_count = 0; + // deal with flag like things + if(num == 0) { + auto res = op->get_flag_value(arg_name, value); + op->add_result(res); + parse_order_.push_back(op.get()); + } // --this=value - if(!value.empty()) { + else if(!value.empty()) { + op->add_result(value, result_count); + parse_order_.push_back(op.get()); + collected += result_count; // If exact number expected if(num > 0) - num--; - op->add_result(value); - parse_order_.push_back(op.get()); - collected += 1; - } else if(num == 0) { - op->add_result(""); - parse_order_.push_back(op.get()); + num = (num >= result_count) ? num - result_count : 0; + // -Trest } else if(!rest.empty()) { - if(num > 0) - num--; - op->add_result(rest); + op->add_result(rest, result_count); parse_order_.push_back(op.get()); rest = ""; - collected += 1; + collected += result_count; + // If exact number expected + if(num > 0) + num = (num >= result_count) ? num - result_count : 0; } // Unlimited vector parser if(num < 0) { - while(!args.empty() && _recognize(args.back()) == detail::Classifer::NONE) { + while(!args.empty() && _recognize(args.back(), false) == detail::Classifier::NONE) { if(collected >= -num) { // We could break here for allow extras, but we don't @@ -1623,23 +2507,23 @@ class App { if(_count_remaining_positionals() > 0) break; } - op->add_result(args.back()); + op->add_result(args.back(), result_count); parse_order_.push_back(op.get()); args.pop_back(); - collected++; + collected += result_count; } // Allow -- to end an unlimited list and "eat" it - if(!args.empty() && _recognize(args.back()) == detail::Classifer::POSITIONAL_MARK) + if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK) args.pop_back(); } else { while(num > 0 && !args.empty()) { - num--; std::string current_ = args.back(); args.pop_back(); - op->add_result(current_); + op->add_result(current_, result_count); parse_order_.push_back(op.get()); + num -= result_count; } if(num > 0) { @@ -1651,16 +2535,192 @@ class App { rest = "-" + rest; args.push_back(rest); } + return true; + } + + /// Trigger the pre_parse callback if needed + void _trigger_pre_parse(size_t remaining_args) { + if(!pre_parse_called_) { + pre_parse_called_ = true; + if(pre_parse_callback_) { + pre_parse_callback_(remaining_args); + } + } else if(immediate_callback_) { + if(!name_.empty()) { + auto pcnt = parsed_; + auto extras = std::move(missing_); + clear(); + parsed_ = pcnt; + pre_parse_called_ = true; + missing_ = std::move(extras); + } + } + } + + /// Get the appropriate parent to fallthrough to which is the first one that has a name or the main app + App *_get_fallthrough_parent() { + if(parent_ == nullptr) { + throw(HorribleError("No Valid parent")); + } + auto fallthrough_parent = parent_; + while((fallthrough_parent->parent_ != nullptr) && (fallthrough_parent->get_name().empty())) { + fallthrough_parent = fallthrough_parent->parent_; + } + return fallthrough_parent; + } + + /// Helper function to place extra values in the most appropriate position + void _move_to_missing(detail::Classifier val_type, const std::string &val) { + if(allow_extras_ || subcommands_.empty()) { + missing_.emplace_back(val_type, val); + return; + } + // allow extra arguments to be places in an option group if it is allowed there + for(auto &subc : subcommands_) { + if(subc->name_.empty() && subc->allow_extras_) { + subc->missing_.emplace_back(val_type, val); + return; + } + } + // if we haven't found any place to put them yet put them in missing + missing_.emplace_back(val_type, val); + } + + public: + /// function that could be used by subclasses of App to shift options around into subcommands + void _move_option(Option *opt, App *app) { + if(opt == nullptr) { + throw OptionNotFound("the option is NULL"); + } + // verify that the give app is actually a subcommand + bool found = false; + for(auto &subc : subcommands_) { + if(app == subc.get()) { + found = true; + } + } + if(!found) { + throw OptionNotFound("The Given app is not a subcommand"); + } + + if((help_ptr_ == opt) || (help_all_ptr_ == opt)) + throw OptionAlreadyAdded("cannot move help options"); + + if(config_ptr_ == opt) + throw OptionAlreadyAdded("cannot move config file options"); + + auto iterator = + std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; }); + if(iterator != std::end(options_)) { + const auto &opt_p = *iterator; + if(std::find_if(std::begin(app->options_), std::end(app->options_), [&opt_p](const Option_p &v) { + return (*v == *opt_p); + }) == std::end(app->options_)) { + // only erase after the insertion was successful + app->options_.push_back(std::move(*iterator)); + options_.erase(iterator); + } else { + throw OptionAlreadyAdded(opt->get_name()); + } + } else { + throw OptionNotFound("could not locate the given App"); + } } }; +/// Extension of App to better manage groups of options +class Option_group : public App { + public: + Option_group(std::string group_description, std::string group_name, App *parent) + : App(std::move(group_description), "", parent) { + group(group_name); + // option groups should have automatic fallthrough + } + using App::add_option; + /// Add an existing option to the Option_group + Option *add_option(Option *opt) { + if(get_parent() == nullptr) { + throw OptionNotFound("Unable to locate the specified option"); + } + get_parent()->_move_option(opt, this); + return opt; + } + /// Add an existing option to the Option_group + void add_options(Option *opt) { add_option(opt); } + /// Add a bunch of options to the group + template <typename... Args> void add_options(Option *opt, Args... args) { + add_option(opt); + add_options(args...); + } + using App::add_subcommand; + /// Add an existing subcommand to be a member of an option_group + App *add_subcommand(App *subcom) { + App_p subc = subcom->get_parent()->get_subcommand_ptr(subcom); + subc->get_parent()->remove_subcommand(subcom); + add_subcommand(std::move(subc)); + return subcom; + } +}; +/// Helper function to enable one option group/subcommand when another is used +inline void TriggerOn(App *trigger_app, App *app_to_enable) { + app_to_enable->enabled_by_default(false); + app_to_enable->disabled_by_default(); + trigger_app->preparse_callback([app_to_enable](size_t) { app_to_enable->disabled(false); }); +} + +/// Helper function to enable one option group/subcommand when another is used +inline void TriggerOn(App *trigger_app, std::vector<App *> apps_to_enable) { + for(auto &app : apps_to_enable) { + app->enabled_by_default(false); + app->disabled_by_default(); + } + + trigger_app->preparse_callback([apps_to_enable](size_t) { + for(auto &app : apps_to_enable) { + app->disabled(false); + } + }); +} + +/// Helper function to disable one option group/subcommand when another is used +inline void TriggerOff(App *trigger_app, App *app_to_enable) { + app_to_enable->disabled_by_default(false); + app_to_enable->enabled_by_default(); + trigger_app->preparse_callback([app_to_enable](size_t) { app_to_enable->disabled(); }); +} + +/// Helper function to disable one option group/subcommand when another is used +inline void TriggerOff(App *trigger_app, std::vector<App *> apps_to_enable) { + for(auto &app : apps_to_enable) { + app->disabled_by_default(false); + app->enabled_by_default(); + } + + trigger_app->preparse_callback([apps_to_enable](size_t) { + for(auto &app : apps_to_enable) { + app->disabled(); + } + }); +} + namespace FailureMessage { /// Printout a clean, simple message on error (the default in CLI11 1.5+) inline std::string simple(const App *app, const Error &e) { std::string header = std::string(e.what()) + "\n"; + std::vector<std::string> names; + + // Collect names if(app->get_help_ptr() != nullptr) - header += "Run with " + app->get_help_ptr()->get_name() + " for more information.\n"; + names.push_back(app->get_help_ptr()->get_name()); + + if(app->get_help_all_ptr() != nullptr) + names.push_back(app->get_help_all_ptr()->get_name()); + + // If any names found, suggest those + if(!names.empty()) + header += "Run with " + detail::join(names, " or ") + " for more information.\n"; + return header; } @@ -1690,6 +2750,8 @@ struct AppFriend { typename std::result_of<decltype (&App::_parse_subcommand)(App, Args...)>::type { return app->_parse_subcommand(std::forward<Args>(args)...); } + /// Wrap the fallthrough parent function to make sure that is working correctly + static App *get_fallthrough_parent(App *app) { return app->_get_fallthrough_parent(); } }; } // namespace detail diff --git a/packages/CLI11/include/CLI/Config.hpp b/packages/CLI11/include/CLI/Config.hpp index 4a72a6198..a48c20e10 100644 --- a/packages/CLI11/include/CLI/Config.hpp +++ b/packages/CLI11/include/CLI/Config.hpp @@ -54,7 +54,12 @@ ConfigINI::to_config(const App *app, bool default_also, bool write_description, } out << "; " << detail::fix_newlines("; ", opt->get_description()) << std::endl; } - out << name << "=" << value << std::endl; + + // Don't try to quote anything that is not size 1 + if(opt->get_items_expected() != 1) + out << name << "=" << value << std::endl; + else + out << name << "=" << detail::add_quotes_if_needed(value) << std::endl; } } } diff --git a/packages/CLI11/include/CLI/ConfigFwd.hpp b/packages/CLI11/include/CLI/ConfigFwd.hpp index 53be44036..9e9de6986 100644 --- a/packages/CLI11/include/CLI/ConfigFwd.hpp +++ b/packages/CLI11/include/CLI/ConfigFwd.hpp @@ -27,10 +27,10 @@ inline std::string ini_join(std::vector<std::string> args) { auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace<char>(ch, std::locale()); }); if(it == arg.end()) s << arg; - else if(arg.find(R"(")") == std::string::npos) - s << R"(")" << arg << R"(")"; + else if(arg.find_first_of('\"') == std::string::npos) + s << '\"' << arg << '\"'; else - s << R"(')" << arg << R"(')"; + s << '\'' << arg << '\''; } return s.str(); @@ -69,27 +69,12 @@ class Config { /// Convert a configuration into an app virtual std::vector<ConfigItem> from_config(std::istream &) const = 0; - /// Convert a flag to a bool - virtual std::vector<std::string> to_flag(const ConfigItem &item) const { + /// Get a flag value + virtual std::string to_flag(const ConfigItem &item) const { if(item.inputs.size() == 1) { - std::string val = item.inputs.at(0); - val = detail::to_lower(val); - - if(val == "true" || val == "on" || val == "yes") { - return std::vector<std::string>(1); - } else if(val == "false" || val == "off" || val == "no") { - return std::vector<std::string>(); - } else { - try { - size_t ui = std::stoul(val); - return std::vector<std::string>(ui); - } catch(const std::invalid_argument &) { - throw ConversionError::TrueFalse(item.fullname()); - } - } - } else { - throw ConversionError::TooManyInputsFlag(item.fullname()); + return item.inputs.at(0); } + throw ConversionError::TooManyInputsFlag(item.fullname()); } /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure @@ -101,7 +86,7 @@ class Config { return from_config(input); } - /// virtual destructor + /// Virtual destructor virtual ~Config() = default; }; diff --git a/packages/CLI11/include/CLI/Error.hpp b/packages/CLI11/include/CLI/Error.hpp index 6a5061358..59687f5c8 100644 --- a/packages/CLI11/include/CLI/Error.hpp +++ b/packages/CLI11/include/CLI/Error.hpp @@ -17,9 +17,9 @@ namespace CLI { // These are temporary and are undef'd at the end of this file. #define CLI11_ERROR_DEF(parent, name) \ protected: \ - name(std::string name, std::string msg, int exit_code) : parent(std::move(name), std::move(msg), exit_code) {} \ - name(std::string name, std::string msg, ExitCodes exit_code) \ - : parent(std::move(name), std::move(msg), exit_code) {} \ + 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) {} \ @@ -61,16 +61,16 @@ enum class ExitCodes { /// All errors derive from this one class Error : public std::runtime_error { - int exit_code; - std::string name{"Error"}; + int actual_exit_code; + std::string error_name{"Error"}; public: - int get_exit_code() const { return exit_code; } + int get_exit_code() const { return actual_exit_code; } - std::string get_name() const { return name; } + 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), exit_code(exit_code), name(std::move(name)) {} + : 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)) {} }; @@ -158,7 +158,7 @@ class CallForHelp : public ParseError { CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} }; -/// Usually somethign like --help-all on command line +/// Usually something like --help-all on command line class CallForAllHelp : public ParseError { CLI11_ERROR_DEF(ParseError, CallForAllHelp) CallForAllHelp() @@ -212,6 +212,27 @@ class RequiredError : public ParseError { return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError); } + static RequiredError Option(size_t min_option, size_t max_option, 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 + "]"); + else if((min_option == 1) && (max_option == 1) && (used > 1)) + return RequiredError("Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) + + " were given", + ExitCodes::RequiredError); + else if((min_option == 1) && (used == 0)) + return RequiredError("At least 1 option from [" + option_list + "]"); + else if(used < min_option) + return RequiredError("Requires at least " + std::to_string(min_option) + " options used and only " + + std::to_string(used) + "were given from [" + option_list + "]", + ExitCodes::RequiredError); + else if(max_option == 1) + return RequiredError("Requires at most 1 options be given from [" + option_list + "]", + ExitCodes::RequiredError); + else + return RequiredError("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 @@ -231,6 +252,9 @@ class ArgumentMismatch : public ParseError { 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"); + } }; /// Thrown when a requires option is missing diff --git a/packages/CLI11/include/CLI/Formatter.hpp b/packages/CLI11/include/CLI/Formatter.hpp index 7f1885ec9..78acbc611 100644 --- a/packages/CLI11/include/CLI/Formatter.hpp +++ b/packages/CLI11/include/CLI/Formatter.hpp @@ -6,7 +6,7 @@ #include <string> #include "CLI/App.hpp" -#include "CLI/Formatter.hpp" +#include "CLI/FormatterFwd.hpp" namespace CLI { @@ -58,11 +58,28 @@ inline std::string Formatter::make_groups(const App *app, AppFormatMode mode) co inline std::string Formatter::make_description(const App *app) const { std::string desc = app->get_description(); - - if(!desc.empty()) - return desc + "\n"; - else - return ""; + auto min_options = app->get_require_option_min(); + auto max_options = app->get_require_option_max(); + if(app->get_required()) { + desc += " REQUIRED "; + } + if((max_options == min_options) && (min_options > 0)) { + if(min_options == 1) { + desc += " \n[Exactly 1 of the following options is required]"; + } else { + desc += " \n[Exactly " + std::to_string(min_options) + "options from the following list are required]"; + } + } else if(max_options > 0) { + if(min_options > 0) { + desc += " \n[Between " + std::to_string(min_options) + " and " + std::to_string(max_options) + + " of the follow options are required]"; + } else { + desc += " \n[At most " + std::to_string(max_options) + " of the following options are allowed]"; + } + } else if(min_options > 0) { + desc += " \n[At least " + std::to_string(min_options) + " of the following options are required]"; + } + return (!desc.empty()) ? desc + "\n" : std::string{}; } inline std::string Formatter::make_usage(const App *app, std::string name) const { @@ -93,7 +110,9 @@ inline std::string Formatter::make_usage(const App *app, std::string name) const } // Add a marker if subcommands are expected or optional - if(!app->get_subcommands({}).empty()) { + if(!app->get_subcommands( + [](const CLI::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); }) + .empty()) { out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "") << get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1 ? "SUBCOMMAND" : "SUBCOMMANDS") @@ -115,12 +134,17 @@ inline std::string Formatter::make_footer(const App *app) const { inline std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const { - // This immediatly forwards to the make_expanded method. This is done this way so that subcommands can + // This immediately forwards to the make_expanded method. This is done this way so that subcommands can // have overridden formatters if(mode == AppFormatMode::Sub) return make_expanded(app); std::stringstream out; + if((app->get_name().empty()) && (app->get_parent() != nullptr)) { + if(app->get_group() != "Subcommands") { + out << app->get_group() << ':'; + } + } out << make_description(app); out << make_usage(app, name); @@ -140,6 +164,10 @@ inline std::string Formatter::make_subcommands(const App *app, AppFormatMode mod // Make a list in definition order of the groups seen std::vector<std::string> subcmd_groups_seen; for(const App *com : subcommands) { + if(com->get_name().empty()) { + out << make_expanded(com); + continue; + } std::string group_key = com->get_group(); if(!group_key.empty() && std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), [&group_key](std::string a) { @@ -152,8 +180,10 @@ inline std::string Formatter::make_subcommands(const App *app, AppFormatMode mod for(const std::string &group : subcmd_groups_seen) { out << "\n" << group << ":\n"; std::vector<const App *> subcommands_group = app->get_subcommands( - [&group](const App *app) { return detail::to_lower(app->get_group()) == detail::to_lower(group); }); + [&group](const App *sub_app) { return detail::to_lower(sub_app->get_group()) == detail::to_lower(group); }); for(const App *new_com : subcommands_group) { + if(new_com->get_name().empty()) + continue; if(mode != AppFormatMode::All) { out << make_subcommand(new_com); } else { @@ -174,7 +204,7 @@ inline std::string Formatter::make_subcommand(const App *sub) const { inline std::string Formatter::make_expanded(const App *sub) const { std::stringstream out; - out << sub->get_name() << "\n"; + out << sub->get_display_name() << "\n"; out << make_description(sub); out << make_positionals(sub); @@ -237,7 +267,6 @@ inline std::string Formatter::make_option_usage(const Option *opt) const { out << "(" << std::to_string(opt->get_expected()) << "x)"; else if(opt->get_expected() < 0) out << "..."; - return opt->get_required() ? out.str() : "[" + out.str() + "]"; } diff --git a/packages/CLI11/include/CLI/FormatterFwd.hpp b/packages/CLI11/include/CLI/FormatterFwd.hpp index 0344256ca..ebaace645 100644 --- a/packages/CLI11/include/CLI/FormatterFwd.hpp +++ b/packages/CLI11/include/CLI/FormatterFwd.hpp @@ -49,7 +49,9 @@ class FormatterBase { FormatterBase() = default; FormatterBase(const FormatterBase &) = default; FormatterBase(FormatterBase &&) = default; - virtual ~FormatterBase() = default; + + /// Adding a destructor in this form to work around bug in GCC 4.7 + virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) /// This is the key method that puts together help virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; @@ -93,6 +95,9 @@ class FormatterLambda final : public FormatterBase { /// Create a FormatterLambda with a lambda function explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} + /// Adding a destructor (mostly to make GCC 4.7 happy) + ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) + /// This will simply call the lambda function std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { return lambda_(app, name, mode); diff --git a/packages/CLI11/include/CLI/Option.hpp b/packages/CLI11/include/CLI/Option.hpp index c37e39f7b..563ae32dd 100644 --- a/packages/CLI11/include/CLI/Option.hpp +++ b/packages/CLI11/include/CLI/Option.hpp @@ -28,7 +28,7 @@ class App; using Option_p = std::unique_ptr<Option>; -enum class MultiOptionPolicy { Throw, TakeLast, TakeFirst, Join }; +enum class MultiOptionPolicy : char { Throw, TakeLast, TakeFirst, Join }; /// This is the CRTP base class for Option and OptionDefaults. It was designed this way /// to share parts of the class; an OptionDefaults can copy to an Option. @@ -45,9 +45,15 @@ template <typename CRTP> class OptionBase { /// Ignore the case when matching (option, not value) bool ignore_case_{false}; + /// Ignore underscores when matching (option, not value) + bool ignore_underscore_{false}; + /// Allow this option to be given in a configuration file bool configurable_{true}; - + /// Disable overriding flag values with '=value' + bool disable_flag_override_{false}; + /// Specify a delimiter character for vector arguments + char delimiter_{'\0'}; /// Policy for multiple arguments when `expected_ == 1` (can be set on bool flags, too) MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw}; @@ -56,7 +62,10 @@ template <typename CRTP> class OptionBase { other->group(group_); other->required(required_); other->ignore_case(ignore_case_); + other->ignore_underscore(ignore_underscore_); other->configurable(configurable_); + other->disable_flag_override(disable_flag_override_); + other->delimiter(delimiter_); other->multi_option_policy(multi_option_policy_); } @@ -67,7 +76,6 @@ template <typename CRTP> class OptionBase { CRTP *group(std::string name) { group_ = name; return static_cast<CRTP *>(this); - ; } /// Set the option as required @@ -90,9 +98,16 @@ template <typename CRTP> class OptionBase { /// The status of ignore case bool get_ignore_case() const { return ignore_case_; } + /// The status of ignore_underscore + bool get_ignore_underscore() const { return ignore_underscore_; } + /// The status of configurable bool get_configurable() const { return configurable_; } + /// The status of configurable + bool get_disable_flag_override() const { return disable_flag_override_; } + + char get_delimiter() const { return delimiter_; } /// The status of the multi option policy MultiOptionPolicy get_multi_option_policy() const { return multi_option_policy_; } @@ -124,6 +139,12 @@ template <typename CRTP> class OptionBase { configurable_ = value; return static_cast<CRTP *>(this); } + + /// Allow in a configuration file + CRTP *delimiter(char value = '\0') { + delimiter_ = value; + return static_cast<CRTP *>(this); + } }; /// This is a version of OptionBase that only supports setting values, @@ -145,6 +166,24 @@ class OptionDefaults : public OptionBase<OptionDefaults> { ignore_case_ = value; return this; } + + /// Ignore underscores in the option name + OptionDefaults *ignore_underscore(bool value = true) { + ignore_underscore_ = value; + return this; + } + + /// Disable overriding flag values with an '=<value>' segment + OptionDefaults *disable_flag_override(bool value = true) { + disable_flag_override_ = value; + return this; + } + + /// set a delimiter character to split up single arguments to treat as multiple inputs + OptionDefaults *delimiter(char value = '\0') { + delimiter_ = value; + return this; + } }; class Option : public OptionBase<Option> { @@ -160,6 +199,13 @@ class Option : public OptionBase<Option> { /// A list of the long names (`--a`) without the leading dashes std::vector<std::string> lnames_; + /// A list of the flag names with the appropriate default value, the first part of the pair should be duplicates of + /// what is in snames or lnames but will trigger a particular response on a flag + std::vector<std::pair<std::string, std::string>> default_flag_values_; + + /// a list of flag names with specified default values; + std::vector<std::string> fnames_; + /// A positional name std::string pname_; @@ -197,7 +243,7 @@ class Option : public OptionBase<Option> { int expected_{1}; /// A list of validators to run on each value parsed - std::vector<std::function<std::string(std::string &)>> validators_; + std::vector<Validator> validators_; /// A list of options that are required with this option std::set<Option *> needs_; @@ -215,9 +261,6 @@ class Option : public OptionBase<Option> { /// Options store a callback to do all the work callback_t callback_; - /// Options can short-circuit for help options or similar (called before parsing is validated) - bool short_circuit_{false}; - ///@} /// @name Parsing results ///@{ @@ -231,11 +274,14 @@ class Option : public OptionBase<Option> { ///@} /// Making an option by hand is not defined, it must be made by the App class - Option( - std::string name, std::string description, std::function<bool(results_t)> callback, bool defaulted, App *parent) - : description_(std::move(description)), default_(defaulted), parent_(parent), - callback_(callback ? std::move(callback) : [](results_t) { return true; }) { - std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(name)); + Option(std::string option_name, + std::string option_description, + std::function<bool(results_t)> callback, + bool defaulted, + App *parent) + : description_(std::move(option_description)), default_(defaulted), parent_(parent), + callback_(std::move(callback)) { + std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name)); } public: @@ -284,42 +330,70 @@ class Option : public OptionBase<Option> { return this; } - /// Adds a validator with a built in type name - Option *check(const Validator &validator) { - validators_.emplace_back(validator.func); - if(!validator.tname.empty()) - type_name(validator.tname); + /// Adds a Validator with a built in type name + Option *check(Validator validator, std::string validator_name = "") { + validator.non_modifying(); + validators_.push_back(std::move(validator)); + if(!validator_name.empty()) + validators_.front().name(validator_name); + return this; + } + + /// Adds a Validator. Takes a const string& and returns an error message (empty if conversion/check is okay). + Option *check(std::function<std::string(const std::string &)> validator, + std::string validator_description = "", + std::string validator_name = "") { + validators_.emplace_back(validator, std::move(validator_description), std::move(validator_name)); + validators_.back().non_modifying(); return this; } - /// Adds a validator. Takes a const string& and returns an error message (empty if conversion/check is okay). - Option *check(std::function<std::string(const std::string &)> validator) { - validators_.emplace_back(validator); + /// Adds a transforming validator with a built in type name + Option *transform(Validator validator, std::string validator_name = "") { + validators_.insert(validators_.begin(), std::move(validator)); + if(!validator_name.empty()) + validators_.front().name(validator_name); return this; } /// Adds a validator-like function that can change result - Option *transform(std::function<std::string(std::string)> func) { - validators_.emplace_back([func](std::string &inout) { - try { - inout = func(inout); - } catch(const ValidationError &e) { - return std::string(e.what()); - } - return std::string(); - }); + Option *transform(std::function<std::string(std::string)> func, + std::string transform_description = "", + std::string transform_name = "") { + validators_.insert(validators_.begin(), + Validator( + [func](std::string &val) { + val = func(val); + return std::string{}; + }, + std::move(transform_description), + std::move(transform_name))); + return this; } /// Adds a user supplied function to run on each item passed in (communicate though lambda capture) Option *each(std::function<void(std::string)> func) { - validators_.emplace_back([func](std::string &inout) { - func(inout); - return std::string(); - }); + validators_.emplace_back( + [func](std::string &inout) { + func(inout); + return std::string{}; + }, + std::string{}); return this; } - + /// Get a named Validator + Validator *get_validator(const std::string &validator_name = "") { + for(auto &validator : validators_) { + if(validator_name == validator.get_name()) { + return &validator; + } + } + if((validator_name.empty()) && (!validators_.empty())) { + return &(validators_.front()); + } + throw OptionNotFound(std::string("Validator ") + validator_name + " Not Found"); + } /// Sets required options Option *needs(Option *opt) { auto tup = needs_.insert(opt); @@ -414,6 +488,20 @@ class Option : public OptionBase<Option> { return this; } + /// Ignore underscores in the option names + /// + /// The template hides the fact that we don't have the definition of App yet. + /// You are never expected to add an argument to the template here. + template <typename T = App> Option *ignore_underscore(bool value = true) { + ignore_underscore_ = value; + auto *parent = dynamic_cast<T *>(parent_); + for(const Option_p &opt : parent->options_) + if(opt.get() != this && *opt == *this) + throw OptionAlreadyAdded(opt->get_name(true, true)); + + return this; + } + /// Take the last argument if given multiple times (or another policy) Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) { @@ -423,14 +511,11 @@ class Option : public OptionBase<Option> { return this; } - /// Options with a short circuit set will run this function before parsing is finished. - /// - /// This is set on help functions, for example, to escape the normal validation. - Option *short_circuit(bool value = true) { - short_circuit_ = value; + /// disable flag overrides + Option *disable_flag_override(bool value = true) { + disable_flag_override_ = value; return this; } - ///@} /// @name Accessors ///@{ @@ -450,9 +535,6 @@ class Option : public OptionBase<Option> { /// The default value (for help printing) std::string get_defaultval() const { return defaultval_; } - /// See if this is supposed to short circuit (skip validation, INI, etc) (Used for help flags) - bool get_short_circuit() const { return short_circuit_; } - /// Get the callback function callback_t get_callback() const { return callback_; } @@ -462,11 +544,14 @@ class Option : public OptionBase<Option> { /// Get the short names const std::vector<std::string> get_snames() const { return snames_; } + /// get the flag names with specified default values + const std::vector<std::string> get_fnames() const { return fnames_; } + /// The number of times the option expects to be included int get_expected() const { return expected_; } /// \brief The total number of expected values (including the type) - /// This is positive if exactly this number is expected, and negitive for at least N values + /// This is positive if exactly this number is expected, and negative for at least N values /// /// v = fabs(size_type*expected) /// !MultiOptionPolicy::Throw @@ -501,11 +586,17 @@ class Option : public OptionBase<Option> { /// Get the description const std::string &get_description() const { return description_; } + /// Set the description + Option *description(std::string option_description) { + description_ = std::move(option_description); + return this; + } + ///@} /// @name Help tools ///@{ - /// \brief Gets a comma seperated list of names. + /// \brief Gets a comma separated list of names. /// Will include / prefer the positional name if positional is true. /// If all_options is false, pick just the most descriptive name to show. /// Use `get_name(true)` to get the positional name (replaces `get_pname`) @@ -517,15 +608,30 @@ class Option : public OptionBase<Option> { std::vector<std::string> name_list; - /// The all list wil never include a positional unless asked or that's the only name. + /// The all list will never include a positional unless asked or that's the only name. if((positional && pname_.length()) || (snames_.empty() && lnames_.empty())) name_list.push_back(pname_); + if((get_items_expected() == 0) && (!fnames_.empty())) { + for(const std::string &sname : snames_) { + name_list.push_back("-" + sname); + if(check_fname(sname)) { + name_list.back() += "{" + get_flag_value(sname, "") + "}"; + } + } - for(const std::string &sname : snames_) - name_list.push_back("-" + sname); + for(const std::string &lname : lnames_) { + name_list.push_back("--" + lname); + if(check_fname(lname)) { + name_list.back() += "{" + get_flag_value(lname, "") + "}"; + } + } + } else { + for(const std::string &sname : snames_) + name_list.push_back("-" + sname); - for(const std::string &lname : lnames_) - name_list.push_back("--" + lname); + for(const std::string &lname : lnames_) + name_list.push_back("--" + lname); + } return detail::join(name_list); @@ -556,16 +662,19 @@ class Option : public OptionBase<Option> { /// Process the callback void run_callback() { + callback_run_ = true; + // Run the validators (can change the string) if(!validators_.empty()) { - for(std::string &result : results_) - for(const std::function<std::string(std::string &)> &vali : validators_) { - std::string err_msg = vali(result); - if(!err_msg.empty()) - throw ValidationError(get_name(), err_msg); - } + for(std::string &result : results_) { + auto err_msg = _validate(result); + if(!err_msg.empty()) + throw ValidationError(get_name(), err_msg); + } + } + if(!(callback_)) { + return; } - bool local_result; // Num items expected or length of vector, always at least 1 @@ -614,25 +723,32 @@ class Option : public OptionBase<Option> { for(const std::string &lname : lnames_) if(other.check_lname(lname)) return true; - // We need to do the inverse, just in case we are ignore_case - for(const std::string &sname : other.snames_) - if(check_sname(sname)) - return true; - for(const std::string &lname : other.lnames_) - if(check_lname(lname)) - return true; + + if(ignore_case_ || + ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore + for(const std::string &sname : other.snames_) + if(check_sname(sname)) + return true; + for(const std::string &lname : other.lnames_) + if(check_lname(lname)) + return true; + } return false; } /// Check a name. Requires "-" or "--" for short / long, supports positional name bool check_name(std::string name) const { - if(name.length() > 2 && name.substr(0, 2) == "--") + if(name.length() > 2 && name[0] == '-' && name[1] == '-') return check_lname(name.substr(2)); - else if(name.length() > 1 && name.substr(0, 1) == "-") + else if(name.length() > 1 && name.front() == '-') return check_sname(name.substr(1)); else { std::string local_pname = pname_; + if(ignore_underscore_) { + local_pname = detail::remove_underscore(local_pname); + name = detail::remove_underscore(name); + } if(ignore_case_) { local_pname = detail::to_lower(local_pname); name = detail::to_lower(name); @@ -642,37 +758,78 @@ class Option : public OptionBase<Option> { } /// Requires "-" to be removed from string - bool check_sname(std::string name) const { - if(ignore_case_) { - name = detail::to_lower(name); - return std::find_if(std::begin(snames_), std::end(snames_), [&name](std::string local_sname) { - return detail::to_lower(local_sname) == name; - }) != std::end(snames_); - } else - return std::find(std::begin(snames_), std::end(snames_), name) != std::end(snames_); - } + bool check_sname(std::string name) const { return (detail::find_member(name, snames_, ignore_case_) >= 0); } /// Requires "--" to be removed from string bool check_lname(std::string name) const { - if(ignore_case_) { - name = detail::to_lower(name); - return std::find_if(std::begin(lnames_), std::end(lnames_), [&name](std::string local_sname) { - return detail::to_lower(local_sname) == name; - }) != std::end(lnames_); - } else - return std::find(std::begin(lnames_), std::end(lnames_), name) != std::end(lnames_); + return (detail::find_member(name, lnames_, ignore_case_, ignore_underscore_) >= 0); + } + + /// Requires "--" to be removed from string + bool check_fname(std::string name) const { + if(fnames_.empty()) { + return false; + } + return (detail::find_member(name, fnames_, ignore_case_, ignore_underscore_) >= 0); + } + + std::string get_flag_value(std::string name, std::string input_value) const { + static const std::string trueString{"true"}; + static const std::string falseString{"false"}; + static const std::string emptyString{"{}"}; + // check for disable flag override_ + if(disable_flag_override_) { + if(!((input_value.empty()) || (input_value == emptyString))) { + auto default_ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_); + if(default_ind >= 0) { + if(default_flag_values_[default_ind].second != input_value) { + throw(ArgumentMismatch::FlagOverride(name)); + } + } else { + if(input_value != trueString) { + throw(ArgumentMismatch::FlagOverride(name)); + } + } + } + } + auto ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_); + if((input_value.empty()) || (input_value == emptyString)) { + return (ind < 0) ? trueString : default_flag_values_[ind].second; + } + if(ind < 0) { + return input_value; + } + if(default_flag_values_[ind].second == falseString) { + try { + auto val = detail::to_flag_value(input_value); + return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val)); + } catch(const std::invalid_argument &) { + return input_value; + } + } else { + return input_value; + } } /// Puts a result at the end Option *add_result(std::string s) { - results_.push_back(s); + _add_result(std::move(s)); + callback_run_ = false; + return this; + } + + /// Puts a result at the end and get a count of the number of arguments actually added + Option *add_result(std::string s, int &results_added) { + results_added = _add_result(std::move(s)); callback_run_ = false; return this; } - /// Set the results vector all at once - Option *set_results(std::vector<std::string> results) { - results_ = results; + /// Puts a result at the end + Option *add_result(std::vector<std::string> s) { + for(auto &str : s) { + _add_result(std::move(str)); + } callback_run_ = false; return this; } @@ -680,6 +837,57 @@ class Option : public OptionBase<Option> { /// Get a copy of the results std::vector<std::string> results() const { return results_; } + /// get the results as a particular type + template <typename T, + enable_if_t<!is_vector<T>::value && !std::is_const<T>::value, detail::enabler> = detail::dummy> + void results(T &output) const { + bool retval; + if(results_.empty()) { + retval = detail::lexical_cast(defaultval_, output); + } else if(results_.size() == 1) { + retval = detail::lexical_cast(results_[0], output); + } else { + switch(multi_option_policy_) { + case MultiOptionPolicy::TakeFirst: + retval = detail::lexical_cast(results_.front(), output); + break; + case MultiOptionPolicy::TakeLast: + default: + retval = detail::lexical_cast(results_.back(), output); + break; + case MultiOptionPolicy::Throw: + throw ConversionError(get_name(), results_); + case MultiOptionPolicy::Join: + retval = detail::lexical_cast(detail::join(results_), output); + break; + } + } + if(!retval) { + throw ConversionError(get_name(), results_); + } + } + /// get the results as a vector of a particular type + template <typename T> void results(std::vector<T> &output) const { + output.clear(); + bool retval = true; + + for(const auto &elem : results_) { + output.emplace_back(); + retval &= detail::lexical_cast(elem, output.back()); + } + + if(!retval) { + throw ConversionError(get_name(), results_); + } + } + + /// return the results as a particular type + template <typename T> T as() const { + T output; + results(output); + return output; + } + /// See if the callback has been run already bool get_callback_run() const { return callback_run_; } @@ -699,16 +907,12 @@ class Option : public OptionBase<Option> { return this; } - /// Provided for backward compatibility \deprecated - CLI11_DEPRECATED("Please use type_name instead") - Option *set_type_name(std::string typeval) { return type_name(typeval); } - /// Set a custom option size - Option *type_size(int type_size) { - type_size_ = type_size; + Option *type_size(int option_type_size) { + type_size_ = option_type_size; if(type_size_ == 0) required_ = false; - if(type_size < 0) + if(option_type_size < 0) expected_ = -1; return this; } @@ -729,8 +933,56 @@ class Option : public OptionBase<Option> { return this; } - /// Get the typename for this option - std::string get_type_name() const { return type_name_(); } + /// Get the full typename for this option + std::string get_type_name() const { + std::string full_type_name = type_name_(); + if(!validators_.empty()) { + for(auto &validator : validators_) { + std::string vtype = validator.get_description(); + if(!vtype.empty()) { + full_type_name += ":" + vtype; + } + } + } + return full_type_name; + } + + private: + // run through the validators + std::string _validate(std::string &result) { + std::string err_msg; + for(const auto &vali : validators_) { + try { + err_msg = vali(result); + } catch(const ValidationError &err) { + err_msg = err.what(); + } + if(!err_msg.empty()) + break; + } + return err_msg; + } + + int _add_result(std::string &&result) { + int result_count = 0; + if(delimiter_ == '\0') { + results_.push_back(std::move(result)); + ++result_count; + } else { + if((result.find_first_of(delimiter_) != std::string::npos)) { + for(const auto &var : CLI::detail::split(result, delimiter_)) { + if(!var.empty()) { + results_.push_back(var); + ++result_count; + } + } + } else { + results_.push_back(std::move(result)); + ++result_count; + } + } + return result_count; + } }; } // namespace CLI diff --git a/packages/CLI11/include/CLI/Optional.hpp b/packages/CLI11/include/CLI/Optional.hpp index d4b7df28d..3fbc43a62 100644 --- a/packages/CLI11/include/CLI/Optional.hpp +++ b/packages/CLI11/include/CLI/Optional.hpp @@ -15,19 +15,25 @@ #if defined(CLI11_CPP17) && __has_include(<optional>) && \ !defined(CLI11_STD_OPTIONAL) #define CLI11_STD_OPTIONAL 1 +#elif !defined(CLI11_STD_OPTIONAL) +#define CLI11_STD_OPTIONAL 0 #endif #if defined(CLI11_CPP14) && __has_include(<experimental/optional>) && \ !defined(CLI11_EXPERIMENTAL_OPTIONAL) \ && (!defined(CLI11_STD_OPTIONAL) || CLI11_STD_OPTIONAL == 0) #define CLI11_EXPERIMENTAL_OPTIONAL 1 +#elif !defined(CLI11_EXPERIMENTAL_OPTIONAL) +#define CLI11_EXPERIMENTAL_OPTIONAL 0 #endif #if __has_include(<boost/optional.hpp>) && !defined(CLI11_BOOST_OPTIONAL) #include <boost/version.hpp> -#if BOOST_VERSION >= 105800 +#if BOOST_VERSION >= 106100 #define CLI11_BOOST_OPTIONAL 1 #endif +#elif !defined(CLI11_BOOST_OPTIONAL) +#define CLI11_BOOST_OPTIONAL 0 #endif #endif diff --git a/packages/CLI11/include/CLI/Split.hpp b/packages/CLI11/include/CLI/Split.hpp index 8a1666cbe..3c3b05230 100644 --- a/packages/CLI11/include/CLI/Split.hpp +++ b/packages/CLI11/include/CLI/Split.hpp @@ -26,7 +26,7 @@ inline bool split_short(const std::string ¤t, std::string &name, std::stri // Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true inline bool split_long(const std::string ¤t, std::string &name, std::string &value) { if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { - auto loc = current.find("="); + auto loc = current.find_first_of('='); if(loc != std::string::npos) { name = current.substr(2, loc - 2); value = current.substr(loc + 1); @@ -39,6 +39,22 @@ inline bool split_long(const std::string ¤t, std::string &name, std::strin return false; } +// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true +inline bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { + if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { + auto loc = current.find_first_of(':'); + if(loc != std::string::npos) { + name = current.substr(1, loc - 1); + value = current.substr(loc + 1); + } else { + name = current.substr(1); + value = ""; + } + return true; + } else + return false; +} + // Splits a string into multiple long and short names inline std::vector<std::string> split_names(std::string current) { std::vector<std::string> output; @@ -51,6 +67,33 @@ inline std::vector<std::string> split_names(std::string current) { return output; } +/// extract default flag values either {def} or starting with a ! +inline std::vector<std::pair<std::string, std::string>> get_default_flag_values(const std::string &str) { + std::vector<std::string> flags = split_names(str); + flags.erase(std::remove_if(flags.begin(), + flags.end(), + [](const std::string &name) { + return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && + (name.back() == '}')) || + (name[0] == '!')))); + }), + flags.end()); + std::vector<std::pair<std::string, std::string>> output; + output.reserve(flags.size()); + for(auto &flag : flags) { + auto def_start = flag.find_first_of('{'); + std::string defval = "false"; + if((def_start != std::string::npos) && (flag.back() == '}')) { + defval = flag.substr(def_start + 1); + defval.pop_back(); + flag.erase(def_start, std::string::npos); + } + flag.erase(0, flag.find_first_not_of("-!")); + output.emplace_back(flag, defval); + } + return output; +} + /// Get a vector of short names, one of long names, and a single name inline std::tuple<std::vector<std::string>, std::vector<std::string>, std::string> get_names(const std::vector<std::string> &input) { diff --git a/packages/CLI11/include/CLI/StringTools.hpp b/packages/CLI11/include/CLI/StringTools.hpp index 1b497837d..a680b5a16 100644 --- a/packages/CLI11/include/CLI/StringTools.hpp +++ b/packages/CLI11/include/CLI/StringTools.hpp @@ -7,11 +7,37 @@ #include <iomanip> #include <locale> #include <sstream> +#include <stdexcept> #include <string> #include <type_traits> #include <vector> namespace CLI { + +/// Include the items in this namespace to get free conversion of enums to/from streams. +/// (This is available inside CLI as well, so CLI11 will use this without a using statement). +namespace enums { + +/// output streaming for enumerations +template <typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type> +std::ostream &operator<<(std::ostream &in, const T &item) { + // make sure this is out of the detail namespace otherwise it won't be found when needed + return in << static_cast<typename std::underlying_type<T>::type>(item); +} + +/// input streaming for enumerations +template <typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type> +std::istream &operator>>(std::istream &in, T &item) { + typename std::underlying_type<T>::type i; + in >> i; + item = static_cast<T>(i); + return in; +} +} // namespace enums + +/// Export to CLI namespace +using namespace enums; + namespace detail { // Based on http://stackoverflow.com/questions/236129/split-a-string-in-c @@ -20,7 +46,7 @@ inline std::vector<std::string> split(const std::string &s, char delim) { std::vector<std::string> elems; // Check to see if empty string, give consistent result if(s.empty()) - elems.emplace_back(""); + elems.emplace_back(); else { std::stringstream ss; ss.str(s); @@ -31,15 +57,43 @@ inline std::vector<std::string> split(const std::string &s, char delim) { } return elems; } +/// simple utility to convert various types to a string +template <typename T> inline std::string as_string(const T &v) { + std::ostringstream s; + s << v; + return s.str(); +} +// if the data type is already a string just forward it +template <typename T, typename = typename std::enable_if<std::is_constructible<std::string, T>::value>::type> +inline auto as_string(T &&v) -> decltype(std::forward<T>(v)) { + return std::forward<T>(v); +} /// Simple function to join a string template <typename T> std::string join(const T &v, std::string delim = ",") { std::ostringstream s; - size_t start = 0; - for(const auto &i : v) { - if(start++ > 0) - s << delim; - s << i; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << *beg++; + while(beg != end) { + s << delim << *beg++; + } + return s.str(); +} + +/// Simple function to join a string from processed elements +template <typename T, + typename Callable, + typename = typename std::enable_if<!std::is_constructible<std::string, Callable>::value>::type> +std::string join(const T &v, Callable func, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << func(*beg++); + while(beg != end) { + s << delim << func(*beg++); } return s.str(); } @@ -117,12 +171,12 @@ inline std::ostream &format_help(std::ostream &out, std::string name, std::strin } /// Verify the first character of an option -template <typename T> bool valid_first_char(T c) { return std::isalpha(c, std::locale()) || c == '_'; } +template <typename T> bool valid_first_char(T c) { + return std::isalnum(c, std::locale()) || c == '_' || c == '?' || c == '@'; +} /// Verify following characters of an option -template <typename T> bool valid_later_char(T c) { - return std::isalnum(c, std::locale()) || c == '_' || c == '.' || c == '-'; -} +template <typename T> bool valid_later_char(T c) { return valid_first_char(c) || c == '.' || c == '-'; } /// Verify an option name inline bool valid_name_string(const std::string &str) { @@ -142,27 +196,101 @@ inline std::string to_lower(std::string str) { return str; } +/// remove underscores from a string +inline std::string remove_underscore(std::string str) { + str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); + return str; +} + +/// Find and replace a substring with another substring +inline std::string find_and_replace(std::string str, std::string from, std::string to) { + + size_t start_pos = 0; + + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; +} + +/// check if the flag definitions has possible false flags +inline bool has_default_flag_values(const std::string &flags) { + return (flags.find_first_of("{!") != std::string::npos); +} + +inline void remove_default_flag_values(std::string &flags) { + size_t loc = flags.find_first_of('{'); + while(loc != std::string::npos) { + auto finish = flags.find_first_of("},", loc + 1); + if((finish != std::string::npos) && (flags[finish] == '}')) { + flags.erase(flags.begin() + loc, flags.begin() + finish + 1); + } + loc = flags.find_first_of('{', loc + 1); + } + flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); +} + +/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores +inline std::ptrdiff_t find_member(std::string name, + const std::vector<std::string> names, + bool ignore_case = false, + bool ignore_underscore = false) { + auto it = std::end(names); + if(ignore_case) { + if(ignore_underscore) { + name = detail::to_lower(detail::remove_underscore(name)); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(detail::remove_underscore(local_name)) == name; + }); + } else { + name = detail::to_lower(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(local_name) == name; + }); + } + + } else if(ignore_underscore) { + name = detail::remove_underscore(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::remove_underscore(local_name) == name; + }); + } else + it = std::find(std::begin(names), std::end(names), name); + + return (it != std::end(names)) ? (it - std::begin(names)) : (-1); +} + +/// Find a trigger string and call a modify callable function that takes the current string and starting position of the +/// trigger and returns the position in the string to search for the next trigger string +template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { + size_t start_pos = 0; + while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { + start_pos = modify(str, start_pos); + } + return str; +} + /// Split a string '"one two" "three"' into 'one two', 'three' +/// Quote characters can be ` ' or " inline std::vector<std::string> split_up(std::string str) { - std::vector<char> delims = {'\'', '\"'}; + const std::string delims("\'\"`"); auto find_ws = [](char ch) { return std::isspace<char>(ch, std::locale()); }; trim(str); std::vector<std::string> output; - + bool embeddedQuote = false; + char keyChar = ' '; while(!str.empty()) { - if(str[0] == '\'') { - auto end = str.find('\'', 1); - if(end != std::string::npos) { - output.push_back(str.substr(1, end - 1)); - str = str.substr(end + 1); - } else { - output.push_back(str.substr(1)); - str = ""; + if(delims.find_first_of(str[0]) != std::string::npos) { + keyChar = str[0]; + auto end = str.find_first_of(keyChar, 1); + while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes + end = str.find_first_of(keyChar, end + 1); + embeddedQuote = true; } - } else if(str[0] == '\"') { - auto end = str.find('\"', 1); if(end != std::string::npos) { output.push_back(str.substr(1, end - 1)); str = str.substr(end + 1); @@ -170,7 +298,6 @@ inline std::vector<std::string> split_up(std::string str) { output.push_back(str.substr(1)); str = ""; } - } else { auto it = std::find_if(std::begin(str), std::end(str), find_ws); if(it != std::end(str)) { @@ -182,9 +309,13 @@ inline std::vector<std::string> split_up(std::string str) { str = ""; } } + // transform any embedded quotes into the regular character + if(embeddedQuote) { + output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); + embeddedQuote = false; + } trim(str); } - return output; } @@ -204,18 +335,34 @@ inline std::string fix_newlines(std::string leader, std::string input) { return input; } -/// Find and replace a subtring with another substring -inline std::string find_and_replace(std::string str, std::string from, std::string to) { - - size_t start_pos = 0; - - while((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); +/// This function detects an equal or colon followed by an escaped quote after an argument +/// then modifies the string to replace the equality with a space. This is needed +/// to allow the split up function to work properly and is intended to be used with the find_and_modify function +/// the return value is the offset+1 which is required by the find_and_modify function. +inline size_t escape_detect(std::string &str, size_t offset) { + auto next = str[offset + 1]; + if((next == '\"') || (next == '\'') || (next == '`')) { + auto astart = str.find_last_of("-/ \"\'`", offset - 1); + if(astart != std::string::npos) { + if(str[astart] == ((str[offset] == '=') ? '-' : '/')) + str[offset] = ' '; // interpret this as a space so the split_up works properly + } } + return offset + 1; +} +/// Add quotes if the string contains spaces +inline std::string &add_quotes_if_needed(std::string &str) { + if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) { + char quote = str.find('"') < str.find('\'') ? '\'' : '"'; + if(str.find(' ') != std::string::npos) { + str.insert(0, 1, quote); + str.append(1, quote); + } + } return str; } } // namespace detail + } // namespace CLI diff --git a/packages/CLI11/include/CLI/Timer.hpp b/packages/CLI11/include/CLI/Timer.hpp index 62ec84f0e..61039c2cd 100644 --- a/packages/CLI11/include/CLI/Timer.hpp +++ b/packages/CLI11/include/CLI/Timer.hpp @@ -119,7 +119,7 @@ class AutoTimer : public Timer { AutoTimer(std::string title = "Timer", time_print_t time_print = Simple) : Timer(title, time_print) {} // GCC 4.7 does not support using inheriting constructors. - /// This desctructor prints the string + /// This destructor prints the string ~AutoTimer() { std::cout << to_string() << std::endl; } }; diff --git a/packages/CLI11/include/CLI/TypeTools.hpp b/packages/CLI11/include/CLI/TypeTools.hpp index d7c3d306f..ca0100605 100644 --- a/packages/CLI11/include/CLI/TypeTools.hpp +++ b/packages/CLI11/include/CLI/TypeTools.hpp @@ -3,7 +3,9 @@ // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/CLIUtils/CLI11 for details. +#include "StringTools.hpp" #include <exception> +#include <memory> #include <string> #include <type_traits> #include <vector> @@ -12,33 +14,115 @@ namespace CLI { // Type tools +// Utilities for type enabling +namespace detail { +// Based generally on https://rmf.io/cxx11/almost-static-if +/// Simple empty scoped class +enum class enabler {}; + +/// An instance to use in EnableIf +constexpr enabler dummy = {}; +} // namespace detail + /// A copy of enable_if_t from C++14, compatible with C++11. /// /// We could check to see if C++14 is being used, but it does not hurt to redefine this /// (even Google does this: https://github.com/google/skia/blob/master/include/private/SkTLogic.h) /// It is not in the std namespace anyway, so no harm done. - template <bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type; +/// A copy of std::void_t from C++17 (helper for C++11 and C++14) +template <typename... Ts> struct make_void { using type = void; }; + +/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine +template <typename... Ts> using void_t = typename make_void<Ts...>::type; + +/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine +template <bool B, class T, class F> using conditional_t = typename std::conditional<B, T, F>::type; + /// Check to see if something is a vector (fail check by default) -template <typename T> struct is_vector { static const bool value = false; }; +template <typename T> struct is_vector : std::false_type {}; /// Check to see if something is a vector (true if actually a vector) -template <class T, class A> struct is_vector<std::vector<T, A>> { static bool const value = true; }; +template <class T, class A> struct is_vector<std::vector<T, A>> : std::true_type {}; /// Check to see if something is bool (fail check by default) -template <typename T> struct is_bool { static const bool value = false; }; +template <typename T> struct is_bool : std::false_type {}; /// Check to see if something is bool (true if actually a bool) -template <> struct is_bool<bool> { static bool const value = true; }; +template <> struct is_bool<bool> : std::true_type {}; + +/// Check to see if something is a shared pointer +template <typename T> struct is_shared_ptr : std::false_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template <typename T> struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template <typename T> struct is_shared_ptr<const std::shared_ptr<T>> : std::true_type {}; + +/// Check to see if something is copyable pointer +template <typename T> struct is_copyable_ptr { + static bool const value = is_shared_ptr<T>::value || std::is_pointer<T>::value; +}; + +/// This can be specialized to override the type deduction for IsMember. +template <typename T> struct IsMemberType { using type = T; }; + +/// The main custom type needed here is const char * should be a string. +template <> struct IsMemberType<const char *> { using type = std::string; }; namespace detail { -// Based generally on https://rmf.io/cxx11/almost-static-if -/// Simple empty scoped class -enum class enabler {}; -/// An instance to use in EnableIf -constexpr enabler dummy = {}; +// These are utilities for IsMember + +/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that +/// pointer_traits<T> be valid. +template <typename T> struct element_type { + using type = + typename std::conditional<is_copyable_ptr<T>::value, typename std::pointer_traits<T>::element_type, T>::type; +}; + +/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of +/// the container +template <typename T> struct element_value_type { using type = typename element_type<T>::type::value_type; }; + +/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. +template <typename T, typename _ = void> struct pair_adaptor : std::false_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const<value_type>::type; + using second_type = typename std::remove_const<value_type>::type; + + /// Get the first value (really just the underlying value) + template <typename Q> static auto first(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) { + return std::forward<Q>(pair_value); + } + /// Get the second value (really just the underlying value) + template <typename Q> static auto second(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) { + return std::forward<Q>(pair_value); + } +}; + +/// Adaptor for map-like structure (true version, must have key_type and mapped_type). +/// This wraps a mapped container in a few utilities access it in a general way. +template <typename T> +struct pair_adaptor< + T, + conditional_t<false, void_t<typename T::value_type::first_type, typename T::value_type::second_type>, void>> + : std::true_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const<typename value_type::first_type>::type; + using second_type = typename std::remove_const<typename value_type::second_type>::type; + + /// Get the first value (really just the underlying value) + template <typename Q> static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward<Q>(pair_value))) { + return std::get<0>(std::forward<Q>(pair_value)); + } + /// Get the second value (really just the underlying value) + template <typename Q> static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward<Q>(pair_value))) { + return std::get<1>(std::forward<Q>(pair_value)); + } +}; // Type name print @@ -68,9 +152,16 @@ template <typename T, enable_if_t<is_vector<T>::value, detail::enabler> = detail constexpr const char *type_name() { return "VECTOR"; } +/// Print name for enumeration types +template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "ENUM"; +} +/// Print for all other types template <typename T, - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value && !is_vector<T>::value, + enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value && !is_vector<T>::value && + !std::is_enum<T>::value, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "TEXT"; @@ -78,9 +169,62 @@ constexpr const char *type_name() { // Lexical cast -/// Signed integers / enums -template <typename T, - enable_if_t<(std::is_integral<T>::value && std::is_signed<T>::value), detail::enabler> = detail::dummy> +/// Convert a flag into an integer value typically binary flags +inline int64_t to_flag_value(std::string val) { + static const std::string trueString("true"); + static const std::string falseString("false"); + if(val == trueString) { + return 1; + } + if(val == falseString) { + return -1; + } + val = detail::to_lower(val); + int64_t ret; + if(val.size() == 1) { + switch(val[0]) { + case '0': + case 'f': + case 'n': + case '-': + ret = -1; + break; + case '1': + case 't': + case 'y': + case '+': + ret = 1; + break; + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + ret = val[0] - '0'; + break; + default: + throw std::invalid_argument("unrecognized character"); + } + return ret; + } + if(val == trueString || val == "on" || val == "yes" || val == "enable") { + ret = 1; + } else if(val == falseString || val == "off" || val == "no" || val == "disable") { + ret = -1; + } else { + ret = std::stoll(val); + } + return ret; +} + +/// Signed integers +template < + typename T, + enable_if_t<std::is_integral<T>::value && std::is_signed<T>::value && !is_bool<T>::value && !std::is_enum<T>::value, + detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { size_t n = 0; @@ -96,7 +240,8 @@ bool lexical_cast(std::string input, T &output) { /// Unsigned integers template <typename T, - enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value, detail::enabler> = detail::dummy> + enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value && !is_bool<T>::value, detail::enabler> = + detail::dummy> bool lexical_cast(std::string input, T &output) { if(!input.empty() && input.front() == '-') return false; // std::stoull happily converts negative values to junk without any errors. @@ -113,6 +258,18 @@ bool lexical_cast(std::string input, T &output) { } } +/// Boolean values +template <typename T, enable_if_t<is_bool<T>::value, detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + try { + auto out = to_flag_value(input); + output = (out > 0); + return true; + } catch(const std::invalid_argument &) { + return false; + } +} + /// Floats template <typename T, enable_if_t<std::is_floating_point<T>::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { @@ -137,10 +294,22 @@ bool lexical_cast(std::string input, T &output) { return true; } +/// Enumerations +template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + typename std::underlying_type<T>::type val; + bool retval = detail::lexical_cast(input, val); + if(!retval) { + return false; + } + output = static_cast<T>(val); + return true; +} + /// Non-string parsable template <typename T, enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value && - !std::is_assignable<T &, std::string>::value, + !std::is_assignable<T &, std::string>::value && !std::is_enum<T>::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { std::istringstream is; @@ -150,5 +319,33 @@ bool lexical_cast(std::string input, T &output) { return !is.fail() && !is.rdbuf()->in_avail(); } +/// Sum a vector of flag representations +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is by +/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most +/// common true and false strings then uses stoll to convert the rest for summing +template <typename T, + enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector<std::string> &flags, T &output) { + int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = (count > 0) ? static_cast<T>(count) : T{0}; +} + +/// Sum a vector of flag representations +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is by +/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most +/// common true and false strings then uses stoll to convert the rest for summing +template <typename T, + enable_if_t<std::is_integral<T>::value && std::is_signed<T>::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector<std::string> &flags, T &output) { + int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = static_cast<T>(count); +} + } // namespace detail } // namespace CLI diff --git a/packages/CLI11/include/CLI/Validators.hpp b/packages/CLI11/include/CLI/Validators.hpp index 91c5d9bc8..54294743e 100644 --- a/packages/CLI11/include/CLI/Validators.hpp +++ b/packages/CLI11/include/CLI/Validators.hpp @@ -3,10 +3,12 @@ // Distributed under the 3-Clause BSD License. See accompanying // file LICENSE or https://github.com/CLIUtils/CLI11 for details. +#include "CLI/StringTools.hpp" #include "CLI/TypeTools.hpp" #include <functional> #include <iostream> +#include <memory> #include <string> // C standard library @@ -17,6 +19,8 @@ namespace CLI { +class Option; + /// @defgroup validator_group Validators /// @brief Some validators that are provided @@ -27,69 +31,193 @@ namespace CLI { /// @{ /// -struct Validator { - /// This is the type name, if empty the type name will not be changed - std::string tname; +class Validator { + protected: + /// This is the description function, if empty the description_ will be used + std::function<std::string()> desc_function_{[]() { return std::string{}; }}; /// This it the base function that is to be called. /// Returns a string error message if validation fails. - std::function<std::string(const std::string &)> func; + std::function<std::string(std::string &)> func_{[](std::string &) { return std::string{}; }}; + /// The name for search purposes of the Validator + std::string name_; + /// Enable for Validator to allow it to be disabled if need be + bool active_{true}; + /// specify that a validator should not modify the input + bool non_modifying_{false}; + + public: + Validator() = default; + /// Construct a Validator with just the description string + explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} + // Construct Validator from basic information + Validator(std::function<std::string(std::string &)> op, std::string validator_desc, std::string validator_name = "") + : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), + name_(std::move(validator_name)) {} + /// Set the Validator operation function + Validator &operation(std::function<std::string(std::string &)> op) { + func_ = std::move(op); + return *this; + } + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(std::string &str) const { + std::string retstring; + if(active_) { + if(non_modifying_) { + std::string value = str; + retstring = func_(value); + } else { + retstring = func_(str); + } + } + return retstring; + }; - /// This is the required operator for a validator - provided to help + /// This is the required operator for a Validator - provided to help /// users (CLI11 uses the member `func` directly) - std::string operator()(const std::string &str) const { return func(str); }; + std::string operator()(const std::string &str) const { + std::string value = str; + return (active_) ? func_(value) : std::string{}; + }; + + /// Specify the type string + Validator &description(std::string validator_desc) { + desc_function_ = [validator_desc]() { return validator_desc; }; + return *this; + } + /// Generate type description information for the Validator + std::string get_description() const { + if(active_) { + return desc_function_(); + } + return std::string{}; + } + /// Specify the type string + Validator &name(std::string validator_name) { + name_ = std::move(validator_name); + return *this; + } + /// Get the name of the Validator + const std::string &get_name() const { return name_; } + /// Specify whether the Validator is active or not + Validator &active(bool active_val = true) { + active_ = active_val; + return *this; + } - /// Combining validators is a new validator + /// Specify whether the Validator can be modifying or not + Validator &non_modifying(bool no_modify = true) { + non_modifying_ = no_modify; + return *this; + } + + /// Get a boolean if the validator is active + bool get_active() const { return active_; } + + /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input + bool get_modifying() const { return !non_modifying_; } + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. Validator operator&(const Validator &other) const { Validator newval; - newval.tname = (tname == other.tname ? tname : ""); + + newval._merge_description(*this, other, " AND "); // Give references (will make a copy in lambda function) - const std::function<std::string(const std::string &filename)> &f1 = func; - const std::function<std::string(const std::string &filename)> &f2 = other.func; + const std::function<std::string(std::string & filename)> &f1 = func_; + const std::function<std::string(std::string & filename)> &f2 = other.func_; - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); if(!s1.empty() && !s2.empty()) - return s1 + " & " + s2; + return std::string("(") + s1 + ") AND (" + s2 + ")"; else return s1 + s2; }; + + newval.active_ = (active_ & other.active_); return newval; } - /// Combining validators is a new validator + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. Validator operator|(const Validator &other) const { Validator newval; - newval.tname = (tname == other.tname ? tname : ""); + + newval._merge_description(*this, other, " OR "); // Give references (will make a copy in lambda function) - const std::function<std::string(const std::string &filename)> &f1 = func; - const std::function<std::string(const std::string &filename)> &f2 = other.func; + const std::function<std::string(std::string &)> &f1 = func_; + const std::function<std::string(std::string &)> &f2 = other.func_; - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); if(s1.empty() || s2.empty()) return std::string(); else - return s1 + " & " + s2; + return std::string("(") + s1 + ") OR (" + s2 + ")"; }; + newval.active_ = (active_ & other.active_); return newval; } + + /// Create a validator that fails when a given validator succeeds + Validator operator!() const { + Validator newval; + const std::function<std::string()> &dfunc1 = desc_function_; + newval.desc_function_ = [dfunc1]() { + auto str = dfunc1(); + return (!str.empty()) ? std::string("NOT ") + str : std::string{}; + }; + // Give references (will make a copy in lambda function) + const std::function<std::string(std::string & res)> &f1 = func_; + + newval.func_ = [f1, dfunc1](std::string &test) -> std::string { + std::string s1 = f1(test); + if(s1.empty()) { + return std::string("check ") + dfunc1() + " succeeded improperly"; + } else + return std::string{}; + }; + newval.active_ = active_; + return newval; + } + + private: + void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { + + const std::function<std::string()> &dfunc1 = val1.desc_function_; + const std::function<std::string()> &dfunc2 = val2.desc_function_; + + desc_function_ = [=]() { + std::string f1 = dfunc1(); + std::string f2 = dfunc2(); + if((f1.empty()) || (f2.empty())) { + return f1 + f2; + } + return std::string("(") + f1 + ")" + merger + "(" + f2 + ")"; + }; + } }; +/// Class wrapping some of the accessors of Validator +class CustomValidator : public Validator { + public: +}; // The implementation of the built in validators is using the Validator class; // the user is only expected to use the const (static) versions (since there's no setup). // Therefore, this is in detail. namespace detail { /// Check for an existing file (returns error message if check fails) -struct ExistingFileValidator : public Validator { - ExistingFileValidator() { - tname = "FILE"; - func = [](const std::string &filename) { +class ExistingFileValidator : public Validator { + public: + ExistingFileValidator() : Validator("FILE") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; bool is_dir = (buffer.st_mode & S_IFDIR) != 0; @@ -104,10 +232,10 @@ struct ExistingFileValidator : public Validator { }; /// Check for an existing directory (returns error message if check fails) -struct ExistingDirectoryValidator : public Validator { - ExistingDirectoryValidator() { - tname = "DIR"; - func = [](const std::string &filename) { +class ExistingDirectoryValidator : public Validator { + public: + ExistingDirectoryValidator() : Validator("DIR") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; bool is_dir = (buffer.st_mode & S_IFDIR) != 0; @@ -122,10 +250,10 @@ struct ExistingDirectoryValidator : public Validator { }; /// Check for an existing path -struct ExistingPathValidator : public Validator { - ExistingPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { +class ExistingPathValidator : public Validator { + public: + ExistingPathValidator() : Validator("PATH(existing)") { + func_ = [](std::string &filename) { struct stat buffer; bool const exist = stat(filename.c_str(), &buffer) == 0; if(!exist) { @@ -137,10 +265,10 @@ struct ExistingPathValidator : public Validator { }; /// Check for an non-existing path -struct NonexistentPathValidator : public Validator { - NonexistentPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { +class NonexistentPathValidator : public Validator { + public: + NonexistentPathValidator() : Validator("PATH(non-existing)") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; if(exist) { @@ -150,6 +278,63 @@ struct NonexistentPathValidator : public Validator { }; } }; + +/// Validate the given string is a legal ipv4 address +class IPV4Validator : public Validator { + public: + IPV4Validator() : Validator("IPV4") { + func_ = [](std::string &ip_addr) { + auto result = CLI::detail::split(ip_addr, '.'); + if(result.size() != 4) { + return "Invalid IPV4 address must have four parts " + ip_addr; + } + int num; + bool retval = true; + for(const auto &var : result) { + retval &= detail::lexical_cast(var, num); + if(!retval) { + return "Failed parsing number " + var; + } + if(num < 0 || num > 255) { + return "Each IP number must be between 0 and 255 " + var; + } + } + return std::string(); + }; + } +}; + +/// Validate the argument is a number and greater than or equal to 0 +class PositiveNumber : public Validator { + public: + PositiveNumber() : Validator("POSITIVE") { + func_ = [](std::string &number_str) { + int number; + if(!detail::lexical_cast(number_str, number)) { + return "Failed parsing number " + number_str; + } + if(number < 0) { + return "Number less then 0 " + number_str; + } + return std::string(); + }; + } +}; + +/// Validate the argument is a number and greater than or equal to 0 +class Number : public Validator { + public: + Number() : Validator("NUMBER") { + func_ = [](std::string &number_str) { + double number; + if(!detail::lexical_cast(number_str, number)) { + return "Failed parsing as a number " + number_str; + } + return std::string(); + }; + } +}; + } // namespace detail // Static is not needed here, because global const implies static. @@ -166,8 +351,18 @@ const detail::ExistingPathValidator ExistingPath; /// Check for an non-existing path const detail::NonexistentPathValidator NonexistentPath; -/// Produce a range (factory). Min and max are inclusive. -struct Range : public Validator { +/// Check for an IP4 address +const detail::IPV4Validator ValidIPV4; + +/// Check for a positive number +const detail::PositiveNumber PositiveNumber; + +/// Check for a number +const detail::Number Number; + +/// Produce a range (factory). Min and max are inclusive. +class Range : public Validator { + public: /// This produces a range with min and max inclusive. /// /// Note that the constructor is templated, but the struct is not, so C++17 is not @@ -175,12 +370,12 @@ struct Range : public Validator { template <typename T> Range(T min, T max) { std::stringstream out; out << detail::type_name<T>() << " in [" << min << " - " << max << "]"; + description(out.str()); - tname = out.str(); - func = [min, max](std::string input) { + func_ = [min, max](std::string &input) { T val; - detail::lexical_cast(input, val); - if(val < min || val > max) + bool converted = detail::lexical_cast(input, val); + if((!converted) || (val < min || val > max)) return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max); return std::string(); @@ -191,6 +386,370 @@ struct Range : public Validator { template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {} }; +/// Produce a bounded range (factory). Min and max are inclusive. +class Bound : public Validator { + public: + /// This bounds a value with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template <typename T> Bound(T min, T max) { + std::stringstream out; + out << detail::type_name<T>() << " bounded to [" << min << " - " << max << "]"; + description(out.str()); + + func_ = [min, max](std::string &input) { + T val; + bool converted = detail::lexical_cast(input, val); + if(!converted) { + return "Value " + input + " could not be converted"; + } + if(val < min) + input = detail::as_string(min); + else if(val > max) + input = detail::as_string(max); + + return std::string(); + }; + } + + /// Range of one value is 0 to value + template <typename T> explicit Bound(T max) : Bound(static_cast<T>(0), max) {} +}; + +namespace detail { +template <typename T, + enable_if_t<is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy> +auto smart_deref(T value) -> decltype(*value) { + return *value; +} + +template < + typename T, + enable_if_t<!is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy> +typename std::remove_reference<T>::type &smart_deref(T &value) { + return value; +} +/// Generate a string representation of a set +template <typename T> std::string generate_set(const T &set) { + using element_t = typename detail::element_type<T>::type; + using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join(detail::smart_deref(set), + [](const iteration_type_t &v) { return detail::pair_adaptor<element_t>::first(v); }, + ",")); + out.push_back('}'); + return out; +} + +/// Generate a string representation of a map +template <typename T> std::string generate_map(const T &map) { + using element_t = typename detail::element_type<T>::type; + using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join(detail::smart_deref(map), + [](const iteration_type_t &v) { + return detail::as_string(detail::pair_adaptor<element_t>::first(v)) + "->" + + detail::as_string(detail::pair_adaptor<element_t>::second(v)); + }, + ",")); + out.push_back('}'); + return out; +} + +template <typename> struct sfinae_true : std::true_type {}; +/// Function to check for the existence of a member find function which presumably is more efficient than looping over +/// everything +template <typename T, typename V> +static auto test_find(int) -> sfinae_true<decltype(std::declval<T>().find(std::declval<V>()))>; +template <typename, typename V> static auto test_find(long) -> std::false_type; + +template <typename T, typename V> struct has_find : decltype(test_find<T, V>(0)) {}; + +/// A search function +template <typename T, typename V, enable_if_t<!has_find<T, V>::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> { + using element_t = typename detail::element_type<T>::type; + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { + return (detail::pair_adaptor<element_t>::first(v) == val); + }); + return {(it != std::end(setref)), it}; +} + +/// A search function that uses the built in find function +template <typename T, typename V, enable_if_t<has_find<T, V>::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> { + auto &setref = detail::smart_deref(set); + auto it = setref.find(val); + return {(it != std::end(setref)), it}; +} + +/// A search function with a filter function +template <typename T, typename V> +auto search(const T &set, const V &val, const std::function<V(V)> &filter_function) + -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> { + using element_t = typename detail::element_type<T>::type; + // do the potentially faster first search + auto res = search(set, val); + if((res.first) || (!(filter_function))) { + return res; + } + // if we haven't found it do the longer linear search with all the element translations + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { + V a = detail::pair_adaptor<element_t>::first(v); + a = filter_function(a); + return (a == val); + }); + return {(it != std::end(setref)), it}; +} + +} // namespace detail +/// Verify items are in a set +class IsMember : public Validator { + public: + using filter_fn_t = std::function<std::string(std::string)>; + + /// This allows in-place construction using an initializer list + template <typename T, typename... Args> + explicit IsMember(std::initializer_list<T> values, Args &&... args) + : IsMember(std::vector<T>(values), std::forward<Args>(args)...) {} + + /// This checks to see if an item is in a set (empty function) + template <typename T> explicit IsMember(T &&set) : IsMember(std::forward<T>(set), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template <typename T, typename F> explicit IsMember(T set, F filter_function) { + + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map + + using local_item_t = typename IsMemberType<item_t>::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function<local_item_t(local_item_t)> filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; + + // This is the function that validates + // It stores a copy of the set pointer-like, so shared_ptr will stay alive + func_ = [set, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + throw ValidationError(input); // name is added later + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(set, b, filter_fn); + if(res.first) { + // Make sure the version in the input string is identical to the one in the set + if(filter_fn) { + input = detail::as_string(detail::pair_adaptor<element_t>::first(*(res.second))); + } + + // Return empty error string (success) + return std::string{}; + } + + // If you reach this point, the result was not found + std::string out(" not in "); + out += detail::generate_set(detail::smart_deref(set)); + return out; + }; + } + + /// You can pass in as many filter functions as you like, they nest (string only currently) + template <typename T, typename... Args> + IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : IsMember(std::forward<T>(set), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// definition of the default transformation object +template <typename T> using TransformPairs = std::vector<std::pair<std::string, T>>; + +/// Translate named items to other or a value set +class Transformer : public Validator { + public: + using filter_fn_t = std::function<std::string(std::string)>; + + /// This allows in-place construction + template <typename... Args> + explicit Transformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&... args) + : Transformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {} + + /// direct map of std::string to std::string + template <typename T> explicit Transformer(T &&mapping) : Transformer(std::forward<T>(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template <typename T, typename F> explicit Transformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType<item_t>::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function<local_item_t(local_item_t)> filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; + + func_ = [mapping, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + return std::string(); + // there is no possible way we can match anything in the mapping if we can't convert so just return + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::as_string(detail::pair_adaptor<element_t>::second(*res.second)); + } + return std::string{}; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template <typename T, typename... Args> + Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : Transformer(std::forward<T>(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// translate named items to other or a value set +class CheckedTransformer : public Validator { + public: + using filter_fn_t = std::function<std::string(std::string)>; + + /// This allows in-place construction + template <typename... Args> + explicit CheckedTransformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&... args) + : CheckedTransformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {} + + /// direct map of std::string to std::string + template <typename T> explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template <typename T, typename F> explicit CheckedTransformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType<item_t>::type; // This will convert bad types to good ones + // (const char * to std::string) + using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair // + // the type of the object pair + + // Make a local copy of the filter function, using a std::function if not one already + std::function<local_item_t(local_item_t)> filter_fn = filter_function; + + auto tfunc = [mapping]() { + std::string out("value in "); + out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; + out += detail::join( + detail::smart_deref(mapping), + [](const iteration_type_t &v) { return detail::as_string(detail::pair_adaptor<element_t>::second(v)); }, + ","); + out.push_back('}'); + return out; + }; + + desc_function_ = tfunc; + + func_ = [mapping, tfunc, filter_fn](std::string &input) { + local_item_t b; + bool converted = detail::lexical_cast(input, b); + if(converted) { + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::as_string(detail::pair_adaptor<element_t>::second(*res.second)); + return std::string{}; + } + } + for(const auto &v : detail::smart_deref(mapping)) { + auto output_string = detail::as_string(detail::pair_adaptor<element_t>::second(v)); + if(output_string == input) { + return std::string(); + } + } + + return "Check " + input + " " + tfunc() + " FAILED"; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template <typename T, typename... Args> + CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : CheckedTransformer(std::forward<T>(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; // namespace CLI + +/// Helper function to allow ignore_case to be passed to IsMember or Transform +inline std::string ignore_case(std::string item) { return detail::to_lower(item); } + +/// Helper function to allow ignore_underscore to be passed to IsMember or Transform +inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } + +/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform +inline std::string ignore_space(std::string item) { + item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); + item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); + return item; +} + +namespace detail { +/// Split a string into a program name and command line arguments +/// the string is assumed to contain a file name followed by other arguments +/// the return value contains is a pair with the first argument containing the program name and the second +/// everything else. +inline std::pair<std::string, std::string> split_program_name(std::string commandline) { + // try to determine the programName + std::pair<std::string, std::string> vals; + trim(commandline); + auto esp = commandline.find_first_of(' ', 1); + while(!ExistingFile(commandline.substr(0, esp)).empty()) { + esp = commandline.find_first_of(' ', esp + 1); + if(esp == std::string::npos) { + // if we have reached the end and haven't found a valid file just assume the first argument is the + // program name + esp = commandline.find_first_of(' ', 1); + break; + } + } + vals.first = commandline.substr(0, esp); + rtrim(vals.first); + // strip the program name + vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{}; + ltrim(vals.second); + return vals; +} + +} // namespace detail /// @} } // namespace CLI diff --git a/packages/CLI11/include/CLI/Version.hpp b/packages/CLI11/include/CLI/Version.hpp index b2ae8641d..c86c09c40 100644 --- a/packages/CLI11/include/CLI/Version.hpp +++ b/packages/CLI11/include/CLI/Version.hpp @@ -6,8 +6,8 @@ // [CLI11:verbatim] #define CLI11_VERSION_MAJOR 1 -#define CLI11_VERSION_MINOR 6 -#define CLI11_VERSION_PATCH 2 -#define CLI11_VERSION "1.6.2" +#define CLI11_VERSION_MINOR 7 +#define CLI11_VERSION_PATCH 1 +#define CLI11_VERSION "1.7.1" // [CLI11:verbatim] diff --git a/packages/CLI11/scripts/check_style_docker.sh b/packages/CLI11/scripts/check_style_docker.sh new file mode 100755 index 000000000..9030dc51e --- /dev/null +++ b/packages/CLI11/scripts/check_style_docker.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +# Also good but untagged: CLANG_FORMAT=unibeautify/clang-format +CLANG_FORMAT=saschpe/clang-format:5.0.1 + +set -evx + +docker run --rm -it ${CLANG_FORMAT} --version +docker run --rm -it -v "$(pwd)":/workdir -w /workdir ${CLANG_FORMAT} -style=file -sort-includes -i $(git ls-files -- '*.cpp' '*.hpp') + +git diff --exit-code --color + +set +evx diff --git a/packages/CLI11/tests/AppTest.cpp b/packages/CLI11/tests/AppTest.cpp index b1bd218e5..54cf85010 100644 --- a/packages/CLI11/tests/AppTest.cpp +++ b/packages/CLI11/tests/AppTest.cpp @@ -2,12 +2,82 @@ #include <complex> #include <cstdlib> +#include "gmock/gmock.h" + TEST_F(TApp, OneFlagShort) { app.add_flag("-c,--count"); args = {"-c"}; run(); - EXPECT_EQ((size_t)1, app.count("-c")); - EXPECT_EQ((size_t)1, app.count("--count")); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); +} + +TEST_F(TApp, OneFlagShortValues) { + app.add_flag("-c{v1},--count{v2}"); + args = {"-c"}; + run(); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); + auto v = app["-c"]->results(); + EXPECT_EQ(v[0], "v1"); + + EXPECT_THROW(app["--invalid"], CLI::OptionNotFound); +} + +TEST_F(TApp, OneFlagShortValuesAs) { + auto flg = app.add_flag("-c{1},--count{2}"); + args = {"-c"}; + run(); + auto opt = app["-c"]; + EXPECT_EQ(opt->as<int>(), 1); + args = {"--count"}; + run(); + EXPECT_EQ(opt->as<int>(), 2); + flg->take_first(); + args = {"-c", "--count"}; + run(); + EXPECT_EQ(opt->as<int>(), 1); + flg->take_last(); + EXPECT_EQ(opt->as<int>(), 2); + flg->multi_option_policy(CLI::MultiOptionPolicy::Throw); + EXPECT_THROW(opt->as<int>(), CLI::ConversionError); + + auto vec = opt->as<std::vector<int>>(); + EXPECT_EQ(vec[0], 1); + EXPECT_EQ(vec[1], 2); + flg->multi_option_policy(CLI::MultiOptionPolicy::Join); + EXPECT_EQ(opt->as<std::string>(), "1,2"); +} + +TEST_F(TApp, OneFlagShortWindows) { + app.add_flag("-c,--count"); + args = {"/c"}; + app.allow_windows_style_options(); + run(); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); +} + +TEST_F(TApp, WindowsLongShortMix1) { + app.allow_windows_style_options(); + + auto a = app.add_flag("-c"); + auto b = app.add_flag("--c"); + args = {"/c"}; + run(); + EXPECT_EQ(1u, a->count()); + EXPECT_EQ(0u, b->count()); +} + +TEST_F(TApp, WindowsLongShortMix2) { + app.allow_windows_style_options(); + + auto a = app.add_flag("--c"); + auto b = app.add_flag("-c"); + args = {"/c"}; + run(); + EXPECT_EQ(1u, a->count()); + EXPECT_EQ(0u, b->count()); } TEST_F(TApp, CountNonExist) { @@ -21,8 +91,8 @@ TEST_F(TApp, OneFlagLong) { app.add_flag("-c,--count"); args = {"--count"}; run(); - EXPECT_EQ((size_t)1, app.count("-c")); - EXPECT_EQ((size_t)1, app.count("--count")); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); } TEST_F(TApp, DashedOptions) { @@ -32,10 +102,59 @@ TEST_F(TApp, DashedOptions) { args = {"-c", "--q", "--this", "--that"}; run(); - EXPECT_EQ((size_t)1, app.count("-c")); - EXPECT_EQ((size_t)1, app.count("--q")); - EXPECT_EQ((size_t)2, app.count("--this")); - EXPECT_EQ((size_t)2, app.count("--that")); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--q")); + EXPECT_EQ(2u, app.count("--this")); + EXPECT_EQ(2u, app.count("--that")); +} + +TEST_F(TApp, DashedOptionsSingleString) { + app.add_flag("-c"); + app.add_flag("--q"); + app.add_flag("--this,--that"); + + app.parse("-c --q --this --that"); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--q")); + EXPECT_EQ(2u, app.count("--this")); + EXPECT_EQ(2u, app.count("--that")); +} + +TEST_F(TApp, RequireOptionsError) { + using ::testing::HasSubstr; + using ::testing::Not; + app.add_flag("-c"); + app.add_flag("--q"); + app.add_flag("--this,--that"); + app.require_option(1, 2); + try { + app.parse("-c --q --this --that"); + } catch(const CLI::RequiredError &re) { + EXPECT_THAT(re.what(), Not(HasSubstr("-h,--help"))); + } + + EXPECT_NO_THROW(app.parse("-c --q")); + EXPECT_NO_THROW(app.parse("-c --this --that")); +} + +TEST_F(TApp, BoolFlagOverride) { + bool val; + auto flg = app.add_flag("--this,--that", val); + + app.parse("--this"); + EXPECT_TRUE(val); + app.parse("--this=false"); + EXPECT_FALSE(val); + flg->disable_flag_override(true); + app.parse("--this"); + EXPECT_TRUE(val); + // this is allowed since the matching string is the default + app.parse("--this=true"); + EXPECT_TRUE(val); + + EXPECT_THROW(app.parse("--this=false"), CLI::ArgumentMismatch); + // try a string that specifies 'use default val' + EXPECT_NO_THROW(app.parse("--this={}")); } TEST_F(TApp, OneFlagRef) { @@ -43,18 +162,100 @@ TEST_F(TApp, OneFlagRef) { app.add_flag("-c,--count", ref); args = {"--count"}; run(); - EXPECT_EQ((size_t)1, app.count("-c")); - EXPECT_EQ((size_t)1, app.count("--count")); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); EXPECT_EQ(1, ref); } +TEST_F(TApp, OneFlagRefValue) { + int ref; + app.add_flag("-c,--count", ref); + args = {"--count=7"}; + run(); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); + EXPECT_EQ(7, ref); +} + +TEST_F(TApp, OneFlagRefValueFalse) { + int ref; + auto flg = app.add_flag("-c,--count", ref); + args = {"--count=false"}; + run(); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); + EXPECT_EQ(-1, ref); + + EXPECT_FALSE(flg->check_fname("c")); + args = {"--count=0"}; + run(); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); + EXPECT_EQ(-1, ref); + + args = {"--count=happy"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, FlagNegation) { + int ref; + auto flg = app.add_flag("-c,--count,--ncount{false}", ref); + args = {"--count", "-c", "--ncount"}; + EXPECT_FALSE(flg->check_fname("count")); + EXPECT_TRUE(flg->check_fname("ncount")); + run(); + EXPECT_EQ(3u, app.count("-c")); + EXPECT_EQ(3u, app.count("--count")); + EXPECT_EQ(3u, app.count("--ncount")); + EXPECT_EQ(1, ref); +} + +TEST_F(TApp, FlagNegationShortcutNotation) { + int ref; + app.add_flag("-c,--count{true},!--ncount", ref); + args = {"--count=TRUE", "-c", "--ncount"}; + run(); + EXPECT_EQ(3u, app.count("-c")); + EXPECT_EQ(3u, app.count("--count")); + EXPECT_EQ(3u, app.count("--ncount")); + EXPECT_EQ(1, ref); +} + +TEST_F(TApp, FlagNegationShortcutNotationInvalid) { + int ref; + app.add_flag("-c,--count,!--ncount", ref); + args = {"--ncount=happy"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + TEST_F(TApp, OneString) { std::string str; app.add_option("-s,--string", str); args = {"--string", "mystring"}; run(); - EXPECT_EQ((size_t)1, app.count("-s")); - EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringWindowsStyle) { + std::string str; + app.add_option("-s,--string", str); + args = {"/string", "mystring"}; + app.allow_windows_style_options(); + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringSingleStringInput) { + std::string str; + app.add_option("-s,--string", str); + + app.parse("--string mystring"); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); EXPECT_EQ(str, "mystring"); } @@ -63,19 +264,155 @@ TEST_F(TApp, OneStringEqualVersion) { app.add_option("-s,--string", str); args = {"--string=mystring"}; run(); - EXPECT_EQ((size_t)1, app.count("-s")); - EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringEqualVersionWindowsStyle) { + std::string str; + app.add_option("-s,--string", str); + args = {"/string:mystring"}; + app.allow_windows_style_options(); + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringEqualVersionSingleString) { + std::string str; + app.add_option("-s,--string", str); + app.parse("--string=mystring"); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); EXPECT_EQ(str, "mystring"); } +TEST_F(TApp, OneStringEqualVersionSingleStringQuoted) { + std::string str; + app.add_option("-s,--string", str); + app.parse("--string=\"this is my quoted string\""); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "this is my quoted string"); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultiple) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringEmbeddedEqual) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse("--string=\"app=\\\"test1 b\\\" test2=\\\"frogs\\\"\" -t 'qstring 2' -m=`\"quoted string\"`"); + EXPECT_EQ(str, "app=\"test1 b\" test2=\"frogs\""); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + app.parse("--string=\"app='test1 b' test2='frogs'\" -t 'qstring 2' -m=`\"quoted string\"`"); + EXPECT_EQ(str, "app='test1 b' test2='frogs'"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringEmbeddedEqualWindowsStyle) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("--mstr", str3); + app.allow_windows_style_options(); + app.parse("/string:\"app:\\\"test1 b\\\" test2:\\\"frogs\\\"\" /t 'qstring 2' /mstr:`\"quoted string\"`"); + EXPECT_EQ(str, "app:\"test1 b\" test2:\"frogs\""); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + app.parse("/string:\"app:'test1 b' test2:'frogs'\" /t 'qstring 2' /mstr:`\"quoted string\"`"); + EXPECT_EQ(str, "app:'test1 b' test2:'frogs'"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleMixedStyle) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.allow_windows_style_options(); + app.parse("/string:\"this is my quoted string\" /t 'qstring 2' -m=`\"quoted string\"`"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleInMiddle) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse(R"raw(--string="this is my quoted string" -t "qst\"ring 2" -m=`"quoted string"`")raw"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qst\"ring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedEscapedCharacters) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse(R"raw(--string="this is my \"quoted\" string" -t 'qst\'ring 2' -m=`"quoted\` string"`")raw"); + EXPECT_EQ(str, "this is my \"quoted\" string"); + EXPECT_EQ(str2, "qst\'ring 2"); + EXPECT_EQ(str3, "\"quoted` string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqual) { + std::string str, str2, str3, str4; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.add_option("-j,--jstr", str4); + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + EXPECT_EQ(str4, "Unquoted"); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqualAndProgram) { + std::string str, str2, str3, str4; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.add_option("-j,--jstr", str4); + app.parse("program --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + EXPECT_EQ(str4, "Unquoted"); +} + TEST_F(TApp, TogetherInt) { int i; app.add_option("-i,--int", i); args = {"-i4"}; run(); - EXPECT_EQ((size_t)1, app.count("--int")); - EXPECT_EQ((size_t)1, app.count("-i")); + EXPECT_EQ(1u, app.count("--int")); + EXPECT_EQ(1u, app.count("-i")); EXPECT_EQ(i, 4); + EXPECT_EQ(app["-i"]->as<std::string>(), "4"); + EXPECT_EQ(app["--int"]->as<double>(), 4.0); } TEST_F(TApp, SepInt) { @@ -83,8 +420,8 @@ TEST_F(TApp, SepInt) { app.add_option("-i,--int", i); args = {"-i", "4"}; run(); - EXPECT_EQ((size_t)1, app.count("--int")); - EXPECT_EQ((size_t)1, app.count("-i")); + EXPECT_EQ(1u, app.count("--int")); + EXPECT_EQ(1u, app.count("-i")); EXPECT_EQ(i, 4); } @@ -93,17 +430,81 @@ TEST_F(TApp, OneStringAgain) { app.add_option("-s,--string", str); args = {"--string", "mystring"}; run(); - EXPECT_EQ((size_t)1, app.count("-s")); - EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringFunction) { + std::string str; + app.add_option_function<std::string>("-s,--string", [&str](const std::string &val) { str = val; }); + args = {"--string", "mystring"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--string")); EXPECT_EQ(str, "mystring"); } +TEST_F(TApp, doubleFunction) { + double res; + app.add_option_function<double>("--val", [&res](double val) { res = std::abs(val + 54); }); + args = {"--val", "-354.356"}; + run(); + EXPECT_EQ(res, 300.356); + // get the original value as entered as an integer + EXPECT_EQ(app["--val"]->as<float>(), -354.356f); +} + +TEST_F(TApp, doubleFunctionFail) { + double res; + app.add_option_function<double>("--val", [&res](double val) { res = std::abs(val + 54); }); + args = {"--val", "not_double"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, doubleVectorFunction) { + std::vector<double> res; + app.add_option_function<std::vector<double>>("--val", [&res](const std::vector<double> &val) { + res = val; + std::transform(res.begin(), res.end(), res.begin(), [](double v) { return v + 5.0; }); + }); + args = {"--val", "5", "--val", "6", "--val", "7"}; + run(); + EXPECT_EQ(res.size(), 3u); + EXPECT_EQ(res[0], 10.0); + EXPECT_EQ(res[2], 12.0); +} + +TEST_F(TApp, doubleVectorFunctionFail) { + std::vector<double> res; + std::string vstring = "--val"; + app.add_option_function<std::vector<double>>(vstring, [&res](const std::vector<double> &val) { + res = val; + std::transform(res.begin(), res.end(), res.begin(), [](double v) { return v + 5.0; }); + }); + args = {"--val", "five", "--val", "nine", "--val", "7"}; + EXPECT_THROW(run(), CLI::ConversionError); + // check that getting the results through the results function generates the same error + EXPECT_THROW(app[vstring]->results(res), CLI::ConversionError); + auto strvec = app[vstring]->as<std::vector<std::string>>(); + EXPECT_EQ(strvec.size(), 3u); +} + TEST_F(TApp, DefaultStringAgain) { std::string str = "previous"; app.add_option("-s,--string", str); run(); - EXPECT_EQ((size_t)0, app.count("-s")); - EXPECT_EQ((size_t)0, app.count("--string")); + EXPECT_EQ(0u, app.count("-s")); + EXPECT_EQ(0u, app.count("--string")); + EXPECT_EQ(str, "previous"); +} + +TEST_F(TApp, DefaultStringAgainEmpty) { + std::string str = "previous"; + app.add_option("-s,--string", str); + app.parse(" "); + EXPECT_EQ(0u, app.count("-s")); + EXPECT_EQ(0u, app.count("--string")); EXPECT_EQ(str, "previous"); } @@ -131,9 +532,63 @@ TEST_F(TApp, LotsOfFlags) { args = {"-a", "-b", "-aA"}; run(); - EXPECT_EQ((size_t)2, app.count("-a")); - EXPECT_EQ((size_t)1, app.count("-b")); - EXPECT_EQ((size_t)1, app.count("-A")); + EXPECT_EQ(2u, app.count("-a")); + EXPECT_EQ(1u, app.count("-b")); + EXPECT_EQ(1u, app.count("-A")); + EXPECT_EQ(app.count_all(), 4u); +} + +TEST_F(TApp, NumberFlags) { + + int val; + app.add_flag("-1{1},-2{2},-3{3},-4{4},-5{5},-6{6}, -7{7}, -8{8}, -9{9}", val); + + args = {"-7"}; + run(); + EXPECT_EQ(1u, app.count("-1")); + EXPECT_EQ(val, 7); +} + +TEST_F(TApp, DisableFlagOverrideTest) { + + int val; + auto opt = app.add_flag("--1{1},--2{2},--3{3},--4{4},--5{5},--6{6}, --7{7}, --8{8}, --9{9}", val); + EXPECT_FALSE(opt->get_disable_flag_override()); + opt->disable_flag_override(); + args = {"--7=5"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); + EXPECT_TRUE(opt->get_disable_flag_override()); + opt->disable_flag_override(false); + EXPECT_FALSE(opt->get_disable_flag_override()); + EXPECT_NO_THROW(run()); + EXPECT_EQ(val, 5); + opt->disable_flag_override(); + args = {"--7=7"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(TApp, LotsOfFlagsSingleString) { + + app.add_flag("-a"); + app.add_flag("-A"); + app.add_flag("-b"); + + app.parse("-a -b -aA"); + EXPECT_EQ(2u, app.count("-a")); + EXPECT_EQ(1u, app.count("-b")); + EXPECT_EQ(1u, app.count("-A")); +} + +TEST_F(TApp, LotsOfFlagsSingleStringExtraSpace) { + + app.add_flag("-a"); + app.add_flag("-A"); + app.add_flag("-b"); + + app.parse(" -a -b -aA "); + EXPECT_EQ(2u, app.count("-a")); + EXPECT_EQ(1u, app.count("-b")); + EXPECT_EQ(1u, app.count("-A")); } TEST_F(TApp, BoolAndIntFlags) { @@ -177,6 +632,23 @@ TEST_F(TApp, BoolOnlyFlag) { EXPECT_THROW(run(), CLI::ConversionError); } +TEST_F(TApp, BoolOption) { + bool bflag; + app.add_option("-b", bflag); + + args = {"-b", "false"}; + run(); + EXPECT_FALSE(bflag); + + args = {"-b", "1"}; + run(); + EXPECT_TRUE(bflag); + + args = {"-b", "-7"}; + run(); + EXPECT_FALSE(bflag); +} + TEST_F(TApp, ShortOpts) { unsigned long long funnyint; @@ -190,10 +662,11 @@ TEST_F(TApp, ShortOpts) { run(); - EXPECT_EQ((size_t)2, app.count("-z")); - EXPECT_EQ((size_t)1, app.count("-y")); + EXPECT_EQ(2u, app.count("-z")); + EXPECT_EQ(1u, app.count("-y")); EXPECT_EQ((unsigned long long)2, funnyint); EXPECT_EQ("zyz", someopt); + EXPECT_EQ(app.count_all(), 3u); } TEST_F(TApp, DefaultOpts) { @@ -208,8 +681,8 @@ TEST_F(TApp, DefaultOpts) { run(); - EXPECT_EQ((size_t)1, app.count("i")); - EXPECT_EQ((size_t)1, app.count("-s")); + EXPECT_EQ(1u, app.count("i")); + EXPECT_EQ(1u, app.count("-s")); EXPECT_EQ(2, i); EXPECT_EQ("9", s); } @@ -460,16 +933,56 @@ TEST_F(TApp, PositionalNoSpace) { args = {"-O", "Test", "param1", "param2"}; run(); - EXPECT_EQ(options.size(), (size_t)1); + EXPECT_EQ(options.size(), 1u); EXPECT_EQ(options.at(0), "Test"); args = {"-OTest", "param1", "param2"}; run(); - EXPECT_EQ(options.size(), (size_t)1); + EXPECT_EQ(options.size(), 1u); EXPECT_EQ(options.at(0), "Test"); } +// Tests positionals at end +TEST_F(TApp, PositionalAtEnd) { + std::string options; + std::string foo; + + app.add_option("-O", options); + app.add_option("foo", foo); + app.positionals_at_end(); + EXPECT_TRUE(app.get_positionals_at_end()); + args = {"-O", "Test", "param1"}; + run(); + + EXPECT_EQ(options, "Test"); + EXPECT_EQ(foo, "param1"); + + args = {"param2", "-O", "Test"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +// Tests positionals at end +TEST_F(TApp, PositionalValidation) { + std::string options; + std::string foo; + + app.add_option("bar", options)->check(CLI::Number); + app.add_option("foo", foo); + app.validate_positionals(); + args = {"1", "param1"}; + run(); + + EXPECT_EQ(options, "1"); + EXPECT_EQ(foo, "param1"); + + args = {"param1", "1"}; + run(); + + EXPECT_EQ(options, "1"); + EXPECT_EQ(foo, "param1"); +} + TEST_F(TApp, PositionalNoSpaceLong) { std::vector<std::string> options; std::string foo, bar; @@ -481,13 +994,13 @@ TEST_F(TApp, PositionalNoSpaceLong) { args = {"--option", "Test", "param1", "param2"}; run(); - EXPECT_EQ(options.size(), (size_t)1); + EXPECT_EQ(options.size(), 1u); EXPECT_EQ(options.at(0), "Test"); args = {"--option=Test", "param1", "param2"}; run(); - EXPECT_EQ(options.size(), (size_t)1); + EXPECT_EQ(options.size(), 1u); EXPECT_EQ(options.at(0), "Test"); } @@ -666,45 +1179,124 @@ TEST_F(TApp, RequiredFlags) { TEST_F(TApp, CallbackFlags) { - size_t value = 0; + int64_t value = 0; - auto func = [&value](size_t x) { value = x; }; + auto func = [&value](int64_t x) { value = x; }; app.add_flag_function("-v", func); run(); - EXPECT_EQ(value, (size_t)0); + EXPECT_EQ(value, 0u); args = {"-v"}; run(); - EXPECT_EQ(value, (size_t)1); + EXPECT_EQ(value, 1u); args = {"-vv"}; run(); - EXPECT_EQ(value, (size_t)2); + EXPECT_EQ(value, 2u); EXPECT_THROW(app.add_flag_function("hi", func), CLI::IncorrectConstruction); } -#if __cplusplus >= 201402L +TEST_F(TApp, CallbackBoolFlags) { + + bool value = false; + + auto func = [&value]() { value = true; }; + + auto cback = app.add_flag_callback("--val", func); + args = {"--val"}; + run(); + EXPECT_TRUE(value); + value = false; + args = {"--val=false"}; + run(); + EXPECT_FALSE(value); + + EXPECT_THROW(app.add_flag_callback("hi", func), CLI::IncorrectConstruction); + cback->multi_option_policy(CLI::MultiOptionPolicy::Throw); + args = {"--val", "--val=false"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, CallbackFlagsFalse) { + int64_t value = 0; + + auto func = [&value](int64_t x) { value = x; }; + + app.add_flag_function("-v,-f{false},--val,--fval{false}", func); + + run(); + EXPECT_EQ(value, 0); + + args = {"-f"}; + run(); + EXPECT_EQ(value, -1); + + args = {"-vfv"}; + run(); + EXPECT_EQ(value, 1); + + args = {"--fval"}; + run(); + EXPECT_EQ(value, -1); + + args = {"--fval=2"}; + run(); + EXPECT_EQ(value, -2); + + EXPECT_THROW(app.add_flag_function("hi", func), CLI::IncorrectConstruction); +} + +TEST_F(TApp, CallbackFlagsFalseShortcut) { + int64_t value = 0; + + auto func = [&value](int64_t x) { value = x; }; + + app.add_flag_function("-v,!-f,--val,!--fval", func); + + run(); + EXPECT_EQ(value, 0); + + args = {"-f"}; + run(); + EXPECT_EQ(value, -1); + + args = {"-vfv"}; + run(); + EXPECT_EQ(value, 1); + + args = {"--fval"}; + run(); + EXPECT_EQ(value, -1); + + args = {"--fval=2"}; + run(); + EXPECT_EQ(value, -2); + + EXPECT_THROW(app.add_flag_function("hi", func), CLI::IncorrectConstruction); +} + +#if __cplusplus >= 201402L || _MSC_VER >= 1900 TEST_F(TApp, CallbackFlagsAuto) { - size_t value = 0; + int64_t value = 0; - auto func = [&value](size_t x) { value = x; }; + auto func = [&value](int64_t x) { value = x; }; app.add_flag("-v", func); run(); - EXPECT_EQ(value, (size_t)0); + EXPECT_EQ(value, 0u); args = {"-v"}; run(); - EXPECT_EQ(value, (size_t)1); + EXPECT_EQ(value, 1u); args = {"-vv"}; run(); - EXPECT_EQ(value, (size_t)2); + EXPECT_EQ(value, 2u); EXPECT_THROW(app.add_flag("hi", func), CLI::IncorrectConstruction); } @@ -721,8 +1313,8 @@ TEST_F(TApp, Positionals) { run(); - EXPECT_EQ((size_t)1, app.count("posit1")); - EXPECT_EQ((size_t)1, app.count("posit2")); + EXPECT_EQ(1u, app.count("posit1")); + EXPECT_EQ(1u, app.count("posit2")); EXPECT_EQ("thing1", posit1); EXPECT_EQ("thing2", posit2); } @@ -757,8 +1349,8 @@ TEST_F(TApp, MixedPositionals) { run(); - EXPECT_EQ((size_t)1, app.count("posit2")); - EXPECT_EQ((size_t)1, app.count("--posit1")); + EXPECT_EQ(1u, app.count("posit2")); + EXPECT_EQ(1u, app.count("--posit1")); EXPECT_EQ(7, positional_int); EXPECT_EQ("thing2", positional_string); } @@ -788,19 +1380,19 @@ TEST_F(TApp, Reset) { run(); - EXPECT_EQ((size_t)1, app.count("--simple")); - EXPECT_EQ((size_t)1, app.count("-d")); + EXPECT_EQ(1u, app.count("--simple")); + EXPECT_EQ(1u, app.count("-d")); EXPECT_DOUBLE_EQ(1.2, doub); app.clear(); - EXPECT_EQ((size_t)0, app.count("--simple")); - EXPECT_EQ((size_t)0, app.count("-d")); + EXPECT_EQ(0u, app.count("--simple")); + EXPECT_EQ(0u, app.count("-d")); run(); - EXPECT_EQ((size_t)1, app.count("--simple")); - EXPECT_EQ((size_t)1, app.count("-d")); + EXPECT_EQ(1u, app.count("--simple")); + EXPECT_EQ(1u, app.count("-d")); EXPECT_DOUBLE_EQ(1.2, doub); } @@ -849,7 +1441,7 @@ TEST_F(TApp, FileNotExists) { ASSERT_NO_THROW(CLI::NonexistentPath(myfile)); std::string filename; - app.add_option("--file", filename)->check(CLI::NonexistentPath); + auto opt = app.add_option("--file", filename)->check(CLI::NonexistentPath, "path_check"); args = {"--file", myfile}; run(); @@ -858,7 +1450,9 @@ TEST_F(TApp, FileNotExists) { bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file EXPECT_TRUE(ok); EXPECT_THROW(run(), CLI::ValidationError); - + // deactivate the check, so it should run now + opt->get_validator("path_check")->active(false); + EXPECT_NO_THROW(run()); std::remove(myfile.c_str()); EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); } @@ -882,116 +1476,22 @@ TEST_F(TApp, FileExists) { EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); } -TEST_F(TApp, InSet) { - - std::string choice; - app.add_set("-q,--quick", choice, {"one", "two", "three"}); - - args = {"--quick", "two"}; - - run(); - EXPECT_EQ("two", choice); - - args = {"--quick", "four"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, InSetWithDefault) { - - std::string choice = "one"; - app.add_set("-q,--quick", choice, {"one", "two", "three"}, "", true); - - run(); - EXPECT_EQ("one", choice); - - args = {"--quick", "two"}; - - run(); - EXPECT_EQ("two", choice); - - args = {"--quick", "four"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, InCaselessSetWithDefault) { - - std::string choice = "one"; - app.add_set_ignore_case("-q,--quick", choice, {"one", "two", "three"}, "", true); - - run(); - EXPECT_EQ("one", choice); - - args = {"--quick", "tWo"}; - - run(); - EXPECT_EQ("two", choice); - - args = {"--quick", "four"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, InIntSet) { - - int choice; - app.add_set("-q,--quick", choice, {1, 2, 3}); - - args = {"--quick", "2"}; - - run(); - EXPECT_EQ(2, choice); - - args = {"--quick", "4"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, FailSet) { - - int choice; - app.add_set("-q,--quick", choice, {1, 2, 3}); - - args = {"--quick", "3", "--quick=2"}; - EXPECT_THROW(run(), CLI::ArgumentMismatch); - - args = {"--quick=hello"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, FailLValueSet) { - - int choice; - std::set<int> vals{1, 2, 3}; - app.add_set("-q,--quick", choice, vals); - app.add_set("-s,--slow", choice, vals, "", true); - - args = {"--quick=hello"}; - EXPECT_THROW(run(), CLI::ConversionError); - - args = {"--slow=hello"}; - EXPECT_THROW(run(), CLI::ConversionError); -} - -TEST_F(TApp, InSetIgnoreCase) { - - std::string choice; - app.add_set_ignore_case("-q,--quick", choice, {"one", "Two", "THREE"}); - - args = {"--quick", "One"}; - run(); - EXPECT_EQ("one", choice); +TEST_F(TApp, NotFileExists) { + std::string myfile{"TestNonFileNotUsed.txt"}; + EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); - args = {"--quick", "two"}; - run(); - EXPECT_EQ("Two", choice); // Keeps caps from set + std::string filename = "Failed"; + app.add_option("--file", filename)->check(!CLI::ExistingFile); + args = {"--file", myfile}; - args = {"--quick", "ThrEE"}; - run(); - EXPECT_EQ("THREE", choice); // Keeps caps from set + EXPECT_NO_THROW(run()); - args = {"--quick", "four"}; - EXPECT_THROW(run(), CLI::ConversionError); + bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file + EXPECT_TRUE(ok); + EXPECT_THROW(run(), CLI::ValidationError); - args = {"--quick=one", "--quick=two"}; - EXPECT_THROW(run(), CLI::ArgumentMismatch); + std::remove(myfile.c_str()); + EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); } TEST_F(TApp, VectorFixedString) { @@ -1003,7 +1503,7 @@ TEST_F(TApp, VectorFixedString) { args = {"--string", "mystring", "mystring2", "mystring3"}; run(); - EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(3u, app.count("--string")); EXPECT_EQ(answer, strvec); } @@ -1016,10 +1516,28 @@ TEST_F(TApp, VectorDefaultedFixedString) { args = {"--string", "mystring", "mystring2", "mystring3"}; run(); - EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(3u, app.count("--string")); EXPECT_EQ(answer, strvec); } +TEST_F(TApp, DefaultedResult) { + std::string sval = "NA"; + int ival; + auto opts = app.add_option("--string", sval, "", true); + auto optv = app.add_option("--val", ival); + args = {}; + run(); + EXPECT_EQ(sval, "NA"); + std::string nString; + opts->results(nString); + EXPECT_EQ(nString, "NA"); + int newIval; + EXPECT_THROW(optv->results(newIval), CLI::ConversionError); + optv->default_str("442"); + optv->results(newIval); + EXPECT_EQ(newIval, 442); +} + TEST_F(TApp, VectorUnlimString) { std::vector<std::string> strvec; std::vector<std::string> answer{"mystring", "mystring2", "mystring3"}; @@ -1029,12 +1547,12 @@ TEST_F(TApp, VectorUnlimString) { args = {"--string", "mystring", "mystring2", "mystring3"}; run(); - EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(3u, app.count("--string")); EXPECT_EQ(answer, strvec); args = {"-s", "mystring", "mystring2", "mystring3"}; run(); - EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(3u, app.count("--string")); EXPECT_EQ(answer, strvec); } @@ -1047,7 +1565,7 @@ TEST_F(TApp, VectorFancyOpts) { args = {"--string", "mystring", "mystring2", "mystring3"}; run(); - EXPECT_EQ((size_t)3, app.count("--string")); + EXPECT_EQ(3u, app.count("--string")); EXPECT_EQ(answer, strvec); args = {"one", "two"}; @@ -1221,7 +1739,7 @@ TEST_F(TApp, Env) { run(); EXPECT_EQ(2, val); - EXPECT_EQ((size_t)1, vopt->count()); + EXPECT_EQ(1u, vopt->count()); vopt->required(); run(); @@ -1272,14 +1790,13 @@ TEST_F(TApp, RangeDouble) { run(); } -// Check to make sure progromatic access to left over is available +// Check to make sure programmatic access to left over is available TEST_F(TApp, AllowExtras) { app.allow_extras(); bool val = true; app.add_flag("-f", val); - EXPECT_FALSE(val); args = {"-x", "-f"}; @@ -1305,55 +1822,67 @@ TEST_F(TApp, AllowExtrasOrder) { TEST_F(TApp, CheckShortFail) { args = {"--two"}; - EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, false), CLI::HorribleError); + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT), CLI::HorribleError); } // Test horrible error TEST_F(TApp, CheckLongFail) { args = {"-t"}; - EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, true), CLI::HorribleError); + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG), CLI::HorribleError); } // Test horrible error -TEST_F(TApp, CheckSubcomFail) { - args = {"subcom"}; +TEST_F(TApp, CheckWindowsFail) { + args = {"-t"}; - EXPECT_THROW(CLI::detail::AppFriend::parse_subcommand(&app, args), CLI::HorribleError); + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS), CLI::HorribleError); } -TEST_F(TApp, OptionWithDefaults) { - int someint = 2; - app.add_option("-a", someint, "", true); - - args = {"-a1", "-a2"}; +// Test horrible error +TEST_F(TApp, CheckOtherFail) { + args = {"-t"}; - EXPECT_THROW(run(), CLI::ArgumentMismatch); + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE), CLI::HorribleError); } -TEST_F(TApp, SetWithDefaults) { - int someint = 2; - app.add_set("-a", someint, {1, 2, 3, 4}, "", true); +// Test horrible error +TEST_F(TApp, CheckSubcomFail) { + args = {"subcom"}; - args = {"-a1", "-a2"}; + EXPECT_THROW(CLI::detail::AppFriend::parse_subcommand(&app, args), CLI::HorribleError); +} - EXPECT_THROW(run(), CLI::ArgumentMismatch); +TEST_F(TApp, FallthroughParentFail) { + EXPECT_THROW(CLI::detail::AppFriend::get_fallthrough_parent(&app), CLI::HorribleError); } -TEST_F(TApp, SetWithDefaultsConversion) { - int someint = 2; - app.add_set("-a", someint, {1, 2, 3, 4}, "", true); +TEST_F(TApp, FallthroughParents) { + auto sub = app.add_subcommand("test"); + EXPECT_EQ(CLI::detail::AppFriend::get_fallthrough_parent(sub), &app); - args = {"-a", "hi"}; + auto ssub = sub->add_subcommand("sub2"); + EXPECT_EQ(CLI::detail::AppFriend::get_fallthrough_parent(ssub), sub); - EXPECT_THROW(run(), CLI::ConversionError); + auto og1 = app.add_option_group("g1"); + auto og2 = og1->add_option_group("g2"); + auto og3 = og2->add_option_group("g3"); + EXPECT_EQ(CLI::detail::AppFriend::get_fallthrough_parent(og3), &app); + + auto ogb1 = sub->add_option_group("g1"); + auto ogb2 = ogb1->add_option_group("g2"); + auto ogb3 = ogb2->add_option_group("g3"); + EXPECT_EQ(CLI::detail::AppFriend::get_fallthrough_parent(ogb3), sub); + + ogb2->name("groupb"); + EXPECT_EQ(CLI::detail::AppFriend::get_fallthrough_parent(ogb3), ogb2); } -TEST_F(TApp, SetWithDefaultsIC) { - std::string someint = "ho"; - app.add_set_ignore_case("-a", someint, {"Hi", "Ho"}, "", true); +TEST_F(TApp, OptionWithDefaults) { + int someint = 2; + app.add_option("-a", someint, "", true); - args = {"-aHi", "-aHo"}; + args = {"-a1", "-a2"}; EXPECT_THROW(run(), CLI::ArgumentMismatch); } @@ -1369,7 +1898,7 @@ TEST_F(TApp, OrderedModifingTransforms) { run(); - EXPECT_EQ(val, std::vector<std::string>({"one12", "two12"})); + EXPECT_EQ(val, std::vector<std::string>({"one21", "two21"})); } TEST_F(TApp, ThrowingTransform) { @@ -1425,98 +1954,193 @@ TEST_F(TApp, CustomDoubleOption) { EXPECT_DOUBLE_EQ(custom_opt.second, 1.5); } -// #113 -TEST_F(TApp, AddRemoveSetItems) { - std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; +// #128 +TEST_F(TApp, RepeatingMultiArgumentOptions) { + std::vector<std::string> entries; + app.add_option("--entry", entries, "set a key and value")->type_name("KEY VALUE")->type_size(-2); - std::string type1, type2; - app.add_set("--type1", type1, items); - app.add_set("--type2", type2, items, "", true); + args = {"--entry", "key1", "value1", "--entry", "key2", "value2"}; + ASSERT_NO_THROW(run()); + EXPECT_EQ(entries, std::vector<std::string>({"key1", "value1", "key2", "value2"})); - args = {"--type1", "TYPE1", "--type2", "TYPE2"}; + args.pop_back(); + ASSERT_THROW(run(), CLI::ArgumentMismatch); +} +// #122 +TEST_F(TApp, EmptyOptionEach) { + std::string q; + app.add_option("--each")->each([&q](std::string s) { q = s; }); + + args = {"--each", "that"}; run(); - EXPECT_EQ(type1, "TYPE1"); - EXPECT_EQ(type2, "TYPE2"); - items.insert("TYPE6"); - items.insert("TYPE7"); + EXPECT_EQ(q, "that"); +} - items.erase("TYPE1"); - items.erase("TYPE2"); +// #122 +TEST_F(TApp, EmptyOptionFail) { + std::string q; + app.add_option("--each"); - args = {"--type1", "TYPE6", "--type2", "TYPE7"}; + args = {"--each", "that"}; run(); - EXPECT_EQ(type1, "TYPE6"); - EXPECT_EQ(type2, "TYPE7"); +} - args = {"--type1", "TYPE1"}; - EXPECT_THROW(run(), CLI::ConversionError); +TEST_F(TApp, BeforeRequirements) { + app.add_flag_function("-a", [](int64_t) { throw CLI::Success(); }); + app.add_flag_function("-b", [](int64_t) { throw CLI::CallForHelp(); }); - args = {"--type2", "TYPE2"}; - EXPECT_THROW(run(), CLI::ConversionError); -} + args = {"extra"}; + EXPECT_THROW(run(), CLI::ExtrasError); + + args = {"-a", "extra"}; + EXPECT_THROW(run(), CLI::Success); -TEST_F(TApp, AddRemoveSetItemsNoCase) { - std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + args = {"-b", "extra"}; + EXPECT_THROW(run(), CLI::CallForHelp); - std::string type1, type2; - app.add_set_ignore_case("--type1", type1, items); - app.add_set_ignore_case("--type2", type2, items, "", true); + // These run in definition order. + args = {"-a", "-b", "extra"}; + EXPECT_THROW(run(), CLI::Success); - args = {"--type1", "TYPe1", "--type2", "TyPE2"}; + // Currently, the original order is not preserved when calling callbacks + // args = {"-b", "-a", "extra"}; + // EXPECT_THROW(run(), CLI::CallForHelp); +} +// #209 +TEST_F(TApp, CustomUserSepParse) { + + std::vector<int> vals = {1, 2, 3}; + args = {"--idx", "1,2,3"}; + auto opt = app.add_option("--idx", vals)->delimiter(','); run(); - EXPECT_EQ(type1, "TYPE1"); - EXPECT_EQ(type2, "TYPE2"); + EXPECT_EQ(vals, std::vector<int>({1, 2, 3})); + std::vector<int> vals2; + // check that the results vector gets the results in the same way + opt->results(vals2); + EXPECT_EQ(vals2, vals); + + app.remove_option(opt); - items.insert("TYPE6"); - items.insert("TYPE7"); + app.add_option("--idx", vals, "", true)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2, 3})); +} - items.erase("TYPE1"); - items.erase("TYPE2"); +// #209 +TEST_F(TApp, DefaultUserSepParse) { - args = {"--type1", "TyPE6", "--type2", "tYPE7"}; + std::vector<std::string> vals; + args = {"--idx", "1 2 3", "4 5 6"}; + auto opt = app.add_option("--idx", vals, ""); + run(); + EXPECT_EQ(vals, std::vector<std::string>({"1 2 3", "4 5 6"})); + opt->delimiter(','); run(); - EXPECT_EQ(type1, "TYPE6"); - EXPECT_EQ(type2, "TYPE7"); + EXPECT_EQ(vals, std::vector<std::string>({"1 2 3", "4 5 6"})); +} - args = {"--type1", "TYPe1"}; - EXPECT_THROW(run(), CLI::ConversionError); +// #209 +TEST_F(TApp, BadUserSepParse) { + + std::vector<int> vals; + app.add_option("--idx", vals); + + args = {"--idx", "1,2,3"}; - args = {"--type2", "TYpE2"}; EXPECT_THROW(run(), CLI::ConversionError); } -// #128 -TEST_F(TApp, RepeatingMultiArgumentOptions) { - std::vector<std::string> entries; - app.add_option("--entry", entries, "set a key and value")->type_name("KEY VALUE")->type_size(-2); +// #209 +TEST_F(TApp, CustomUserSepParse2) { - args = {"--entry", "key1", "value1", "--entry", "key2", "value2"}; - ASSERT_NO_THROW(run()); - EXPECT_EQ(entries, std::vector<std::string>({"key1", "value1", "key2", "value2"})); + std::vector<int> vals = {1, 2, 3}; + args = {"--idx", "1,2,"}; + auto opt = app.add_option("--idx", vals)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); - args.pop_back(); - ASSERT_THROW(run(), CLI::ArgumentMismatch); + app.remove_option(opt); + + app.add_option("--idx", vals, "", true)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); } -// #122 -TEST_F(TApp, EmptyOptionEach) { - std::string q; - app.add_option("--each", {})->each([&q](std::string s) { q = s; }); +TEST_F(TApp, CustomUserSepParseFunction) { - args = {"--each", "that"}; + std::vector<int> vals = {1, 2, 3}; + args = {"--idx", "1,2,3"}; + app.add_option_function<std::vector<int>>("--idx", [&vals](std::vector<int> v) { vals = std::move(v); }) + ->delimiter(','); run(); + EXPECT_EQ(vals, std::vector<int>({1, 2, 3})); +} - EXPECT_EQ(q, "that"); +// delimiter removal +TEST_F(TApp, CustomUserSepParseToggle) { + + std::vector<std::string> vals; + args = {"--idx", "1,2,3"}; + auto opt = app.add_option("--idx", vals)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<std::string>({"1", "2", "3"})); + opt->delimiter('\0'); + run(); + EXPECT_EQ(vals, std::vector<std::string>({"1,2,3"})); + opt->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<std::string>({"1", "2", "3"})); } -// #122 -TEST_F(TApp, EmptyOptionFail) { - std::string q; - app.add_option("--each", {}); +// #209 +TEST_F(TApp, CustomUserSepParse3) { - args = {"--each", "that"}; + std::vector<int> vals = {1, 2, 3}; + args = {"--idx", + "1", + "," + "2"}; + auto opt = app.add_option("--idx", vals)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); + app.remove_option(opt); + + app.add_option("--idx", vals, "", false)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); +} + +// #209 +TEST_F(TApp, CustomUserSepParse4) { + + std::vector<int> vals; + args = {"--idx", "1, 2"}; + auto opt = app.add_option("--idx", vals)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); + + app.remove_option(opt); + + app.add_option("--idx", vals, "", true)->delimiter(','); + run(); + EXPECT_EQ(vals, std::vector<int>({1, 2})); +} + +// #218 +TEST_F(TApp, CustomUserSepParse5) { + + std::vector<std::string> bar; + args = {"this", "is", "a", "test"}; + auto opt = app.add_option("bar", bar, "bar"); + run(); + EXPECT_EQ(bar, std::vector<std::string>({"this", "is", "a", "test"})); + + app.remove_option(opt); + args = {"this", "is", "a", "test"}; + app.add_option("bar", bar, "bar", true); run(); + EXPECT_EQ(bar, std::vector<std::string>({"this", "is", "a", "test"})); } diff --git a/packages/CLI11/tests/CMakeLists.txt b/packages/CLI11/tests/CMakeLists.txt index 848f1bae1..ec1f931a2 100644 --- a/packages/CLI11/tests/CMakeLists.txt +++ b/packages/CLI11/tests/CMakeLists.txt @@ -26,6 +26,8 @@ set(CLI11_TESTS IniTest SimpleTest AppTest + SetTest + TransformTest CreationTest SubcommandTest HelpTest @@ -33,6 +35,9 @@ set(CLI11_TESTS NewParseTest OptionalTest DeprecatedTest + StringParseTest + TrueFalseTest + OptionGroupTest ) if(WIN32) @@ -107,11 +112,15 @@ file(WRITE "${PROJECT_BINARY_DIR}/CTestCustom.cmake" ) # Add boost to test boost::optional if available -find_package(Boost 1.58) +find_package(Boost 1.61) if(Boost_FOUND) - target_link_libraries(informational PUBLIC Boost::boost) - target_link_libraries(OptionalTest PUBLIC Boost::boost) - + if(TARGET Boost::boost) + target_link_libraries(informational PUBLIC Boost::boost) + target_link_libraries(OptionalTest PUBLIC Boost::boost) + else() + target_include_directories(informational PUBLIC ${Boost_INCLUDE_DIRS}) + target_include_directories(OptionalTest PUBLIC ${Boost_INCLUDE_DIRS}) + endif() # Enforce Boost::Optional even if __has_include is missing on your compiler target_compile_definitions(informational PUBLIC CLI11_BOOST_OPTIONAL) target_compile_definitions(OptionalTest PUBLIC CLI11_BOOST_OPTIONAL) diff --git a/packages/CLI11/tests/CreationTest.cpp b/packages/CLI11/tests/CreationTest.cpp index e7c75f836..8fc5d6243 100644 --- a/packages/CLI11/tests/CreationTest.cpp +++ b/packages/CLI11/tests/CreationTest.cpp @@ -48,6 +48,20 @@ TEST_F(TApp, AddingExistingWithCaseAfter2) { EXPECT_THROW(cat->ignore_case(), CLI::OptionAlreadyAdded); } +TEST_F(TApp, AddingExistingWithUnderscoreAfter) { + auto count = app.add_flag("--underscore"); + app.add_flag("--under_score"); + + EXPECT_THROW(count->ignore_underscore(), CLI::OptionAlreadyAdded); +} + +TEST_F(TApp, AddingExistingWithUnderscoreAfter2) { + auto count = app.add_flag("--under_score"); + app.add_flag("--underscore"); + + EXPECT_THROW(count->ignore_underscore(), CLI::OptionAlreadyAdded); +} + TEST_F(TApp, AddingMultipleInfPositionals) { std::vector<std::string> one, two; app.add_option("one", one); @@ -92,6 +106,17 @@ TEST_F(TApp, MultipleSubcomMatchingWithCaseFirst) { EXPECT_THROW(app.add_subcommand("fIrst"), CLI::OptionAlreadyAdded); } +TEST_F(TApp, MultipleSubcomMatchingWithUnderscore) { + app.add_subcommand("first_option")->ignore_underscore(); + EXPECT_THROW(app.add_subcommand("firstoption"), CLI::OptionAlreadyAdded); +} + +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreFirst) { + app.ignore_underscore(); + app.add_subcommand("first_option"); + EXPECT_THROW(app.add_subcommand("firstoption"), CLI::OptionAlreadyAdded); +} + TEST_F(TApp, MultipleSubcomMatchingWithCaseInplace) { app.add_subcommand("first"); auto first = app.add_subcommand("fIrst"); @@ -106,6 +131,20 @@ TEST_F(TApp, MultipleSubcomMatchingWithCaseInplace2) { EXPECT_THROW(first->ignore_case(), CLI::OptionAlreadyAdded); } +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreInplace) { + app.add_subcommand("first_option"); + auto first = app.add_subcommand("firstoption"); + + EXPECT_THROW(first->ignore_underscore(), CLI::OptionAlreadyAdded); +} + +TEST_F(TApp, MultipleSubcomMatchingWithUnderscoreInplace2) { + auto first = app.add_subcommand("firstoption"); + app.add_subcommand("first_option"); + + EXPECT_THROW(first->ignore_underscore(), CLI::OptionAlreadyAdded); +} + TEST_F(TApp, MultipleSubcomNoMatchingInplace2) { auto first = app.add_subcommand("first"); auto second = app.add_subcommand("second"); @@ -114,6 +153,14 @@ TEST_F(TApp, MultipleSubcomNoMatchingInplace2) { EXPECT_NO_THROW(second->ignore_case()); } +TEST_F(TApp, MultipleSubcomNoMatchingInplaceUnderscore2) { + auto first = app.add_subcommand("first_option"); + auto second = app.add_subcommand("second_option"); + + EXPECT_NO_THROW(first->ignore_underscore()); + EXPECT_NO_THROW(second->ignore_underscore()); +} + TEST_F(TApp, IncorrectConstructionFlagPositional1) { EXPECT_THROW(app.add_flag("cat"), CLI::IncorrectConstruction); } TEST_F(TApp, IncorrectConstructionFlagPositional2) { @@ -262,6 +309,68 @@ TEST_F(TApp, CheckNameNoCase) { EXPECT_TRUE(pos2->check_name("pos2")); } +TEST_F(TApp, CheckNameNoUnderscore) { + auto long1 = app.add_flag("--longoption1")->ignore_underscore(); + auto long2 = app.add_flag("--long_option2")->ignore_underscore(); + + int x, y; + auto pos1 = app.add_option("pos_option_1", x)->ignore_underscore(); + auto pos2 = app.add_option("posoption2", y)->ignore_underscore(); + + EXPECT_TRUE(long1->check_name("--long_option1")); + EXPECT_TRUE(long1->check_name("--longoption_1")); + EXPECT_TRUE(long1->check_name("--longoption1")); + EXPECT_TRUE(long1->check_name("--long__opt_ion__1")); + EXPECT_TRUE(long1->check_name("--__l_o_n_g_o_p_t_i_o_n_1")); + + EXPECT_TRUE(long2->check_name("--long_option2")); + EXPECT_TRUE(long2->check_name("--longoption2")); + EXPECT_TRUE(long2->check_name("--longoption_2")); + EXPECT_TRUE(long2->check_name("--long__opt_ion__2")); + EXPECT_TRUE(long2->check_name("--__l_o_n_go_p_t_i_o_n_2__")); + + EXPECT_TRUE(pos1->check_name("pos_option1")); + EXPECT_TRUE(pos1->check_name("pos_option_1")); + EXPECT_TRUE(pos1->check_name("pos_o_p_t_i_on_1")); + EXPECT_TRUE(pos1->check_name("posoption1")); + + EXPECT_TRUE(pos2->check_name("pos_option2")); + EXPECT_TRUE(pos2->check_name("pos_option_2")); + EXPECT_TRUE(pos2->check_name("pos_o_p_t_i_on_2")); + EXPECT_TRUE(pos2->check_name("posoption2")); +} + +TEST_F(TApp, CheckNameNoCaseNoUnderscore) { + auto long1 = app.add_flag("--LongoptioN1")->ignore_underscore()->ignore_case(); + auto long2 = app.add_flag("--long_Option2")->ignore_case()->ignore_underscore(); + + int x, y; + auto pos1 = app.add_option("pos_Option_1", x)->ignore_underscore()->ignore_case(); + auto pos2 = app.add_option("posOption2", y)->ignore_case()->ignore_underscore(); + + EXPECT_TRUE(long1->check_name("--Long_Option1")); + EXPECT_TRUE(long1->check_name("--lONgoption_1")); + EXPECT_TRUE(long1->check_name("--LongOption1")); + EXPECT_TRUE(long1->check_name("--long__Opt_ion__1")); + EXPECT_TRUE(long1->check_name("--__l_o_N_g_o_P_t_i_O_n_1")); + + EXPECT_TRUE(long2->check_name("--long_Option2")); + EXPECT_TRUE(long2->check_name("--LongOption2")); + EXPECT_TRUE(long2->check_name("--longOPTION_2")); + EXPECT_TRUE(long2->check_name("--long__OPT_ion__2")); + EXPECT_TRUE(long2->check_name("--__l_o_n_GO_p_t_i_o_n_2__")); + + EXPECT_TRUE(pos1->check_name("POS_Option1")); + EXPECT_TRUE(pos1->check_name("pos_option_1")); + EXPECT_TRUE(pos1->check_name("pos_o_p_t_i_on_1")); + EXPECT_TRUE(pos1->check_name("posoption1")); + + EXPECT_TRUE(pos2->check_name("pos_option2")); + EXPECT_TRUE(pos2->check_name("pos_OPTION_2")); + EXPECT_TRUE(pos2->check_name("poS_o_p_T_I_on_2")); + EXPECT_TRUE(pos2->check_name("PosOption2")); +} + TEST_F(TApp, PreSpaces) { int x; auto myapp = app.add_option(" -a, --long, other", x); @@ -301,6 +410,12 @@ TEST_F(TApp, OptionFromDefaults) { auto opt3 = app.add_option("--simple3", x); EXPECT_TRUE(opt3->get_required()); EXPECT_TRUE(opt3->get_ignore_case()); + + app.option_defaults()->required()->ignore_underscore(); + + auto opt4 = app.add_option("--simple4", x); + EXPECT_TRUE(opt4->get_required()); + EXPECT_TRUE(opt4->get_ignore_underscore()); } TEST_F(TApp, OptionFromDefaultsSubcommands) { @@ -308,6 +423,8 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) { EXPECT_FALSE(app.option_defaults()->get_required()); EXPECT_EQ(app.option_defaults()->get_multi_option_policy(), CLI::MultiOptionPolicy::Throw); EXPECT_FALSE(app.option_defaults()->get_ignore_case()); + EXPECT_FALSE(app.option_defaults()->get_ignore_underscore()); + EXPECT_FALSE(app.option_defaults()->get_disable_flag_override()); EXPECT_TRUE(app.option_defaults()->get_configurable()); EXPECT_EQ(app.option_defaults()->get_group(), "Options"); @@ -315,7 +432,9 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) { ->required() ->multi_option_policy(CLI::MultiOptionPolicy::TakeLast) ->ignore_case() + ->ignore_underscore() ->configurable(false) + ->disable_flag_override() ->group("Something"); auto app2 = app.add_subcommand("app2"); @@ -323,7 +442,9 @@ TEST_F(TApp, OptionFromDefaultsSubcommands) { EXPECT_TRUE(app2->option_defaults()->get_required()); EXPECT_EQ(app2->option_defaults()->get_multi_option_policy(), CLI::MultiOptionPolicy::TakeLast); EXPECT_TRUE(app2->option_defaults()->get_ignore_case()); + EXPECT_TRUE(app2->option_defaults()->get_ignore_underscore()); EXPECT_FALSE(app2->option_defaults()->get_configurable()); + EXPECT_TRUE(app.option_defaults()->get_disable_flag_override()); EXPECT_EQ(app2->option_defaults()->get_group(), "Something"); } @@ -346,22 +467,40 @@ TEST_F(TApp, GetNameCheck) { } TEST_F(TApp, SubcommandDefaults) { - // allow_extras, prefix_command, ignore_case, fallthrough, group, min/max subcommand + // allow_extras, prefix_command, ignore_case, fallthrough, group, min/max subcommand, validate_positionals // Initial defaults EXPECT_FALSE(app.get_allow_extras()); EXPECT_FALSE(app.get_prefix_command()); + EXPECT_FALSE(app.get_immediate_callback()); EXPECT_FALSE(app.get_ignore_case()); + EXPECT_FALSE(app.get_ignore_underscore()); +#ifdef _WIN32 + EXPECT_TRUE(app.get_allow_windows_style_options()); +#else + EXPECT_FALSE(app.get_allow_windows_style_options()); +#endif EXPECT_FALSE(app.get_fallthrough()); + EXPECT_FALSE(app.get_validate_positionals()); + EXPECT_EQ(app.get_footer(), ""); EXPECT_EQ(app.get_group(), "Subcommands"); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)0); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)0); + EXPECT_EQ(app.get_require_subcommand_min(), 0u); + EXPECT_EQ(app.get_require_subcommand_max(), 0u); app.allow_extras(); app.prefix_command(); + app.immediate_callback(); app.ignore_case(); + app.ignore_underscore(); +#ifdef _WIN32 + app.allow_windows_style_options(false); +#else + app.allow_windows_style_options(); +#endif + app.fallthrough(); + app.validate_positionals(); app.footer("footy"); app.group("Stuff"); app.require_subcommand(2, 3); @@ -371,43 +510,51 @@ TEST_F(TApp, SubcommandDefaults) { // Initial defaults EXPECT_TRUE(app2->get_allow_extras()); EXPECT_TRUE(app2->get_prefix_command()); + EXPECT_TRUE(app2->get_immediate_callback()); EXPECT_TRUE(app2->get_ignore_case()); + EXPECT_TRUE(app2->get_ignore_underscore()); +#ifdef _WIN32 + EXPECT_FALSE(app2->get_allow_windows_style_options()); +#else + EXPECT_TRUE(app2->get_allow_windows_style_options()); +#endif EXPECT_TRUE(app2->get_fallthrough()); + EXPECT_TRUE(app2->get_validate_positionals()); EXPECT_EQ(app2->get_footer(), "footy"); EXPECT_EQ(app2->get_group(), "Stuff"); - EXPECT_EQ(app2->get_require_subcommand_min(), (size_t)0); - EXPECT_EQ(app2->get_require_subcommand_max(), (size_t)3); + EXPECT_EQ(app2->get_require_subcommand_min(), 0u); + EXPECT_EQ(app2->get_require_subcommand_max(), 3u); } TEST_F(TApp, SubcommandMinMax) { - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)0); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)0); + EXPECT_EQ(app.get_require_subcommand_min(), 0u); + EXPECT_EQ(app.get_require_subcommand_max(), 0u); app.require_subcommand(); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)1); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)0); + EXPECT_EQ(app.get_require_subcommand_min(), 1u); + EXPECT_EQ(app.get_require_subcommand_max(), 0u); app.require_subcommand(2); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)2); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)2); + EXPECT_EQ(app.get_require_subcommand_min(), 2u); + EXPECT_EQ(app.get_require_subcommand_max(), 2u); app.require_subcommand(0); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)0); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)0); + EXPECT_EQ(app.get_require_subcommand_min(), 0u); + EXPECT_EQ(app.get_require_subcommand_max(), 0u); app.require_subcommand(-2); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)0); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)2); + EXPECT_EQ(app.get_require_subcommand_min(), 0u); + EXPECT_EQ(app.get_require_subcommand_max(), 2u); app.require_subcommand(3, 7); - EXPECT_EQ(app.get_require_subcommand_min(), (size_t)3); - EXPECT_EQ(app.get_require_subcommand_max(), (size_t)7); + EXPECT_EQ(app.get_require_subcommand_min(), 3u); + EXPECT_EQ(app.get_require_subcommand_max(), 7u); } TEST_F(TApp, GetOptionList) { @@ -421,3 +568,164 @@ TEST_F(TApp, GetOptionList) { EXPECT_EQ(opt_list.at(1), flag); EXPECT_EQ(opt_list.at(2), opt); } + +TEST(ValidatorTests, TestValidatorCreation) { + std::function<std::string(std::string &)> op1 = [](std::string &val) { + return (val.size() >= 5) ? std::string{} : val; + }; + CLI::Validator V(op1, "", "size"); + + EXPECT_EQ(V.get_name(), "size"); + V.name("harry"); + EXPECT_EQ(V.get_name(), "harry"); + EXPECT_TRUE(V.get_active()); + + EXPECT_EQ(V("test"), "test"); + EXPECT_EQ(V("test5"), std::string{}); + + EXPECT_EQ(V.get_description(), std::string{}); + V.description("this is a description"); + EXPECT_EQ(V.get_description(), "this is a description"); +} + +TEST(ValidatorTests, TestValidatorOps) { + std::function<std::string(std::string &)> op1 = [](std::string &val) { + return (val.size() >= 5) ? std::string{} : val; + }; + std::function<std::string(std::string &)> op2 = [](std::string &val) { + return (val.size() >= 9) ? std::string{} : val; + }; + std::function<std::string(std::string &)> op3 = [](std::string &val) { + return (val.size() < 3) ? std::string{} : val; + }; + std::function<std::string(std::string &)> op4 = [](std::string &val) { + return (val.size() <= 9) ? std::string{} : val; + }; + CLI::Validator V1(op1, "SIZE >= 5"); + + CLI::Validator V2(op2, "SIZE >= 9"); + CLI::Validator V3(op3, "SIZE < 3"); + CLI::Validator V4(op4, "SIZE <= 9"); + + std::string two(2, 'a'); + std::string four(4, 'a'); + std::string five(5, 'a'); + std::string eight(8, 'a'); + std::string nine(9, 'a'); + std::string ten(10, 'a'); + EXPECT_TRUE(V1(five).empty()); + EXPECT_FALSE(V1(four).empty()); + + EXPECT_TRUE(V2(nine).empty()); + EXPECT_FALSE(V2(eight).empty()); + + EXPECT_TRUE(V3(two).empty()); + EXPECT_FALSE(V3(four).empty()); + + EXPECT_TRUE(V4(eight).empty()); + EXPECT_FALSE(V4(ten).empty()); + + auto V1a2 = V1 & V2; + EXPECT_EQ(V1a2.get_description(), "(SIZE >= 5) AND (SIZE >= 9)"); + EXPECT_FALSE(V1a2(five).empty()); + EXPECT_TRUE(V1a2(nine).empty()); + + auto V1a4 = V1 & V4; + EXPECT_EQ(V1a4.get_description(), "(SIZE >= 5) AND (SIZE <= 9)"); + EXPECT_TRUE(V1a4(five).empty()); + EXPECT_TRUE(V1a4(eight).empty()); + EXPECT_FALSE(V1a4(ten).empty()); + EXPECT_FALSE(V1a4(four).empty()); + + auto V1o3 = V1 | V3; + EXPECT_EQ(V1o3.get_description(), "(SIZE >= 5) OR (SIZE < 3)"); + EXPECT_TRUE(V1o3(two).empty()); + EXPECT_TRUE(V1o3(eight).empty()); + EXPECT_TRUE(V1o3(ten).empty()); + EXPECT_TRUE(V1o3(two).empty()); + EXPECT_FALSE(V1o3(four).empty()); + + auto m1 = V1o3 & V4; + EXPECT_EQ(m1.get_description(), "((SIZE >= 5) OR (SIZE < 3)) AND (SIZE <= 9)"); + EXPECT_TRUE(m1(two).empty()); + EXPECT_TRUE(m1(eight).empty()); + EXPECT_FALSE(m1(ten).empty()); + EXPECT_TRUE(m1(two).empty()); + EXPECT_TRUE(m1(five).empty()); + EXPECT_FALSE(m1(four).empty()); + + auto m2 = m1 & V2; + EXPECT_EQ(m2.get_description(), "(((SIZE >= 5) OR (SIZE < 3)) AND (SIZE <= 9)) AND (SIZE >= 9)"); + EXPECT_FALSE(m2(two).empty()); + EXPECT_FALSE(m2(eight).empty()); + EXPECT_FALSE(m2(ten).empty()); + EXPECT_FALSE(m2(two).empty()); + EXPECT_TRUE(m2(nine).empty()); + EXPECT_FALSE(m2(four).empty()); + + auto m3 = m2 | V3; + EXPECT_EQ(m3.get_description(), "((((SIZE >= 5) OR (SIZE < 3)) AND (SIZE <= 9)) AND (SIZE >= 9)) OR (SIZE < 3)"); + EXPECT_TRUE(m3(two).empty()); + EXPECT_FALSE(m3(eight).empty()); + EXPECT_TRUE(m3(nine).empty()); + EXPECT_FALSE(m3(four).empty()); + + auto m4 = V3 | m2; + EXPECT_EQ(m4.get_description(), "(SIZE < 3) OR ((((SIZE >= 5) OR (SIZE < 3)) AND (SIZE <= 9)) AND (SIZE >= 9))"); + EXPECT_TRUE(m4(two).empty()); + EXPECT_FALSE(m4(eight).empty()); + EXPECT_TRUE(m4(nine).empty()); + EXPECT_FALSE(m4(four).empty()); +} + +TEST(ValidatorTests, TestValidatorNegation) { + + std::function<std::string(std::string &)> op1 = [](std::string &val) { + return (val.size() >= 5) ? std::string{} : val; + }; + + CLI::Validator V1(op1, "SIZE >= 5", "size"); + + std::string four(4, 'a'); + std::string five(5, 'a'); + + EXPECT_TRUE(V1(five).empty()); + EXPECT_FALSE(V1(four).empty()); + + auto V2 = !V1; + EXPECT_FALSE(V2(five).empty()); + EXPECT_TRUE(V2(four).empty()); + EXPECT_EQ(V2.get_description(), "NOT SIZE >= 5"); + + V2.active(false); + EXPECT_TRUE(V2(five).empty()); + EXPECT_TRUE(V2(four).empty()); + EXPECT_TRUE(V2.get_description().empty()); +} + +TEST(ValidatorTests, ValidatorDefaults) { + + CLI::Validator V1{}; + + std::string four(4, 'a'); + std::string five(5, 'a'); + + // make sure this doesn't generate a seg fault or something + EXPECT_TRUE(V1(five).empty()); + EXPECT_TRUE(V1(four).empty()); + + EXPECT_TRUE(V1.get_name().empty()); + EXPECT_TRUE(V1.get_description().empty()); + EXPECT_TRUE(V1.get_active()); + EXPECT_TRUE(V1.get_modifying()); + + CLI::Validator V2{"check"}; + // make sure this doesn't generate a seg fault or something + EXPECT_TRUE(V2(five).empty()); + EXPECT_TRUE(V2(four).empty()); + + EXPECT_TRUE(V2.get_name().empty()); + EXPECT_EQ(V2.get_description(), "check"); + EXPECT_TRUE(V2.get_active()); + EXPECT_TRUE(V2.get_modifying()); +} diff --git a/packages/CLI11/tests/DeprecatedTest.cpp b/packages/CLI11/tests/DeprecatedTest.cpp index ba574365d..c1981eaca 100644 --- a/packages/CLI11/tests/DeprecatedTest.cpp +++ b/packages/CLI11/tests/DeprecatedTest.cpp @@ -1,43 +1,328 @@ -#ifdef CLI11_SINGLE_FILE -#include "CLI11.hpp" -#else -#include "CLI/CLI.hpp" -#endif +#include "app_helper.hpp" -#include "gtest/gtest.h" +TEST(Deprecated, Emtpy) { + // No deprecated features at this time. + EXPECT_TRUE(true); +} + +// Classic sets + +TEST_F(TApp, SetWithDefaults) { + int someint = 2; + app.add_set("-a", someint, {1, 2, 3, 4}, "", true); + + args = {"-a1", "-a2"}; + + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, SetWithDefaultsConversion) { + int someint = 2; + app.add_set("-a", someint, {1, 2, 3, 4}, "", true); + + args = {"-a", "hi"}; + + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, SetWithDefaultsIC) { + std::string someint = "ho"; + app.add_set_ignore_case("-a", someint, {"Hi", "Ho"}, "", true); + + args = {"-aHi", "-aHo"}; + + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSet) { + + std::string choice; + app.add_set("-q,--quick", choice, {"one", "two", "three"}); + + args = {"--quick", "two"}; -TEST(Deprecated, SetFooter) { - CLI::App app{"My prog"}; + run(); + EXPECT_EQ("two", choice); - app.set_footer("My Footer"); - EXPECT_EQ("My Footer", app.get_footer()); + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); } -TEST(Deprecated, SetName) { - CLI::App app{"My prog"}; +TEST_F(TApp, InSetWithDefault) { + + std::string choice = "one"; + app.add_set("-q,--quick", choice, {"one", "two", "three"}, "", true); + + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; - app.set_name("My Name"); - EXPECT_EQ("My Name", app.get_name()); + run(); + EXPECT_EQ("two", choice); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); } -TEST(Deprecated, SetCallback) { - CLI::App app{"My prog"}; +TEST_F(TApp, InCaselessSetWithDefault) { + + std::string choice = "one"; + app.add_set_ignore_case("-q,--quick", choice, {"one", "two", "three"}, "", true); + + run(); + EXPECT_EQ("one", choice); - bool val; - app.set_callback([&val]() { val = true; }); + args = {"--quick", "tWo"}; - std::vector<std::string> something; - app.parse(something); + run(); + EXPECT_EQ("two", choice); - EXPECT_TRUE(val); + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); } -TEST(Deprecated, SetTypeName) { - CLI::App app{"My prog"}; +TEST_F(TApp, InIntSet) { + + int choice; + app.add_set("-q,--quick", choice, {1, 2, 3}); + + args = {"--quick", "2"}; + + run(); + EXPECT_EQ(2, choice); + + args = {"--quick", "4"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InIntSetWindows) { + + int choice; + app.add_set("-q,--quick", choice, {1, 2, 3}); + app.allow_windows_style_options(); + args = {"/q", "2"}; + + run(); + EXPECT_EQ(2, choice); + + args = {"/q", "4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"/q4"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(TApp, FailSet) { + + int choice; + app.add_set("-q,--quick", choice, {1, 2, 3}); + + args = {"--quick", "3", "--quick=2"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); + + args = {"--quick=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, FailMutableSet) { + + int choice; + std::set<int> vals{1, 2, 3}; + app.add_mutable_set("-q,--quick", choice, vals); + app.add_mutable_set("-s,--slow", choice, vals, "", true); + + args = {"--quick=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--slow=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InSetIgnoreCase) { + + std::string choice; + app.add_set_ignore_case("-q,--quick", choice, {"one", "Two", "THREE"}); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=one", "--quick=two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSetIgnoreCaseMutableValue) { + + std::set<std::string> options{"one", "Two", "THREE"}; + std::string choice; + app.add_mutable_set_ignore_case("-q,--quick", choice, options); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + options.clear(); + args = {"--quick", "ThrEE"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InSetIgnoreCasePointer) { + + auto options = std::make_shared<std::set<std::string>>(std::initializer_list<std::string>{"one", "Two", "THREE"}); + std::string choice; + app.add_set_ignore_case("-q,--quick", choice, *options); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + options.reset(); + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // this does not throw a segfault + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=one", "--quick=two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSetIgnoreUnderscore) { + + std::string choice; + app.add_set_ignore_underscore("-q,--quick", choice, {"option_one", "option_two", "optionthree"}); + + args = {"--quick", "option_one"}; + run(); + EXPECT_EQ("option_one", choice); + + args = {"--quick", "optiontwo"}; + run(); + EXPECT_EQ("option_two", choice); // Keeps underscore from set + + args = {"--quick", "_option_thr_ee"}; + run(); + EXPECT_EQ("optionthree", choice); // no underscore + + args = {"--quick", "Option4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=option_one", "--quick=option_two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSetIgnoreCaseUnderscore) { + + std::string choice; + app.add_set_ignore_case_underscore("-q,--quick", choice, {"Option_One", "option_two", "OptionThree"}); + + args = {"--quick", "option_one"}; + run(); + EXPECT_EQ("Option_One", choice); + + args = {"--quick", "OptionTwo"}; + run(); + EXPECT_EQ("option_two", choice); // Keeps underscore and case from set + + args = {"--quick", "_OPTION_thr_ee"}; + run(); + EXPECT_EQ("OptionThree", choice); // no underscore + + args = {"--quick", "Option4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=option_one", "--quick=option_two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +// #113 +TEST_F(TApp, AddRemoveSetItems) { + std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_mutable_set("--type1", type1, items); + app.add_mutable_set("--type2", type2, items, "", true); + + args = {"--type1", "TYPE1", "--type2", "TYPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + args = {"--type1", "TYPE6", "--type2", "TYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); + + args = {"--type1", "TYPE1"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--type2", "TYPE2"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, AddRemoveSetItemsNoCase) { + std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_mutable_set_ignore_case("--type1", type1, items); + app.add_mutable_set_ignore_case("--type2", type2, items, "", true); + + args = {"--type1", "TYPe1", "--type2", "TyPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + args = {"--type1", "TyPE6", "--type2", "tYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); - std::string val; - auto opt = app.add_option("--val", val); - opt->set_type_name("THAT"); + args = {"--type1", "TYPe1"}; + EXPECT_THROW(run(), CLI::ValidationError); - EXPECT_EQ(opt->get_type_name(), "THAT"); + args = {"--type2", "TYpE2"}; + EXPECT_THROW(run(), CLI::ValidationError); } diff --git a/packages/CLI11/tests/FormatterTest.cpp b/packages/CLI11/tests/FormatterTest.cpp index 74af18b9e..dc9bb664e 100644 --- a/packages/CLI11/tests/FormatterTest.cpp +++ b/packages/CLI11/tests/FormatterTest.cpp @@ -83,6 +83,25 @@ TEST(Formatter, OptCustomizeSimple) { " --opt INT (MUST HAVE) Something\n"); } +TEST(Formatter, FalseFlagExample) { + CLI::App app{"My prog"}; + + app.get_formatter()->column_width(25); + app.get_formatter()->label("REQUIRED", "(MUST HAVE)"); + + int v; + app.add_flag("--opt,!--no_opt", v, "Something"); + + bool flag; + app.add_flag("!-O,--opt2,--no_opt2{false}", flag, "Something else"); + + std::string help = app.help(); + + EXPECT_THAT(help, HasSubstr("--no_opt{false}")); + EXPECT_THAT(help, HasSubstr("--no_opt2{false}")); + EXPECT_THAT(help, HasSubstr("-O{false}")); +} + TEST(Formatter, AppCustomize) { CLI::App app{"My prog"}; app.add_subcommand("subcom1", "This"); @@ -134,3 +153,41 @@ TEST(Formatter, AllSub) { EXPECT_THAT(help, HasSubstr("--insub")); EXPECT_THAT(help, HasSubstr("subcom")); } + +TEST(Formatter, AllSubRequired) { + CLI::App app{"My prog"}; + CLI::App *sub = app.add_subcommand("subcom", "This"); + sub->add_flag("--insub", "MyFlag"); + sub->required(); + std::string help = app.help("", CLI::AppFormatMode::All); + EXPECT_THAT(help, HasSubstr("--insub")); + EXPECT_THAT(help, HasSubstr("subcom")); + EXPECT_THAT(help, HasSubstr("REQUIRED")); +} + +TEST(Formatter, NamelessSub) { + CLI::App app{"My prog"}; + CLI::App *sub = app.add_subcommand("", "This subcommand"); + sub->add_flag("--insub", "MyFlag"); + + std::string help = app.help("", CLI::AppFormatMode::Normal); + EXPECT_THAT(help, HasSubstr("--insub")); + EXPECT_THAT(help, HasSubstr("This subcommand")); +} + +TEST(Formatter, NamelessSubInGroup) { + CLI::App app{"My prog"}; + CLI::App *sub = app.add_subcommand("", "This subcommand"); + CLI::App *sub2 = app.add_subcommand("sub2", "subcommand2"); + sub->add_flag("--insub", "MyFlag"); + int val; + sub2->add_option("pos", val, "positional"); + sub->group("group1"); + sub2->group("group1"); + std::string help = app.help("", CLI::AppFormatMode::Normal); + EXPECT_THAT(help, HasSubstr("--insub")); + EXPECT_THAT(help, HasSubstr("This subcommand")); + EXPECT_THAT(help, HasSubstr("group1")); + EXPECT_THAT(help, HasSubstr("sub2")); + EXPECT_TRUE(help.find("pos") == std::string::npos); +} diff --git a/packages/CLI11/tests/HelpTest.cpp b/packages/CLI11/tests/HelpTest.cpp index 8825b08e0..d6e0d7c26 100644 --- a/packages/CLI11/tests/HelpTest.cpp +++ b/packages/CLI11/tests/HelpTest.cpp @@ -237,6 +237,26 @@ TEST(THelp, ManualSetters) { EXPECT_THAT(help, HasSubstr("=14")); } +TEST(THelp, ManualSetterOverFunction) { + + CLI::App app{"My prog"}; + + int x = 1; + + CLI::Option *op1 = app.add_option("--op1", x)->check(CLI::IsMember({1, 2})); + CLI::Option *op2 = app.add_option("--op2", x)->transform(CLI::IsMember({1, 2})); + op1->default_str("12"); + op1->type_name("BIGGLES"); + op2->type_name("QUIGGLES"); + EXPECT_EQ(x, 1); + + std::string help = app.help(); + EXPECT_THAT(help, HasSubstr("=12")); + EXPECT_THAT(help, HasSubstr("BIGGLES")); + EXPECT_THAT(help, HasSubstr("QUIGGLES")); + EXPECT_THAT(help, HasSubstr("{1,2}")); +} + TEST(THelp, Subcom) { CLI::App app{"My prog"}; @@ -280,7 +300,7 @@ TEST(THelp, IntDefaults) { int one{1}, two{2}; app.add_option("--one", one, "Help for one", true); - app.add_set("--set", two, {2, 3, 4}, "Help for set", true); + app.add_option("--set", two, "Help for set", true)->check(CLI::IsMember({2, 3, 4})); std::string help = app.help(); @@ -295,7 +315,7 @@ TEST(THelp, SetLower) { CLI::App app{"My prog"}; std::string def{"One"}; - app.add_set_ignore_case("--set", def, {"oNe", "twO", "THREE"}, "Help for set", true); + app.add_option("--set", def, "Help for set", true)->check(CLI::IsMember({"oNe", "twO", "THREE"})); std::string help = app.help(); @@ -317,6 +337,17 @@ TEST(THelp, OnlyOneHelp) { EXPECT_THROW(app.parse(input), CLI::ExtrasError); } +TEST(THelp, MultiHelp) { + CLI::App app{"My prog"}; + + // It is not supported to have more than one help flag, last one wins + app.set_help_flag("--help,-h,-?", "No short name allowed"); + app.allow_windows_style_options(); + + std::vector<std::string> input{"/?"}; + EXPECT_THROW(app.parse(input), CLI::CallForHelp); +} + TEST(THelp, OnlyOneAllHelp) { CLI::App app{"My prog"}; @@ -355,6 +386,49 @@ TEST(THelp, RemoveHelp) { } } +TEST(THelp, RemoveOtherMethodHelp) { + CLI::App app{"My prog"}; + + // Don't do this. Just in case, let's make sure it works. + app.remove_option(const_cast<CLI::Option *>(app.get_help_ptr())); + + std::string help = app.help(); + + EXPECT_THAT(help, HasSubstr("My prog")); + EXPECT_THAT(help, Not(HasSubstr("-h,--help"))); + EXPECT_THAT(help, Not(HasSubstr("Options:"))); + EXPECT_THAT(help, HasSubstr("Usage:")); + + std::vector<std::string> input{"--help"}; + try { + app.parse(input); + } catch(const CLI::ParseError &e) { + EXPECT_EQ(static_cast<int>(CLI::ExitCodes::ExtrasError), e.get_exit_code()); + } +} + +TEST(THelp, RemoveOtherMethodHelpAll) { + CLI::App app{"My prog"}; + + app.set_help_all_flag("--help-all"); + // Don't do this. Just in case, let's make sure it works. + app.remove_option(const_cast<CLI::Option *>(app.get_help_all_ptr())); + + std::string help = app.help(); + + EXPECT_THAT(help, HasSubstr("My prog")); + EXPECT_THAT(help, Not(HasSubstr("--help-all"))); + EXPECT_THAT(help, HasSubstr("Options:")); + EXPECT_THAT(help, HasSubstr("Usage:")); + + std::vector<std::string> input{"--help-all"}; + try { + app.parse(input); + } catch(const CLI::ParseError &e) { + EXPECT_EQ(static_cast<int>(CLI::ExitCodes::ExtrasError), e.get_exit_code()); + } +} + TEST(THelp, NoHelp) { CLI::App app{"My prog"}; app.set_help_flag(); @@ -530,6 +604,34 @@ TEST_F(CapturedHelp, NormalError) { EXPECT_THAT(err.str(), HasSubstr("for more information")); EXPECT_THAT(err.str(), Not(HasSubstr("ExtrasError"))); EXPECT_THAT(err.str(), HasSubstr("Thing")); + EXPECT_THAT(err.str(), Not(HasSubstr(" or "))); + EXPECT_THAT(err.str(), Not(HasSubstr("Usage"))); +} + +TEST_F(CapturedHelp, DoubleError) { + app.set_help_all_flag("--help-all"); + EXPECT_EQ(run(CLI::ExtrasError({"Thing"})), static_cast<int>(CLI::ExitCodes::ExtrasError)); + EXPECT_EQ(out.str(), ""); + EXPECT_THAT(err.str(), HasSubstr("for more information")); + EXPECT_THAT(err.str(), HasSubstr(" --help ")); + EXPECT_THAT(err.str(), HasSubstr(" --help-all ")); + EXPECT_THAT(err.str(), HasSubstr(" or ")); + EXPECT_THAT(err.str(), Not(HasSubstr("ExtrasError"))); + EXPECT_THAT(err.str(), HasSubstr("Thing")); + EXPECT_THAT(err.str(), Not(HasSubstr("Usage"))); +} + +TEST_F(CapturedHelp, AllOnlyError) { + app.set_help_all_flag("--help-all"); + app.set_help_flag(); + EXPECT_EQ(run(CLI::ExtrasError({"Thing"})), static_cast<int>(CLI::ExitCodes::ExtrasError)); + EXPECT_EQ(out.str(), ""); + EXPECT_THAT(err.str(), HasSubstr("for more information")); + EXPECT_THAT(err.str(), Not(HasSubstr(" --help "))); + EXPECT_THAT(err.str(), HasSubstr(" --help-all ")); + EXPECT_THAT(err.str(), Not(HasSubstr(" or "))); + EXPECT_THAT(err.str(), Not(HasSubstr("ExtrasError"))); + EXPECT_THAT(err.str(), HasSubstr("Thing")); EXPECT_THAT(err.str(), Not(HasSubstr("Usage"))); } @@ -574,6 +676,35 @@ TEST(THelp, AccessDescription) { EXPECT_EQ(app.get_description(), "My description goes here"); } +TEST(THelp, SetDescriptionAfterCreation) { + CLI::App app{""}; + + app.description("My description goes here"); + + EXPECT_EQ(app.get_description(), "My description goes here"); + EXPECT_THAT(app.help(), HasSubstr("My description goes here")); +} + +TEST(THelp, AccessOptionDescription) { + CLI::App app{}; + + int x; + auto opt = app.add_option("-a,--alpha", x, "My description goes here"); + + EXPECT_EQ(opt->get_description(), "My description goes here"); +} + +TEST(THelp, SetOptionDescriptionAfterCreation) { + CLI::App app{}; + + int x; + auto opt = app.add_option("-a,--alpha", x); + opt->description("My description goes here"); + + EXPECT_EQ(opt->get_description(), "My description goes here"); + EXPECT_THAT(app.help(), HasSubstr("My description goes here")); +} + TEST(THelp, CleanNeeds) { CLI::App app; @@ -622,10 +753,9 @@ TEST(THelp, ValidatorsText) { app.add_option("--f4", y)->check(CLI::Range(12)); std::string help = app.help(); - EXPECT_THAT(help, HasSubstr("FILE")); + EXPECT_THAT(help, HasSubstr("TEXT:FILE")); EXPECT_THAT(help, HasSubstr("INT in [1 - 4]")); - EXPECT_THAT(help, HasSubstr("INT in [0 - 12]")); // Loses UINT - EXPECT_THAT(help, Not(HasSubstr("TEXT"))); + EXPECT_THAT(help, HasSubstr("UINT:INT in [0 - 12]")); // Loses UINT } TEST(THelp, ValidatorsNonPathText) { @@ -635,8 +765,7 @@ TEST(THelp, ValidatorsNonPathText) { app.add_option("--f2", filename)->check(CLI::NonexistentPath); std::string help = app.help(); - EXPECT_THAT(help, HasSubstr("PATH")); - EXPECT_THAT(help, Not(HasSubstr("TEXT"))); + EXPECT_THAT(help, HasSubstr("TEXT:PATH")); } TEST(THelp, ValidatorsDirText) { @@ -646,8 +775,7 @@ TEST(THelp, ValidatorsDirText) { app.add_option("--f2", filename)->check(CLI::ExistingDirectory); std::string help = app.help(); - EXPECT_THAT(help, HasSubstr("DIR")); - EXPECT_THAT(help, Not(HasSubstr("TEXT"))); + EXPECT_THAT(help, HasSubstr("TEXT:DIR")); } TEST(THelp, ValidatorsPathText) { @@ -657,8 +785,7 @@ TEST(THelp, ValidatorsPathText) { app.add_option("--f2", filename)->check(CLI::ExistingPath); std::string help = app.help(); - EXPECT_THAT(help, HasSubstr("PATH")); - EXPECT_THAT(help, Not(HasSubstr("TEXT"))); + EXPECT_THAT(help, HasSubstr("TEXT:PATH")); } TEST(THelp, CombinedValidatorsText) { @@ -671,9 +798,8 @@ TEST(THelp, CombinedValidatorsText) { // Can't programatically tell! // (Users can use ExistingPath, by the way) std::string help = app.help(); - EXPECT_THAT(help, HasSubstr("TEXT")); + EXPECT_THAT(help, HasSubstr("TEXT:(FILE) OR (DIR)")); EXPECT_THAT(help, Not(HasSubstr("PATH"))); - EXPECT_THAT(help, Not(HasSubstr("FILE"))); } // Don't do this in real life, please @@ -685,17 +811,29 @@ TEST(THelp, CombinedValidatorsPathyText) { // Combining validators with the same type string is OK std::string help = app.help(); - EXPECT_THAT(help, Not(HasSubstr("TEXT"))); + EXPECT_THAT(help, HasSubstr("TEXT:")); EXPECT_THAT(help, HasSubstr("PATH")); } +// Don't do this in real life, please (and transform does nothing here) +TEST(THelp, CombinedValidatorsPathyTextAsTransform) { + CLI::App app; + + std::string filename; + app.add_option("--f1", filename)->transform(CLI::ExistingPath | CLI::NonexistentPath); + + // Combining validators with the same type string is OK + std::string help = app.help(); + EXPECT_THAT(help, HasSubstr("TEXT:(PATH(existing)) OR (PATH")); +} + // #113 Part 2 TEST(THelp, ChangingSet) { CLI::App app; std::set<int> vals{1, 2, 3}; int val; - app.add_set("--val", val, vals); + app.add_option("--val", val)->check(CLI::IsMember(&vals)); std::string help = app.help(); @@ -716,7 +854,7 @@ TEST(THelp, ChangingSetDefaulted) { std::set<int> vals{1, 2, 3}; int val = 2; - app.add_set("--val", val, vals, "", true); + app.add_option("--val", val, "", true)->check(CLI::IsMember(&vals)); std::string help = app.help(); @@ -736,7 +874,7 @@ TEST(THelp, ChangingCaselessSet) { std::set<std::string> vals{"1", "2", "3"}; std::string val; - app.add_set_ignore_case("--val", val, vals); + app.add_option("--val", val)->check(CLI::IsMember(&vals, CLI::ignore_case)); std::string help = app.help(); @@ -757,7 +895,7 @@ TEST(THelp, ChangingCaselessSetDefaulted) { std::set<std::string> vals{"1", "2", "3"}; std::string val = "2"; - app.add_set_ignore_case("--val", val, vals, "", true); + app.add_option("--val", val, "", true)->check(CLI::IsMember(&vals, CLI::ignore_case)); std::string help = app.help(); diff --git a/packages/CLI11/tests/HelpersTest.cpp b/packages/CLI11/tests/HelpersTest.cpp index d79354e78..afbefc9a7 100644 --- a/packages/CLI11/tests/HelpersTest.cpp +++ b/packages/CLI11/tests/HelpersTest.cpp @@ -8,7 +8,7 @@ TEST(Split, SimpleByToken) { auto out = CLI::detail::split("one.two.three", '.'); - ASSERT_EQ((size_t)3, out.size()); + ASSERT_EQ(3u, out.size()); EXPECT_EQ("one", out.at(0)); EXPECT_EQ("two", out.at(1)); EXPECT_EQ("three", out.at(2)); @@ -16,13 +16,13 @@ TEST(Split, SimpleByToken) { TEST(Split, Single) { auto out = CLI::detail::split("one", '.'); - ASSERT_EQ((size_t)1, out.size()); + ASSERT_EQ(1u, out.size()); EXPECT_EQ("one", out.at(0)); } TEST(Split, Empty) { auto out = CLI::detail::split("", '.'); - ASSERT_EQ((size_t)1, out.size()); + ASSERT_EQ(1u, out.size()); EXPECT_EQ("", out.at(0)); } @@ -32,6 +32,58 @@ TEST(String, InvalidName) { EXPECT_TRUE(CLI::detail::valid_name_string("va-li-d")); EXPECT_FALSE(CLI::detail::valid_name_string("vali&d")); EXPECT_TRUE(CLI::detail::valid_name_string("_valid")); + EXPECT_FALSE(CLI::detail::valid_name_string("/valid")); + EXPECT_TRUE(CLI::detail::valid_name_string("vali?d")); + EXPECT_TRUE(CLI::detail::valid_name_string("@@@@")); + EXPECT_TRUE(CLI::detail::valid_name_string("b@d2?")); + EXPECT_TRUE(CLI::detail::valid_name_string("2vali?d")); +} + +TEST(StringTools, Modify) { + int cnt = 0; + std::string newString = CLI::detail::find_and_modify("======", "=", [&cnt](std::string &str, size_t index) { + if((++cnt) % 2 == 0) { + str[index] = ':'; + } + return index + 1; + }); + EXPECT_EQ(newString, "=:=:=:"); +} + +TEST(StringTools, Modify2) { + std::string newString = + CLI::detail::find_and_modify("this is a string test", "is", [](std::string &str, size_t index) { + if((index > 1) && (str[index - 1] != ' ')) { + str[index] = 'a'; + str[index + 1] = 't'; + } + return index + 1; + }); + EXPECT_EQ(newString, "that is a string test"); +} + +TEST(StringTools, Modify3) { + // this picks up 3 sets of 3 after the 'b' then collapses the new first set + std::string newString = CLI::detail::find_and_modify("baaaaaaaaaa", "aaa", [](std::string &str, size_t index) { + str.erase(index, 3); + str.insert(str.begin(), 'a'); + return 0; + }); + EXPECT_EQ(newString, "aba"); +} + +TEST(StringTools, flagValues) { + EXPECT_EQ(CLI::detail::to_flag_value("0"), -1); + EXPECT_EQ(CLI::detail::to_flag_value("t"), 1); + EXPECT_EQ(CLI::detail::to_flag_value("1"), 1); + EXPECT_EQ(CLI::detail::to_flag_value("6"), 6); + EXPECT_EQ(CLI::detail::to_flag_value("-6"), -6); + EXPECT_EQ(CLI::detail::to_flag_value("false"), -1); + EXPECT_EQ(CLI::detail::to_flag_value("YES"), 1); + EXPECT_THROW(CLI::detail::to_flag_value("frog"), std::invalid_argument); + EXPECT_THROW(CLI::detail::to_flag_value("q"), std::invalid_argument); + EXPECT_EQ(CLI::detail::to_flag_value("NO"), -1); + EXPECT_EQ(CLI::detail::to_flag_value("475555233"), 475555233); } TEST(Trim, Various) { @@ -155,6 +207,53 @@ TEST(Validators, PathNotExistsDir) { EXPECT_NE(CLI::ExistingPath(mydir), ""); } +TEST(Validators, IPValidate1) { + std::string ip = "1.1.1.1"; + EXPECT_TRUE(CLI::ValidIPV4(ip).empty()); + ip = "224.255.0.1"; + EXPECT_TRUE(CLI::ValidIPV4(ip).empty()); + ip = "-1.255.0.1"; + EXPECT_FALSE(CLI::ValidIPV4(ip).empty()); + ip = "1.256.0.1"; + EXPECT_FALSE(CLI::ValidIPV4(ip).empty()); + ip = "1.256.0.1"; + EXPECT_FALSE(CLI::ValidIPV4(ip).empty()); + ip = "aaa"; + EXPECT_FALSE(CLI::ValidIPV4(ip).empty()); + ip = "11.22"; + EXPECT_FALSE(CLI::ValidIPV4(ip).empty()); +} + +TEST(Validators, PositiveValidator) { + std::string num = "1.1.1.1"; + EXPECT_FALSE(CLI::PositiveNumber(num).empty()); + num = "1"; + EXPECT_TRUE(CLI::PositiveNumber(num).empty()); + num = "10000"; + EXPECT_TRUE(CLI::PositiveNumber(num).empty()); + num = "0"; + EXPECT_TRUE(CLI::PositiveNumber(num).empty()); + num = "-1"; + EXPECT_FALSE(CLI::PositiveNumber(num).empty()); + num = "a"; + EXPECT_FALSE(CLI::PositiveNumber(num).empty()); +} + +TEST(Validators, NumberValidator) { + std::string num = "1.1.1.1"; + EXPECT_FALSE(CLI::Number(num).empty()); + num = "1.7"; + EXPECT_TRUE(CLI::Number(num).empty()); + num = "10000"; + EXPECT_TRUE(CLI::Number(num).empty()); + num = "-0.000"; + EXPECT_TRUE(CLI::Number(num).empty()); + num = "+1.55"; + EXPECT_TRUE(CLI::Number(num).empty()); + num = "a"; + EXPECT_FALSE(CLI::Number(num).empty()); +} + TEST(Validators, CombinedAndRange) { auto crange = CLI::Range(0, 12) & CLI::Range(4, 16); EXPECT_TRUE(crange("4").empty()); @@ -212,6 +311,36 @@ TEST(Validators, CombinedPaths) { EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); } +TEST(Validators, ProgramNameSplit) { + TempFile myfile{"program_name1.exe"}; + { + std::ofstream out{myfile}; + out << "useless string doesn't matter" << std::endl; + } + auto res = + CLI::detail::split_program_name(std::string("./") + std::string(myfile) + " this is a bunch of extra stuff "); + EXPECT_EQ(res.first, std::string("./") + std::string(myfile)); + EXPECT_EQ(res.second, "this is a bunch of extra stuff"); + + TempFile myfile2{"program name1.exe"}; + { + std::ofstream out{myfile2}; + out << "useless string doesn't matter" << std::endl; + } + res = CLI::detail::split_program_name(std::string(" ") + std::string("./") + std::string(myfile2) + + " this is a bunch of extra stuff "); + EXPECT_EQ(res.first, std::string("./") + std::string(myfile2)); + EXPECT_EQ(res.second, "this is a bunch of extra stuff"); + + res = CLI::detail::split_program_name("./program_name this is a bunch of extra stuff "); + EXPECT_EQ(res.first, "./program_name"); // test sectioning of first argument even if it can't detect the file + EXPECT_EQ(res.second, "this is a bunch of extra stuff"); + + res = CLI::detail::split_program_name(std::string(" ./") + std::string(myfile) + " "); + EXPECT_EQ(res.first, std::string("./") + std::string(myfile)); + EXPECT_TRUE(res.second.empty()); +} + // Yes, this is testing an app_helper :) TEST(AppHelper, TempfileCreated) { std::string name = "TestFileNotUsed.txt"; @@ -356,6 +485,20 @@ TEST(SplitUp, Simple) { EXPECT_EQ(oput, result); } +TEST(SplitUp, SimpleDifferentQuotes) { + std::vector<std::string> oput = {"one", "two three"}; + std::string orig{R"(one `two three`)"}; + std::vector<std::string> result = CLI::detail::split_up(orig); + EXPECT_EQ(oput, result); +} + +TEST(SplitUp, SimpleDifferentQuotes2) { + std::vector<std::string> oput = {"one", "two three"}; + std::string orig{R"(one 'two three')"}; + std::vector<std::string> result = CLI::detail::split_up(orig); + EXPECT_EQ(oput, result); +} + TEST(SplitUp, Layered) { std::vector<std::string> output = {R"(one 'two three')"}; std::string orig{R"("one 'two three'")"}; @@ -403,6 +546,10 @@ TEST(Types, TypeName) { std::string text2_name = CLI::detail::type_name<char *>(); EXPECT_EQ("TEXT", text2_name); + + enum class test { test1, test2, test3 }; + std::string enum_name = CLI::detail::type_name<test>(); + EXPECT_EQ("ENUM", enum_name); } TEST(Types, OverflowSmall) { @@ -458,6 +605,20 @@ TEST(Types, LexicalCastDouble) { EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, x)); } +TEST(Types, LexicalCastBool) { + std::string input = "false"; + bool x; + EXPECT_TRUE(CLI::detail::lexical_cast(input, x)); + EXPECT_FALSE(x); + + std::string bad_input = "happy"; + EXPECT_FALSE(CLI::detail::lexical_cast(bad_input, x)); + + std::string input_true = "EnaBLE"; + EXPECT_TRUE(CLI::detail::lexical_cast(input_true, x)); + EXPECT_TRUE(x); +} + TEST(Types, LexicalCastString) { std::string input = "one"; std::string output; @@ -479,6 +640,25 @@ TEST(Types, LexicalCastParsable) { EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, output)); } +TEST(Types, LexicalCastEnum) { + enum t1 : char { v1 = 5, v3 = 7, v5 = -9 }; + + t1 output; + EXPECT_TRUE(CLI::detail::lexical_cast("-9", output)); + EXPECT_EQ(output, v5); + + EXPECT_FALSE(CLI::detail::lexical_cast("invalid", output)); + enum class t2 : uint64_t { enum1 = 65, enum2 = 45667, enum3 = 9999999999999 }; + t2 output2; + EXPECT_TRUE(CLI::detail::lexical_cast("65", output2)); + EXPECT_EQ(output2, t2::enum1); + + EXPECT_FALSE(CLI::detail::lexical_cast("invalid", output2)); + + EXPECT_TRUE(CLI::detail::lexical_cast("9999999999999", output2)); + EXPECT_EQ(output2, t2::enum3); +} + TEST(FixNewLines, BasicCheck) { std::string input = "one\ntwo"; std::string output = "one\n; two"; diff --git a/packages/CLI11/tests/IniTest.cpp b/packages/CLI11/tests/IniTest.cpp index 24d24ba48..97c573766 100644 --- a/packages/CLI11/tests/IniTest.cpp +++ b/packages/CLI11/tests/IniTest.cpp @@ -24,12 +24,12 @@ TEST(StringBased, First) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)2, output.size()); + EXPECT_EQ(2u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -45,12 +45,12 @@ TEST(StringBased, FirstWithComments) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)2, output.size()); + EXPECT_EQ(2u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -65,15 +65,15 @@ TEST(StringBased, Quotes) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)3, output.size()); + EXPECT_EQ(3u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); EXPECT_EQ("five", output.at(2).name); - EXPECT_EQ((size_t)1, output.at(2).inputs.size()); + EXPECT_EQ(1u, output.at(2).inputs.size()); EXPECT_EQ("six and seven", output.at(2).inputs.at(0)); } @@ -88,15 +88,15 @@ TEST(StringBased, Vector) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)3, output.size()); + EXPECT_EQ(3u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); EXPECT_EQ("five", output.at(2).name); - EXPECT_EQ((size_t)3, output.at(2).inputs.size()); + EXPECT_EQ(3u, output.at(2).inputs.size()); EXPECT_EQ("six", output.at(2).inputs.at(0)); EXPECT_EQ("and", output.at(2).inputs.at(1)); EXPECT_EQ("seven", output.at(2).inputs.at(2)); @@ -112,12 +112,12 @@ TEST(StringBased, Spaces) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)2, output.size()); + EXPECT_EQ(2u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -132,13 +132,13 @@ TEST(StringBased, Sections) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)2, output.size()); + EXPECT_EQ(2u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); EXPECT_EQ("second", output.at(1).parents.at(0)); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); EXPECT_EQ("second.two", output.at(1).fullname()); } @@ -156,14 +156,14 @@ TEST(StringBased, SpacesSections) { std::vector<CLI::ConfigItem> output = CLI::ConfigINI().from_config(ofile); - EXPECT_EQ((size_t)2, output.size()); + EXPECT_EQ(2u, output.size()); EXPECT_EQ("one", output.at(0).name); - EXPECT_EQ((size_t)1, output.at(0).inputs.size()); + EXPECT_EQ(1u, output.at(0).inputs.size()); EXPECT_EQ("three", output.at(0).inputs.at(0)); EXPECT_EQ("two", output.at(1).name); - EXPECT_EQ((size_t)1, output.at(1).parents.size()); + EXPECT_EQ(1u, output.at(1).parents.size()); EXPECT_EQ("second", output.at(1).parents.at(0)); - EXPECT_EQ((size_t)1, output.at(1).inputs.size()); + EXPECT_EQ(1u, output.at(1).inputs.size()); EXPECT_EQ("four", output.at(1).inputs.at(0)); } @@ -256,7 +256,7 @@ TEST_F(TApp, IniGetNoRemaining) { int two = 0; app.add_option("--two", two); ASSERT_NO_THROW(run()); - EXPECT_EQ(app.remaining().size(), (size_t)0); + EXPECT_EQ(app.remaining().size(), 0u); } TEST_F(TApp, IniNotRequiredNotDefault) { @@ -516,8 +516,13 @@ TEST_F(TApp, IniFlagConvertFailure) { std::ofstream out{tmpini}; out << "flag=moobook" << std::endl; } - - EXPECT_THROW(run(), CLI::ConversionError); + run(); + bool result; + auto *opt = app.get_option("--flag"); + EXPECT_THROW(opt->results(result), CLI::ConversionError); + std::string res; + opt->results(res); + EXPECT_EQ(res, "moobook"); } TEST_F(TApp, IniFlagNumbers) { @@ -603,9 +608,110 @@ TEST_F(TApp, IniFlags) { run(); EXPECT_EQ(2, two); - EXPECT_EQ(true, three); - EXPECT_EQ(true, four); - EXPECT_EQ(true, five); + EXPECT_TRUE(three); + EXPECT_TRUE(four); + EXPECT_TRUE(five); +} + +TEST_F(TApp, IniFalseFlags) { + TempFile tmpini{"TestIniTmp.ini"}; + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=-2" << std::endl; + out << "three=false" << std::endl; + out << "four=1" << std::endl; + out << "five" << std::endl; + } + + int two; + bool three, four, five; + app.add_flag("--two", two); + app.add_flag("--three", three); + app.add_flag("--four", four); + app.add_flag("--five", five); + + run(); + + EXPECT_EQ(-2, two); + EXPECT_FALSE(three); + EXPECT_TRUE(four); + EXPECT_TRUE(five); +} + +TEST_F(TApp, IniFalseFlagsDef) { + TempFile tmpini{"TestIniTmp.ini"}; + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=2" << std::endl; + out << "three=true" << std::endl; + out << "four=on" << std::endl; + out << "five" << std::endl; + } + + int two; + bool three, four, five; + app.add_flag("--two{false}", two); + app.add_flag("--three", three); + app.add_flag("!--four", four); + app.add_flag("--five", five); + + run(); + + EXPECT_EQ(-2, two); + EXPECT_TRUE(three); + EXPECT_FALSE(four); + EXPECT_TRUE(five); +} + +TEST_F(TApp, IniFalseFlagsDefDisableOverrideError) { + TempFile tmpini{"TestIniTmp.ini"}; + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=2" << std::endl; + out << "four=on" << std::endl; + out << "five" << std::endl; + } + + int two; + bool four, five; + app.add_flag("--two{false}", two)->disable_flag_override(); + app.add_flag("!--four", four); + app.add_flag("--five", five); + + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, IniFalseFlagsDefDisableOverrideSuccess) { + TempFile tmpini{"TestIniTmp.ini"}; + app.set_config("--config", tmpini); + + { + std::ofstream out{tmpini}; + out << "[default]" << std::endl; + out << "two=2" << std::endl; + out << "four={}" << std::endl; + out << "val=15" << std::endl; + } + + int two, four, val; + app.add_flag("--two{2}", two)->disable_flag_override(); + app.add_flag("--four{4}", four)->disable_flag_override(); + app.add_flag("--val", val); + + run(); + + EXPECT_EQ(2, two); + EXPECT_EQ(4, four); + EXPECT_EQ(15, val); } TEST_F(TApp, IniOutputSimple) { @@ -637,7 +743,7 @@ TEST_F(TApp, IniOutputNoConfigurable) { TEST_F(TApp, IniOutputShortSingleDescription) { std::string flag = "some_flag"; - std::string description = "Some short description."; + const std::string description = "Some short description."; app.add_flag("--" + flag, description); run(); @@ -649,8 +755,8 @@ TEST_F(TApp, IniOutputShortSingleDescription) { TEST_F(TApp, IniOutputShortDoubleDescription) { std::string flag1 = "flagnr1"; std::string flag2 = "flagnr2"; - std::string description1 = "First description."; - std::string description2 = "Second description."; + const std::string description1 = "First description."; + const std::string description2 = "Second description."; app.add_flag("--" + flag1, description1); app.add_flag("--" + flag2, description2); @@ -662,7 +768,7 @@ TEST_F(TApp, IniOutputShortDoubleDescription) { TEST_F(TApp, IniOutputMultiLineDescription) { std::string flag = "some_flag"; - std::string description = "Some short description.\nThat has lines."; + const std::string description = "Some short description.\nThat has lines."; app.add_flag("--" + flag, description); run(); @@ -711,7 +817,7 @@ TEST_F(TApp, IniOutputFlag) { TEST_F(TApp, IniOutputSet) { int v; - app.add_set("--simple", v, {1, 2, 3}); + app.add_option("--simple", v)->check(CLI::IsMember({1, 2, 3})); args = {"--simple=2"}; @@ -768,3 +874,18 @@ TEST_F(TApp, IniQuotedOutput) { EXPECT_THAT(str, HasSubstr("val1=\"I am a string\"")); EXPECT_THAT(str, HasSubstr("val2='I am a \"confusing\" string'")); } + +TEST_F(TApp, DefaultsIniQuotedOutput) { + + std::string val1{"I am a string"}; + app.add_option("--val1", val1, "", true); + + std::string val2{R"(I am a "confusing" string)"}; + app.add_option("--val2", val2, "", true); + + run(); + + std::string str = app.config_to_str(true); + EXPECT_THAT(str, HasSubstr("val1=\"I am a string\"")); + EXPECT_THAT(str, HasSubstr("val2='I am a \"confusing\" string'")); +} diff --git a/packages/CLI11/tests/NewParseTest.cpp b/packages/CLI11/tests/NewParseTest.cpp index 68c6adfd7..89e7fbeea 100644 --- a/packages/CLI11/tests/NewParseTest.cpp +++ b/packages/CLI11/tests/NewParseTest.cpp @@ -77,6 +77,38 @@ TEST_F(TApp, BuiltinComplex) { EXPECT_DOUBLE_EQ(3, comp.imag()); } +TEST_F(TApp, BuiltinComplexWithDelimiter) { + cx comp{1, 2}; + app.add_complex("-c,--complex", comp, "", true)->delimiter('+'); + + args = {"-c", "4+3i"}; + + std::string help = app.help(); + EXPECT_THAT(help, HasSubstr("1")); + EXPECT_THAT(help, HasSubstr("2")); + EXPECT_THAT(help, HasSubstr("COMPLEX")); + + EXPECT_DOUBLE_EQ(1, comp.real()); + EXPECT_DOUBLE_EQ(2, comp.imag()); + + run(); + + EXPECT_DOUBLE_EQ(4, comp.real()); + EXPECT_DOUBLE_EQ(3, comp.imag()); + + args = {"-c", "5+-3i"}; + run(); + + EXPECT_DOUBLE_EQ(5, comp.real()); + EXPECT_DOUBLE_EQ(-3, comp.imag()); + + args = {"-c", "6", "-4i"}; + run(); + + EXPECT_DOUBLE_EQ(6, comp.real()); + EXPECT_DOUBLE_EQ(-4, comp.imag()); +} + TEST_F(TApp, BuiltinComplexIgnoreI) { cx comp{1, 2}; app.add_complex("-c,--complex", comp); @@ -97,3 +129,133 @@ TEST_F(TApp, BuiltinComplexFail) { EXPECT_THROW(run(), CLI::ArgumentMismatch); } + +// an example of custom converter that can be used to add new parsing options +// On MSVC and possibly some other new compilers this can be a free standing function without the template +// specialization but this is compiler dependent +namespace CLI { +namespace detail { + +template <> +bool lexical_cast<std::pair<std::string, std::string>>(std::string input, std::pair<std::string, std::string> &output) { + + auto sep = input.find_first_of(':'); + if((sep == std::string::npos) && (sep > 0)) { + return false; + } + output = {input.substr(0, sep), input.substr(sep + 1)}; + return true; +} +} // namespace detail +} // namespace CLI + +TEST_F(TApp, custom_string_converter) { + std::pair<std::string, std::string> val; + app.add_option("-d,--dual_string", val); + + args = {"-d", "string1:string2"}; + + run(); + EXPECT_EQ(val.first, "string1"); + EXPECT_EQ(val.second, "string2"); +} + +TEST_F(TApp, custom_string_converterFail) { + std::pair<std::string, std::string> val; + app.add_option("-d,--dual_string", val); + + args = {"-d", "string2"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +// an example of custom complex number converter that can be used to add new parsing options +#if defined(__has_include) +#if __has_include(<regex>) +// an example of custom converter that can be used to add new parsing options +#define HAS_REGEX_INCLUDE +#endif +#endif + +#ifdef HAS_REGEX_INCLUDE +// Gcc 4.8 and older and the corresponding standard libraries have a broken <regex> so this would +// fail. And if a clang compiler is using libstd++ then this will generate an error as well so this is just a check to +// simplify compilation and prevent a much more complicated #if expression +#include <regex> +namespace CLI { +namespace detail { + +// On MSVC and possibly some other new compilers this can be a free standing function without the template +// specialization but this is compiler dependent +template <> bool lexical_cast<std::complex<double>>(std::string input, std::complex<double> &output) { + // regular expression to handle complex numbers of various formats + static const std::regex creg( + R"(([+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?)\s*([+-]\s*(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?)[ji]*)"); + + std::smatch m; + double x = 0.0, y = 0.0; + bool worked; + std::regex_search(input, m, creg); + if(m.size() == 9) { + worked = CLI::detail::lexical_cast(m[1], x) && CLI::detail::lexical_cast(m[6], y); + if(worked) { + if(*m[5].first == '-') { + y = -y; + } + } + } else { + if((input.back() == 'j') || (input.back() == 'i')) { + auto strval = input.substr(0, input.size() - 1); + CLI::detail::trim(strval); + worked = CLI::detail::lexical_cast(strval, y); + } else { + CLI::detail::trim(input); + worked = CLI::detail::lexical_cast(input, x); + } + } + if(worked) { + output = cx{x, y}; + } + return worked; +} +} // namespace detail +} // namespace CLI + +TEST_F(TApp, AddingComplexParserDetail) { + + bool skip_tests = false; + try { // check if the library actually supports regex, it is possible to link against a non working regex in the + // standard library + std::smatch m; + std::string input = "1.5+2.5j"; + static const std::regex creg( + R"(([+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?)\s*([+-]\s*(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?)[ji]*)"); + + auto rsearch = std::regex_search(input, m, creg); + if(!rsearch) { + skip_tests = true; + } else { + EXPECT_EQ(m.size(), 9u); + } + + } catch(...) { + skip_tests = true; + } + if(!skip_tests) { + cx comp{0, 0}; + app.add_option("-c,--complex", comp, "add a complex number option"); + args = {"-c", "1.5+2.5j"}; + + run(); + + EXPECT_DOUBLE_EQ(1.5, comp.real()); + EXPECT_DOUBLE_EQ(2.5, comp.imag()); + args = {"-c", "1.5-2.5j"}; + + run(); + + EXPECT_DOUBLE_EQ(1.5, comp.real()); + EXPECT_DOUBLE_EQ(-2.5, comp.imag()); + } +} +#endif diff --git a/packages/CLI11/tests/OptionGroupTest.cpp b/packages/CLI11/tests/OptionGroupTest.cpp new file mode 100644 index 000000000..e6de7014e --- /dev/null +++ b/packages/CLI11/tests/OptionGroupTest.cpp @@ -0,0 +1,720 @@ +#include "app_helper.hpp" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::HasSubstr; +using ::testing::Not; + +using vs_t = std::vector<std::string>; + +TEST_F(TApp, BasicOptionGroup) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + EXPECT_EQ(app.count_all(), 1u); +} + +TEST_F(TApp, BasicOptionGroupExact) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(1); + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + + args = {"--test1", "5", "--test2", "4"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[Exactly 1"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupExactTooMany) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(10); + args = {"--test1", "5"}; + EXPECT_THROW(run(), CLI::InvalidError); +} + +TEST_F(TApp, BasicOptionGroupMinMax) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(1, 1); + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + + args = {"--test1", "5", "--test2", "4"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[Exactly 1"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMinMaxDifferent) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(1, 2); + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + + args = {"--test1", "5", "--test2", "4"}; + EXPECT_NO_THROW(run()); + EXPECT_EQ(app.count_all(), 2u); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[Between 1 and 2"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMinMaxDifferentReversed) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(2, 1); + EXPECT_EQ(ogroup->get_require_option_min(), 2u); + EXPECT_EQ(ogroup->get_require_option_max(), 1u); + args = {"--test1", "5"}; + EXPECT_THROW(run(), CLI::InvalidError); + ogroup->require_option(1, 2); + EXPECT_NO_THROW(run()); + EXPECT_EQ(res, 5); + EXPECT_EQ(ogroup->get_require_option_min(), 1u); + EXPECT_EQ(ogroup->get_require_option_max(), 2u); + args = {"--test1", "5", "--test2", "4"}; + EXPECT_NO_THROW(run()); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[Between 1 and 2"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMax) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(-2); + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + + args = {"--option", "9"}; + EXPECT_NO_THROW(run()); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[At most 2"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMax1) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(-1); + args = {"--test1", "5"}; + run(); + EXPECT_EQ(res, 5); + + args = {"--option", "9"}; + EXPECT_NO_THROW(run()); + + args = {"--test1", "5", "--test2", "4"}; + EXPECT_THROW(run(), CLI::RequiredError); + + std::string help = ogroup->help(); + auto exactloc = help.find("[At most 1"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMin) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_NO_THROW(run()); + + std::string help = ogroup->help(); + auto exactloc = help.find("[At least 1"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupExact2) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(2); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test3=5"}; + EXPECT_NO_THROW(run()); + + std::string help = ogroup->help(); + auto exactloc = help.find("[Exactly 2"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMin2) { + auto ogroup = app.add_option_group("clusters"); + int res; + ogroup->add_option("--test1", res); + ogroup->add_option("--test2", res); + ogroup->add_option("--test3", res); + int val2; + app.add_option("--option", val2); + ogroup->require_option(2, 0); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_NO_THROW(run()); + + std::string help = ogroup->help(); + auto exactloc = help.find("[At least 2"); + EXPECT_NE(exactloc, std::string::npos); +} + +TEST_F(TApp, BasicOptionGroupMinMoved) { + + int res; + auto opt1 = app.add_option("--test1", res); + auto opt2 = app.add_option("--test2", res); + auto opt3 = app.add_option("--test3", res); + int val2; + app.add_option("--option", val2); + + auto ogroup = app.add_option_group("clusters"); + ogroup->require_option(); + ogroup->add_option(opt1); + ogroup->add_option(opt2); + ogroup->add_option(opt3); + + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_NO_THROW(run()); + + std::string help = app.help(); + auto exactloc = help.find("[At least 1"); + auto oloc = help.find("--test1"); + EXPECT_NE(exactloc, std::string::npos); + EXPECT_NE(oloc, std::string::npos); + EXPECT_LT(exactloc, oloc); +} + +TEST_F(TApp, BasicOptionGroupMinMovedAsGroup) { + + int res; + auto opt1 = app.add_option("--test1", res); + auto opt2 = app.add_option("--test2", res); + auto opt3 = app.add_option("--test3", res); + int val2; + app.add_option("--option", val2); + + auto ogroup = app.add_option_group("clusters"); + ogroup->require_option(); + ogroup->add_options(opt1, opt2, opt3); + + EXPECT_THROW(ogroup->add_options(opt1), CLI::OptionNotFound); + args = {"--option", "9"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--test1", "5", "--test2", "4", "--test3=5"}; + EXPECT_NO_THROW(run()); + + std::string help = app.help(); + auto exactloc = help.find("[At least 1"); + auto oloc = help.find("--test1"); + EXPECT_NE(exactloc, std::string::npos); + EXPECT_NE(oloc, std::string::npos); + EXPECT_LT(exactloc, oloc); +} + +TEST_F(TApp, BasicOptionGroupAddFailures) { + + int res; + auto opt1 = app.add_option("--test1", res); + app.set_config("--config"); + int val2; + app.add_option("--option", val2); + + auto ogroup = app.add_option_group("clusters"); + EXPECT_THROW(ogroup->add_options(app.get_config_ptr()), CLI::OptionAlreadyAdded); + EXPECT_THROW(ogroup->add_options(app.get_help_ptr()), CLI::OptionAlreadyAdded); + + auto sub = app.add_subcommand("sub", "subcommand"); + auto opt2 = sub->add_option("--option2", val2); + + EXPECT_THROW(ogroup->add_option(opt2), CLI::OptionNotFound); + + EXPECT_THROW(ogroup->add_options(nullptr), CLI::OptionNotFound); + + ogroup->add_option(opt1); + + auto opt3 = app.add_option("--test1", res); + + EXPECT_THROW(ogroup->add_option(opt3), CLI::OptionAlreadyAdded); +} + +TEST_F(TApp, BasicOptionGroupScrewedUpMove) { + + int res; + auto opt1 = app.add_option("--test1", res); + auto opt2 = app.add_option("--test2", res); + int val2; + app.add_option("--option", val2); + + auto ogroup = app.add_option_group("clusters"); + ogroup->require_option(); + auto ogroup2 = ogroup->add_option_group("clusters2"); + EXPECT_THROW(ogroup2->add_options(opt1, opt2), CLI::OptionNotFound); + + CLI::Option_group EmptyGroup("description", "new group", nullptr); + + EXPECT_THROW(EmptyGroup.add_option(opt2), CLI::OptionNotFound); + EXPECT_THROW(app._move_option(opt2, ogroup2), CLI::OptionNotFound); +} + +TEST_F(TApp, InvalidOptions) { + auto ogroup = app.add_option_group("clusters"); + CLI::Option *opt = nullptr; + EXPECT_THROW(ogroup->excludes(opt), CLI::OptionNotFound); + CLI::App *app_p = nullptr; + EXPECT_THROW(ogroup->excludes(app_p), CLI::OptionNotFound); + EXPECT_THROW(ogroup->excludes(ogroup), CLI::OptionNotFound); + EXPECT_THROW(ogroup->add_option(opt), CLI::OptionNotFound); +} + +struct ManyGroups : public TApp { + + CLI::Option_group *main; + CLI::Option_group *g1; + CLI::Option_group *g2; + CLI::Option_group *g3; + std::string name1; + std::string name2; + std::string name3; + std::string val1; + std::string val2; + std::string val3; + ManyGroups() { + main = app.add_option_group("main", "the main outer group"); + g1 = main->add_option_group("g1", "group1 description"); + g2 = main->add_option_group("g2", "group2 description"); + g3 = main->add_option_group("g3", "group3 description"); + g1->add_option("--name1", name1)->required(); + g1->add_option("--val1", val1); + g2->add_option("--name2", name2)->required(); + g2->add_option("--val2", val2); + g3->add_option("--name3", name3)->required(); + g3->add_option("--val3", val3); + } + + void remove_required() { + g1->get_option("--name1")->required(false); + g2->get_option("--name2")->required(false); + g3->get_option("--name3")->required(false); + g1->required(false); + g2->required(false); + g3->required(false); + } +}; + +TEST_F(ManyGroups, SingleGroup) { + // only 1 group can be used + main->require_option(1); + args = {"--name1", "test"}; + run(); + EXPECT_EQ(name1, "test"); + + args = {"--name2", "test", "--val2", "tval"}; + + run(); + EXPECT_EQ(val2, "tval"); + + args = {"--name1", "test", "--val2", "tval"}; + + EXPECT_THROW(run(), CLI::RequiredError); +} + +TEST_F(ManyGroups, ExcludesGroup) { + // only 1 group can be used + g1->excludes(g2); + g1->excludes(g3); + args = {"--name1", "test"}; + run(); + EXPECT_EQ(name1, "test"); + + args = {"--name1", "test", "--name2", "test2"}; + + EXPECT_THROW(run(), CLI::ExcludesError); + + EXPECT_TRUE(g1->remove_excludes(g2)); + EXPECT_NO_THROW(run()); + EXPECT_FALSE(g1->remove_excludes(g1)); + EXPECT_FALSE(g1->remove_excludes(g2)); +} + +TEST_F(ManyGroups, SingleGroupError) { + // only 1 group can be used + main->require_option(1); + args = {"--name1", "test", "--name2", "test3"}; + EXPECT_THROW(run(), CLI::RequiredError); +} + +TEST_F(ManyGroups, AtMostOneGroup) { + // only 1 group can be used + main->require_option(0, 1); + args = {"--name1", "test", "--name2", "test3"}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManyGroups, AtLeastTwoGroups) { + // only 1 group can be used + main->require_option(2, 0); + args = {"--name1", "test", "--name2", "test3"}; + run(); + + args = {"--name1", "test"}; + EXPECT_THROW(run(), CLI::RequiredError); +} + +TEST_F(ManyGroups, BetweenOneAndTwoGroups) { + // only 1 group can be used + main->require_option(1, 2); + args = {"--name1", "test", "--name2", "test3"}; + run(); + + args = {"--name1", "test"}; + run(); + + args = {}; + EXPECT_THROW(run(), CLI::RequiredError); + + args = {"--name1", "test", "--name2", "test3", "--name3=test3"}; + EXPECT_THROW(run(), CLI::RequiredError); +} + +TEST_F(ManyGroups, RequiredFirst) { + // only 1 group can be used + remove_required(); + g1->required(); + + EXPECT_TRUE(g1->get_required()); + EXPECT_FALSE(g2->get_required()); + args = {"--name1", "test", "--name2", "test3"}; + run(); + + args = {"--name2", "test"}; + try { + run(); + } catch(const CLI::RequiredError &re) { + EXPECT_THAT(re.what(), HasSubstr("g1")); + } + + args = {"--name1", "test", "--name2", "test3", "--name3=test3"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManyGroups, DisableFirst) { + // only 1 group can be used + remove_required(); + g1->disabled(); + + EXPECT_TRUE(g1->get_disabled()); + EXPECT_FALSE(g2->get_disabled()); + args = {"--name2", "test"}; + + run(); + + args = {"--name1", "test", "--name2", "test3"}; + EXPECT_THROW(run(), CLI::ExtrasError); + g1->disabled(false); + args = {"--name1", "test", "--name2", "test3", "--name3=test3"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManyGroups, SameSubcommand) { + // only 1 group can be used + remove_required(); + auto sub1 = g1->add_subcommand("sub1"); + auto sub2 = g2->add_subcommand("sub1"); + auto sub3 = g3->add_subcommand("sub1"); + + args = {"sub1", "sub1", "sub1"}; + + run(); + + EXPECT_TRUE(*sub1); + EXPECT_TRUE(*sub2); + EXPECT_TRUE(*sub3); + /// This should be made to work at some point + auto subs = app.get_subcommands(); + EXPECT_EQ(subs.size(), 3u); + EXPECT_EQ(subs[0], sub1); + EXPECT_EQ(subs[1], sub2); + EXPECT_EQ(subs[2], sub3); + + args = {"sub1", "sub1", "sub1", "sub1"}; + // for the 4th and future ones they will route to the first one + run(); + EXPECT_EQ(sub1->count(), 2u); + EXPECT_EQ(sub2->count(), 1u); + EXPECT_EQ(sub3->count(), 1u); + + // subs should remain the same since the duplicate would not be registered there + subs = app.get_subcommands(); + EXPECT_EQ(subs.size(), 3u); + EXPECT_EQ(subs[0], sub1); + EXPECT_EQ(subs[1], sub2); + EXPECT_EQ(subs[2], sub3); +} +TEST_F(ManyGroups, CallbackOrder) { + // only 1 group can be used + remove_required(); + std::vector<int> callback_order; + g1->callback([&callback_order]() { callback_order.push_back(1); }); + g2->callback([&callback_order]() { callback_order.push_back(2); }); + main->callback([&callback_order]() { callback_order.push_back(3); }); + + args = {"--name2", "test"}; + run(); + EXPECT_EQ(callback_order, std::vector<int>({2, 3})); + + callback_order.clear(); + args = {"--name1", "t2", "--name2", "test"}; + g2->immediate_callback(); + run(); + EXPECT_EQ(callback_order, std::vector<int>({2, 1, 3})); + callback_order.clear(); + + args = {"--name2", "test", "--name1", "t2"}; + g2->immediate_callback(false); + run(); + EXPECT_EQ(callback_order, std::vector<int>({1, 2, 3})); +} + +// Test the fallthrough for extra arguments +TEST_F(ManyGroups, ExtrasFallDown) { + remove_required(); + + args = {"--test1", "--flag", "extra"}; + EXPECT_THROW(run(), CLI::ExtrasError); + main->allow_extras(); + EXPECT_NO_THROW(run()); + + EXPECT_EQ(app.remaining_size(true), 3u); + EXPECT_EQ(main->remaining_size(), 3u); + + std::vector<std::string> extras{"--test1", "--flag", "extra"}; + EXPECT_EQ(app.remaining(true), extras); + EXPECT_EQ(main->remaining(), extras); +} + +// Test the option Inheritance +TEST_F(ManyGroups, Inheritance) { + remove_required(); + g1->ignore_case(); + g1->ignore_underscore(); + auto t2 = g1->add_subcommand("t2"); + args = {"T2", "t_2"}; + EXPECT_TRUE(t2->get_ignore_underscore()); + EXPECT_TRUE(t2->get_ignore_case()); + run(); + EXPECT_EQ(t2->count(), 2u); +} + +TEST_F(ManyGroups, Moving) { + remove_required(); + auto mg = app.add_option_group("maing"); + mg->add_subcommand(g1); + mg->add_subcommand(g2); + + EXPECT_EQ(g1->get_parent(), mg); + EXPECT_EQ(g2->get_parent(), mg); + EXPECT_EQ(g3->get_parent(), main); +} + +struct ManyGroupsPreTrigger : public ManyGroups { + size_t triggerMain, trigger1{87u}, trigger2{34u}, trigger3{27u}; + ManyGroupsPreTrigger() { + remove_required(); + app.preparse_callback([this](size_t count) { triggerMain = count; }); + + g1->preparse_callback([this](size_t count) { trigger1 = count; }); + g2->preparse_callback([this](size_t count) { trigger2 = count; }); + g3->preparse_callback([this](size_t count) { trigger3 = count; }); + } +}; + +TEST_F(ManyGroupsPreTrigger, PreTriggerTestsOptions) { + + args = {"--name1", "test", "--name2", "test3"}; + run(); + EXPECT_EQ(triggerMain, 4u); + EXPECT_EQ(trigger1, 2u); + EXPECT_EQ(trigger2, 0u); + EXPECT_EQ(trigger3, 27u); + + args = {"--name1", "test"}; + trigger2 = 34u; + run(); + EXPECT_EQ(triggerMain, 2u); + EXPECT_EQ(trigger1, 0u); + EXPECT_EQ(trigger2, 34u); + + args = {}; + run(); + EXPECT_EQ(triggerMain, 0u); + + args = {"--name1", "test", "--val1", "45", "--name2", "test3", "--name3=test3", "--val2=37"}; + run(); + EXPECT_EQ(triggerMain, 8u); + EXPECT_EQ(trigger1, 6u); + EXPECT_EQ(trigger2, 2u); + EXPECT_EQ(trigger3, 1u); +} + +TEST_F(ManyGroupsPreTrigger, PreTriggerTestsPositionals) { + // only 1 group can be used + g1->add_option("pos1"); + g2->add_option("pos2"); + g3->add_option("pos3"); + + args = {"pos1"}; + run(); + EXPECT_EQ(triggerMain, 1u); + EXPECT_EQ(trigger1, 0u); + EXPECT_EQ(trigger2, 34u); + EXPECT_EQ(trigger3, 27u); + + args = {"pos1", "pos2"}; + run(); + EXPECT_EQ(triggerMain, 2u); + EXPECT_EQ(trigger1, 1u); + EXPECT_EQ(trigger2, 0u); + + args = {"pos1", "pos2", "pos3"}; + run(); + EXPECT_EQ(triggerMain, 3u); + EXPECT_EQ(trigger1, 2u); + EXPECT_EQ(trigger2, 1u); + EXPECT_EQ(trigger3, 0u); +} + +TEST_F(ManyGroupsPreTrigger, PreTriggerTestsSubcommand) { + + auto sub1 = g1->add_subcommand("sub1")->fallthrough(); + g2->add_subcommand("sub2")->fallthrough(); + g3->add_subcommand("sub3")->fallthrough(); + + size_t subtrigger; + sub1->preparse_callback([&subtrigger](size_t count) { subtrigger = count; }); + args = {"sub1"}; + run(); + EXPECT_EQ(triggerMain, 1u); + EXPECT_EQ(trigger1, 0u); + EXPECT_EQ(trigger2, 34u); + EXPECT_EQ(trigger3, 27u); + + args = {"sub1", "sub2"}; + run(); + EXPECT_EQ(triggerMain, 2u); + EXPECT_EQ(subtrigger, 1u); + EXPECT_EQ(trigger1, 1u); + EXPECT_EQ(trigger2, 0u); + + args = {"sub2", "sub3", "--name1=test", "sub1"}; + run(); + EXPECT_EQ(triggerMain, 4u); + EXPECT_EQ(trigger1, 1u); + EXPECT_EQ(trigger2, 3u); + EXPECT_EQ(trigger3, 1u); // processes the first argument in group3 which includes the entire subcommand, which will + // go until the sub1 command is given +} diff --git a/packages/CLI11/tests/OptionalTest.cpp b/packages/CLI11/tests/OptionalTest.cpp index 5d68c7243..720e73b7a 100644 --- a/packages/CLI11/tests/OptionalTest.cpp +++ b/packages/CLI11/tests/OptionalTest.cpp @@ -62,6 +62,20 @@ TEST_F(TApp, BoostOptionalTest) { EXPECT_EQ(*opt, 3); } +TEST_F(TApp, BoostOptionalVector) { + boost::optional<std::vector<int>> opt; + app.add_option_function<std::vector<int>>("-v,--vec", [&opt](const std::vector<int> &v) { opt = v; }, "some vector") + ->expected(3); + run(); + EXPECT_FALSE(opt); + + args = {"-v", "1", "4", "5"}; + run(); + EXPECT_TRUE(opt); + std::vector<int> expV{1, 4, 5}; + EXPECT_EQ(*opt, expV); +} + #endif #if !CLI11_OPTIONAL diff --git a/packages/CLI11/tests/SetTest.cpp b/packages/CLI11/tests/SetTest.cpp new file mode 100644 index 000000000..30d400b4b --- /dev/null +++ b/packages/CLI11/tests/SetTest.cpp @@ -0,0 +1,703 @@ +#include "app_helper.hpp" +#include <map> + +static_assert(CLI::is_shared_ptr<std::shared_ptr<int>>::value == true, "is_shared_ptr should work on shared pointers"); +static_assert(CLI::is_shared_ptr<int *>::value == false, "is_shared_ptr should work on pointers"); +static_assert(CLI::is_shared_ptr<int>::value == false, "is_shared_ptr should work on non-pointers"); +static_assert(CLI::is_shared_ptr<const std::shared_ptr<int>>::value == true, + "is_shared_ptr should work on const shared pointers"); +static_assert(CLI::is_shared_ptr<const int *>::value == false, "is_shared_ptr should work on const pointers"); +static_assert(CLI::is_shared_ptr<const int &>::value == false, "is_shared_ptr should work on const references"); +static_assert(CLI::is_shared_ptr<int &>::value == false, "is_shared_ptr should work on non-const references"); + +static_assert(CLI::is_copyable_ptr<std::shared_ptr<int>>::value == true, + "is_copyable_ptr should work on shared pointers"); +static_assert(CLI::is_copyable_ptr<int *>::value == true, "is_copyable_ptr should work on pointers"); +static_assert(CLI::is_copyable_ptr<int>::value == false, "is_copyable_ptr should work on non-pointers"); +static_assert(CLI::is_copyable_ptr<const std::shared_ptr<int>>::value == true, + "is_copyable_ptr should work on const shared pointers"); +static_assert(CLI::is_copyable_ptr<const int *>::value == true, "is_copyable_ptr should work on const pointers"); +static_assert(CLI::is_copyable_ptr<const int &>::value == false, "is_copyable_ptr should work on const references"); +static_assert(CLI::is_copyable_ptr<int &>::value == false, "is_copyable_ptr should work on non-const references"); + +static_assert(CLI::detail::pair_adaptor<std::set<int>>::value == false, "Should not have pairs"); +static_assert(CLI::detail::pair_adaptor<std::vector<std::string>>::value == false, "Should not have pairs"); +static_assert(CLI::detail::pair_adaptor<std::map<int, int>>::value == true, "Should have pairs"); +static_assert(CLI::detail::pair_adaptor<std::vector<std::pair<int, int>>>::value == true, "Should have pairs"); + +TEST_F(TApp, SimpleMaps) { + int value; + std::map<std::string, int> map = {{"one", 1}, {"two", 2}}; + auto opt = app.add_option("-s,--set", value)->transform(CLI::Transformer(map)); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, StringStringMap) { + std::string value; + std::map<std::string, std::string> map = {{"a", "b"}, {"b", "c"}}; + app.add_option("-s,--set", value)->transform(CLI::CheckedTransformer(map)); + args = {"-s", "a"}; + run(); + EXPECT_EQ(value, "b"); + + args = {"-s", "b"}; + run(); + EXPECT_EQ(value, "c"); + + args = {"-s", "c"}; + EXPECT_EQ(value, "c"); +} + +TEST_F(TApp, StringStringMapNoModify) { + std::string value; + std::map<std::string, std::string> map = {{"a", "b"}, {"b", "c"}}; + app.add_option("-s,--set", value)->check(CLI::IsMember(map)); + args = {"-s", "a"}; + run(); + EXPECT_EQ(value, "a"); + + args = {"-s", "b"}; + run(); + EXPECT_EQ(value, "b"); + + args = {"-s", "c"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +enum SimpleEnum { SE_one = 1, SE_two = 2 }; + +TEST_F(TApp, EnumMap) { + SimpleEnum value; + std::map<std::string, SimpleEnum> map = {{"one", SE_one}, {"two", SE_two}}; + auto opt = app.add_option("-s,--set", value)->transform(CLI::Transformer(map)); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, SE_one); +} + +enum class SimpleEnumC { one = 1, two = 2 }; + +TEST_F(TApp, EnumCMap) { + SimpleEnumC value; + std::map<std::string, SimpleEnumC> map = {{"one", SimpleEnumC::one}, {"two", SimpleEnumC::two}}; + auto opt = app.add_option("-s,--set", value)->transform(CLI::Transformer(map)); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, SimpleEnumC::one); +} + +TEST_F(TApp, structMap) { + struct tstruct { + int val2; + double val3; + std::string v4; + }; + std::string struct_name; + std::map<std::string, struct tstruct> map = {{"sone", {4, 32.4, "foo"}}, {"stwo", {5, 99.7, "bar"}}}; + auto opt = app.add_option("-s,--set", struct_name)->check(CLI::IsMember(map)); + args = {"-s", "sone"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(struct_name, "sone"); + + args = {"-s", "sthree"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, structMapChange) { + struct tstruct { + int val2; + double val3; + std::string v4; + }; + std::string struct_name; + std::map<std::string, struct tstruct> map = {{"sone", {4, 32.4, "foo"}}, {"stwo", {5, 99.7, "bar"}}}; + auto opt = app.add_option("-s,--set", struct_name) + ->transform(CLI::IsMember(map, CLI::ignore_case, CLI::ignore_underscore, CLI::ignore_space)); + args = {"-s", "s one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(struct_name, "sone"); + + args = {"-s", "sthree"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-s", "S_t_w_o"}; + run(); + EXPECT_EQ(struct_name, "stwo"); + args = {"-s", "S two"}; + run(); + EXPECT_EQ(struct_name, "stwo"); +} + +TEST_F(TApp, structMapNoChange) { + struct tstruct { + int val2; + double val3; + std::string v4; + }; + std::string struct_name; + std::map<std::string, struct tstruct> map = {{"sone", {4, 32.4, "foo"}}, {"stwo", {5, 99.7, "bar"}}}; + auto opt = app.add_option("-s,--set", struct_name) + ->check(CLI::IsMember(map, CLI::ignore_case, CLI::ignore_underscore, CLI::ignore_space)); + args = {"-s", "SONE"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(struct_name, "SONE"); + + args = {"-s", "sthree"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-s", "S_t_w_o"}; + run(); + EXPECT_EQ(struct_name, "S_t_w_o"); + + args = {"-s", "S two"}; + run(); + EXPECT_EQ(struct_name, "S two"); +} + +TEST_F(TApp, NonCopyableMap) { + + std::string map_name; + std::map<std::string, std::unique_ptr<double>> map; + map["e1"] = std::unique_ptr<double>(new double(5.7)); + map["e3"] = std::unique_ptr<double>(new double(23.8)); + auto opt = app.add_option("-s,--set", map_name)->check(CLI::IsMember(&map)); + args = {"-s", "e1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(map_name, "e1"); + + args = {"-s", "e45"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NonCopyableMapWithFunction) { + + std::string map_name; + std::map<std::string, std::unique_ptr<double>> map; + map["e1"] = std::unique_ptr<double>(new double(5.7)); + map["e3"] = std::unique_ptr<double>(new double(23.8)); + auto opt = app.add_option("-s,--set", map_name)->transform(CLI::IsMember(&map, CLI::ignore_underscore)); + args = {"-s", "e_1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(map_name, "e1"); + + args = {"-s", "e45"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, NonCopyableMapNonStringMap) { + + std::string map_name; + std::map<int, std::unique_ptr<double>> map; + map[4] = std::unique_ptr<double>(new double(5.7)); + map[17] = std::unique_ptr<double>(new double(23.8)); + auto opt = app.add_option("-s,--set", map_name)->check(CLI::IsMember(&map)); + args = {"-s", "4"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(map_name, "4"); + + args = {"-s", "e45"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, CopyableMapMove) { + + std::string map_name; + std::map<int, double> map; + map[4] = 5.7; + map[17] = 23.8; + auto opt = app.add_option("-s,--set", map_name)->check(CLI::IsMember(std::move(map))); + args = {"-s", "4"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(map_name, "4"); + + args = {"-s", "e45"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, SimpleSets) { + std::string value; + auto opt = app.add_option("-s,--set", value)->check(CLI::IsMember{std::set<std::string>({"one", "two", "three"})}); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, "one"); +} + +TEST_F(TApp, SimpleSetsPtrs) { + auto set = std::shared_ptr<std::set<std::string>>(new std::set<std::string>{"one", "two", "three"}); + std::string value; + auto opt = app.add_option("-s,--set", value)->check(CLI::IsMember{set}); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, "one"); + + set->insert("four"); + + args = {"-s", "four"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, "four"); +} + +TEST_F(TApp, SimiShortcutSets) { + std::string value; + auto opt = app.add_option("--set", value)->check(CLI::IsMember({"one", "two", "three"})); + args = {"--set", "one"}; + run(); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, "one"); + + std::string value2; + auto opt2 = app.add_option("--set2", value2)->transform(CLI::IsMember({"One", "two", "three"}, CLI::ignore_case)); + args = {"--set2", "onE"}; + run(); + EXPECT_EQ(1u, app.count("--set2")); + EXPECT_EQ(1u, opt2->count()); + EXPECT_EQ(value2, "One"); + + std::string value3; + auto opt3 = app.add_option("--set3", value3) + ->transform(CLI::IsMember({"O_ne", "two", "three"}, CLI::ignore_case, CLI::ignore_underscore)); + args = {"--set3", "onE"}; + run(); + EXPECT_EQ(1u, app.count("--set3")); + EXPECT_EQ(1u, opt3->count()); + EXPECT_EQ(value3, "O_ne"); +} + +TEST_F(TApp, SetFromCharStarArrayVector) { + constexpr const char *names[] = {"one", "two", "three"}; + std::string value; + auto opt = app.add_option("-s,--set", value) + ->check(CLI::IsMember{std::vector<std::string>(std::begin(names), std::end(names))}); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, "one"); +} + +TEST_F(TApp, OtherTypeSets) { + int value; + std::vector<int> set = {2, 3, 4}; + auto opt = app.add_option("--set", value)->check(CLI::IsMember(set)); + args = {"--set", "3"}; + run(); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 3); + + args = {"--set", "5"}; + EXPECT_THROW(run(), CLI::ValidationError); + + std::vector<int> set2 = {-2, 3, 4}; + auto opt2 = app.add_option("--set2", value)->transform(CLI::IsMember(set2, [](int x) { return std::abs(x); })); + args = {"--set2", "-3"}; + run(); + EXPECT_EQ(1u, app.count("--set2")); + EXPECT_EQ(1u, opt2->count()); + EXPECT_EQ(value, 3); + + args = {"--set2", "-3"}; + run(); + EXPECT_EQ(1u, app.count("--set2")); + EXPECT_EQ(1u, opt2->count()); + EXPECT_EQ(value, 3); + + args = {"--set2", "2"}; + run(); + EXPECT_EQ(1u, app.count("--set2")); + EXPECT_EQ(1u, opt2->count()); + EXPECT_EQ(value, -2); +} + +TEST_F(TApp, NumericalSets) { + int value; + auto opt = app.add_option("-s,--set", value)->check(CLI::IsMember{std::set<int>({1, 2, 3})}); + args = {"-s", "1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, app.count("--set")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +// Converted original set tests + +TEST_F(TApp, SetWithDefaults) { + int someint = 2; + app.add_option("-a", someint, "", true)->check(CLI::IsMember({1, 2, 3, 4})); + + args = {"-a1", "-a2"}; + + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, SetWithDefaultsConversion) { + int someint = 2; + app.add_option("-a", someint, "", true)->check(CLI::IsMember({1, 2, 3, 4})); + + args = {"-a", "hi"}; + + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, SetWithDefaultsIC) { + std::string someint = "ho"; + app.add_option("-a", someint, "", true)->check(CLI::IsMember({"Hi", "Ho"})); + + args = {"-aHi", "-aHo"}; + + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSet) { + + std::string choice; + app.add_option("-q,--quick", choice)->check(CLI::IsMember({"one", "two", "three"})); + + args = {"--quick", "two"}; + + run(); + EXPECT_EQ("two", choice); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InSetWithDefault) { + + std::string choice = "one"; + app.add_option("-q,--quick", choice, "", true)->check(CLI::IsMember({"one", "two", "three"})); + + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + + run(); + EXPECT_EQ("two", choice); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InCaselessSetWithDefault) { + + std::string choice = "one"; + app.add_option("-q,--quick", choice, "", true)->transform(CLI::IsMember({"one", "two", "three"}, CLI::ignore_case)); + + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "tWo"}; + + run(); + EXPECT_EQ("two", choice); + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InIntSet) { + + int choice; + app.add_option("-q,--quick", choice)->check(CLI::IsMember({1, 2, 3})); + + args = {"--quick", "2"}; + + run(); + EXPECT_EQ(2, choice); + + args = {"--quick", "4"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InIntSetWindows) { + + int choice; + app.add_option("-q,--quick", choice)->check(CLI::IsMember({1, 2, 3})); + app.allow_windows_style_options(); + args = {"/q", "2"}; + + run(); + EXPECT_EQ(2, choice); + + args = {"/q", "4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"/q4"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(TApp, FailSet) { + + int choice; + app.add_option("-q,--quick", choice)->check(CLI::IsMember({1, 2, 3})); + + args = {"--quick", "3", "--quick=2"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); + + args = {"--quick=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, FailMutableSet) { + + int choice; + auto vals = std::shared_ptr<std::set<int>>(new std::set<int>({1, 2, 3})); + app.add_option("-q,--quick", choice)->check(CLI::IsMember(vals)); + app.add_option("-s,--slow", choice, "", true)->check(CLI::IsMember(vals)); + + args = {"--quick=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--slow=hello"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InSetIgnoreCase) { + + std::string choice; + app.add_option("-q,--quick", choice)->transform(CLI::IsMember({"one", "Two", "THREE"}, CLI::ignore_case)); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=one", "--quick=two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSetIgnoreCaseMutableValue) { + + std::set<std::string> options{"one", "Two", "THREE"}; + std::string choice; + app.add_option("-q,--quick", choice)->transform(CLI::IsMember(&options, CLI::ignore_case)); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + options.clear(); + args = {"--quick", "ThrEE"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, InSetIgnoreCasePointer) { + + std::set<std::string> *options = new std::set<std::string>{"one", "Two", "THREE"}; + std::string choice; + app.add_option("-q,--quick", choice)->transform(CLI::IsMember(*options, CLI::ignore_case)); + + args = {"--quick", "One"}; + run(); + EXPECT_EQ("one", choice); + + args = {"--quick", "two"}; + run(); + EXPECT_EQ("Two", choice); // Keeps caps from set + + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // Keeps caps from set + + delete options; + args = {"--quick", "ThrEE"}; + run(); + EXPECT_EQ("THREE", choice); // this does not throw a segfault + + args = {"--quick", "four"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=one", "--quick=two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, NotInSetIgnoreCasePointer) { + + std::set<std::string> *options = new std::set<std::string>{"one", "Two", "THREE"}; + std::string choice; + app.add_option("-q,--quick", choice)->check(!CLI::IsMember(*options, CLI::ignore_case)); + + args = {"--quick", "One"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick", "four"}; + run(); + EXPECT_EQ(choice, "four"); +} + +TEST_F(TApp, InSetIgnoreUnderscore) { + + std::string choice; + app.add_option("-q,--quick", choice) + ->transform(CLI::IsMember({"option_one", "option_two", "optionthree"}, CLI::ignore_underscore)); + + args = {"--quick", "option_one"}; + run(); + EXPECT_EQ("option_one", choice); + + args = {"--quick", "optiontwo"}; + run(); + EXPECT_EQ("option_two", choice); // Keeps underscore from set + + args = {"--quick", "_option_thr_ee"}; + run(); + EXPECT_EQ("optionthree", choice); // no underscore + + args = {"--quick", "Option4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=option_one", "--quick=option_two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +TEST_F(TApp, InSetIgnoreCaseUnderscore) { + + std::string choice; + app.add_option("-q,--quick", choice) + ->transform( + CLI::IsMember({"Option_One", "option_two", "OptionThree"}, CLI::ignore_case, CLI::ignore_underscore)); + + args = {"--quick", "option_one"}; + run(); + EXPECT_EQ("Option_One", choice); + + args = {"--quick", "OptionTwo"}; + run(); + EXPECT_EQ("option_two", choice); // Keeps underscore and case from set + + args = {"--quick", "_OPTION_thr_ee"}; + run(); + EXPECT_EQ("OptionThree", choice); // no underscore + + args = {"--quick", "Option4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--quick=option_one", "--quick=option_two"}; + EXPECT_THROW(run(), CLI::ArgumentMismatch); +} + +// #113 +TEST_F(TApp, AddRemoveSetItems) { + std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_option("--type1", type1)->check(CLI::IsMember(&items)); + app.add_option("--type2", type2, "", true)->check(CLI::IsMember(&items)); + + args = {"--type1", "TYPE1", "--type2", "TYPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + args = {"--type1", "TYPE6", "--type2", "TYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); + + args = {"--type1", "TYPE1"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--type2", "TYPE2"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, AddRemoveSetItemsNoCase) { + std::set<std::string> items{"TYPE1", "TYPE2", "TYPE3", "TYPE4", "TYPE5"}; + + std::string type1, type2; + app.add_option("--type1", type1)->transform(CLI::IsMember(&items, CLI::ignore_case)); + app.add_option("--type2", type2, "", true)->transform(CLI::IsMember(&items, CLI::ignore_case)); + + args = {"--type1", "TYPe1", "--type2", "TyPE2"}; + + run(); + EXPECT_EQ(type1, "TYPE1"); + EXPECT_EQ(type2, "TYPE2"); + + items.insert("TYPE6"); + items.insert("TYPE7"); + + items.erase("TYPE1"); + items.erase("TYPE2"); + + args = {"--type1", "TyPE6", "--type2", "tYPE7"}; + run(); + EXPECT_EQ(type1, "TYPE6"); + EXPECT_EQ(type2, "TYPE7"); + + args = {"--type1", "TYPe1"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--type2", "TYpE2"}; + EXPECT_THROW(run(), CLI::ValidationError); +} diff --git a/packages/CLI11/tests/StringParseTest.cpp b/packages/CLI11/tests/StringParseTest.cpp new file mode 100644 index 000000000..a457403c1 --- /dev/null +++ b/packages/CLI11/tests/StringParseTest.cpp @@ -0,0 +1,75 @@ +#include "app_helper.hpp" + +#include "gmock/gmock.h" +#include <cstdio> +#include <sstream> + +TEST_F(TApp, ExistingExeCheck) { + + TempFile tmpexe{"existingExe.out"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, ExistingExeCheckWithSpace) { + + TempFile tmpexe{"Space File.out"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe)); +} + +TEST_F(TApp, ExistingExeCheckWithLotsOfSpace) { + + TempFile tmpexe{"this is a weird file.exe"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe)); +} diff --git a/packages/CLI11/tests/SubcommandTest.cpp b/packages/CLI11/tests/SubcommandTest.cpp index e752f4f2c..4db7f2cf0 100644 --- a/packages/CLI11/tests/SubcommandTest.cpp +++ b/packages/CLI11/tests/SubcommandTest.cpp @@ -19,19 +19,19 @@ TEST_F(TApp, BasicSubcommands) { EXPECT_THROW(app.get_subcommand("sub3"), CLI::OptionNotFound); run(); - EXPECT_EQ((size_t)0, app.get_subcommands().size()); + EXPECT_EQ(0u, app.get_subcommands().size()); args = {"sub1"}; run(); EXPECT_EQ(sub1, app.get_subcommands().at(0)); - EXPECT_EQ((size_t)1, app.get_subcommands().size()); + EXPECT_EQ(1u, app.get_subcommands().size()); app.clear(); - EXPECT_EQ((size_t)0, app.get_subcommands().size()); + EXPECT_EQ(0u, app.get_subcommands().size()); args = {"sub2"}; run(); - EXPECT_EQ((size_t)1, app.get_subcommands().size()); + EXPECT_EQ(1u, app.get_subcommands().size()); EXPECT_EQ(sub2, app.get_subcommands().at(0)); args = {"SUb2"}; @@ -64,21 +64,19 @@ TEST_F(TApp, MultiSubFallthrough) { EXPECT_TRUE(app.got_subcommand(sub1)); EXPECT_TRUE(*sub1); EXPECT_TRUE(sub1->parsed()); + EXPECT_EQ(sub1->count(), 1u); EXPECT_TRUE(app.got_subcommand("sub2")); EXPECT_TRUE(app.got_subcommand(sub2)); EXPECT_TRUE(*sub2); app.require_subcommand(); - run(); app.require_subcommand(2); - run(); app.require_subcommand(1); - EXPECT_THROW(run(), CLI::ExtrasError); args = {"sub1"}; @@ -90,6 +88,7 @@ TEST_F(TApp, MultiSubFallthrough) { EXPECT_TRUE(*sub1); EXPECT_FALSE(*sub2); EXPECT_FALSE(sub2->parsed()); + EXPECT_EQ(sub2->count(), 0u); EXPECT_THROW(app.got_subcommand("sub3"), CLI::OptionNotFound); } @@ -164,6 +163,59 @@ TEST_F(TApp, FooFooProblem) { EXPECT_EQ(other_str, ""); } +TEST_F(TApp, DuplicateSubcommands) { + + auto foo = app.add_subcommand("foo"); + + args = {"foo", "foo"}; + run(); + EXPECT_TRUE(*foo); + EXPECT_EQ(foo->count(), 2u); + + args = {"foo", "foo", "foo"}; + run(); + EXPECT_TRUE(*foo); + EXPECT_EQ(foo->count(), 3u); +} + +TEST_F(TApp, DuplicateSubcommandCallbacks) { + + auto foo = app.add_subcommand("foo"); + int count = 0; + foo->callback([&count]() { ++count; }); + foo->immediate_callback(); + EXPECT_TRUE(foo->get_immediate_callback()); + args = {"foo", "foo"}; + run(); + EXPECT_EQ(count, 2); + count = 0; + args = {"foo", "foo", "foo"}; + run(); + EXPECT_EQ(count, 3); +} + +TEST_F(TApp, DuplicateSubcommandCallbacksValues) { + + auto foo = app.add_subcommand("foo"); + int val; + foo->add_option("--val", val); + std::vector<int> vals; + foo->callback([&vals, &val]() { vals.push_back(val); }); + foo->immediate_callback(); + args = {"foo", "--val=45", "foo", "--val=27"}; + run(); + EXPECT_EQ(vals.size(), 2u); + EXPECT_EQ(vals[0], 45); + EXPECT_EQ(vals[1], 27); + vals.clear(); + args = {"foo", "--val=45", "foo", "--val=27", "foo", "--val=36"}; + run(); + EXPECT_EQ(vals.size(), 3u); + EXPECT_EQ(vals[0], 45); + EXPECT_EQ(vals[1], 27); + EXPECT_EQ(vals[2], 36); +} + TEST_F(TApp, Callbacks) { auto sub1 = app.add_subcommand("sub1"); sub1->callback([]() { throw CLI::Success(); }); @@ -224,6 +276,147 @@ TEST_F(TApp, NoFallThroughPositionals) { EXPECT_THROW(run(), CLI::ExtrasError); } +TEST_F(TApp, NoFallThroughOptsWithTerminator) { + int val = 1; + app.add_option("--val", val); + + app.add_subcommand("sub"); + + args = {"sub", "++", "--val", "2"}; + run(); + EXPECT_EQ(val, 2); +} + +TEST_F(TApp, NoFallThroughPositionalsWithTerminator) { + int val = 1; + app.add_option("val", val); + + app.add_subcommand("sub"); + + args = {"sub", "++", "2"}; + run(); + EXPECT_EQ(val, 2); + + // try with positional only mark + args = {"sub", "--", "3"}; + run(); + EXPECT_EQ(val, 3); +} + +TEST_F(TApp, NamelessSubComPositionals) { + + auto sub = app.add_subcommand(); + int val = 1; + sub->add_option("val", val); + + args = {"2"}; + run(); + EXPECT_EQ(val, 2); +} + +TEST_F(TApp, NamelessSubWithSub) { + + auto sub = app.add_subcommand(); + auto subsub = sub->add_subcommand("val"); + + args = {"val"}; + run(); + EXPECT_TRUE(subsub->parsed()); + EXPECT_TRUE(app.got_subcommand("val")); +} + +TEST_F(TApp, NamelessSubWithMultipleSub) { + + auto sub1 = app.add_subcommand(); + auto sub2 = app.add_subcommand(); + auto sub1sub1 = sub1->add_subcommand("val1"); + auto sub1sub2 = sub1->add_subcommand("val2"); + auto sub2sub1 = sub2->add_subcommand("val3"); + auto sub2sub2 = sub2->add_subcommand("val4"); + args = {"val1"}; + run(); + EXPECT_TRUE(sub1sub1->parsed()); + EXPECT_TRUE(app.got_subcommand("val1")); + + args = {"val2"}; + run(); + EXPECT_TRUE(sub1sub2->parsed()); + EXPECT_TRUE(app.got_subcommand("val2")); + + args = {"val3"}; + run(); + EXPECT_TRUE(sub2sub1->parsed()); + EXPECT_TRUE(app.got_subcommand("val3")); + + args = {"val4"}; + run(); + EXPECT_TRUE(sub2sub2->parsed()); + EXPECT_TRUE(app.got_subcommand("val4")); + + args = {"val4", "val1"}; + run(); + EXPECT_TRUE(sub2sub2->parsed()); + EXPECT_TRUE(app.got_subcommand("val4")); + EXPECT_TRUE(sub1sub1->parsed()); + EXPECT_TRUE(app.got_subcommand("val1")); +} + +TEST_F(TApp, Nameless4LayerDeep) { + + auto sub = app.add_subcommand(); + auto ssub = sub->add_subcommand(); + auto sssub = ssub->add_subcommand(); + + auto ssssub = sssub->add_subcommand(); + auto sssssub = ssssub->add_subcommand("val"); + + args = {"val"}; + run(); + EXPECT_TRUE(sssssub->parsed()); + EXPECT_TRUE(app.got_subcommand("val")); +} + +/// Put subcommands in some crazy pattern and make everything still works +TEST_F(TApp, Nameless4LayerDeepMulit) { + + auto sub1 = app.add_subcommand(); + auto sub2 = app.add_subcommand(); + auto ssub1 = sub1->add_subcommand(); + auto ssub2 = sub2->add_subcommand(); + + auto sssub1 = ssub1->add_subcommand(); + auto sssub2 = ssub2->add_subcommand(); + sssub1->add_subcommand("val1"); + ssub2->add_subcommand("val2"); + sub2->add_subcommand("val3"); + ssub1->add_subcommand("val4"); + sssub2->add_subcommand("val5"); + args = {"val1"}; + run(); + EXPECT_TRUE(app.got_subcommand("val1")); + + args = {"val2"}; + run(); + EXPECT_TRUE(app.got_subcommand("val2")); + + args = {"val3"}; + run(); + EXPECT_TRUE(app.got_subcommand("val3")); + + args = {"val4"}; + run(); + EXPECT_TRUE(app.got_subcommand("val4")); + args = {"val5"}; + run(); + EXPECT_TRUE(app.got_subcommand("val5")); + + args = {"val4", "val1", "val5"}; + run(); + EXPECT_TRUE(app.got_subcommand("val4")); + EXPECT_TRUE(app.got_subcommand("val1")); + EXPECT_TRUE(app.got_subcommand("val5")); +} + TEST_F(TApp, FallThroughRegular) { app.fallthrough(); int val = 1; @@ -307,6 +500,25 @@ TEST_F(TApp, CallbackOrdering) { EXPECT_EQ(2, sub_val); } +TEST_F(TApp, CallbackOrderingImmediate) { + app.fallthrough(); + int val = 1, sub_val = 0; + app.add_option("--val", val); + + auto sub = app.add_subcommand("sub")->immediate_callback(); + sub->callback([&val, &sub_val]() { sub_val = val; }); + + args = {"sub", "--val=2"}; + run(); + EXPECT_EQ(2, val); + EXPECT_EQ(1, sub_val); + + args = {"--val=2", "sub"}; + run(); + EXPECT_EQ(2, val); + EXPECT_EQ(2, sub_val); +} + TEST_F(TApp, RequiredSubCom) { app.add_subcommand("sub1"); app.add_subcommand("sub2"); @@ -366,6 +578,7 @@ TEST_F(TApp, BadSubcomSearch) { auto two = one->add_subcommand("two"); EXPECT_THROW(app.get_subcommand(two), CLI::OptionNotFound); + EXPECT_THROW(app.get_subcommand_ptr(two), CLI::OptionNotFound); } TEST_F(TApp, PrefixProgram) { @@ -416,8 +629,8 @@ TEST_F(TApp, PrefixSubcom) { args = {"--simple", "subc", "other", "--simple", "--mine"}; run(); - EXPECT_EQ(app.remaining_size(), (size_t)0); - EXPECT_EQ(app.remaining_size(true), (size_t)3); + EXPECT_EQ(app.remaining_size(), 0u); + EXPECT_EQ(app.remaining_size(true), 3u); EXPECT_EQ(subc->remaining(), std::vector<std::string>({"other", "--simple", "--mine"})); } @@ -425,7 +638,28 @@ TEST_F(TApp, InheritHelpAllFlag) { app.set_help_all_flag("--help-all"); auto subc = app.add_subcommand("subc"); auto help_opt_list = subc->get_options([](const CLI::Option *opt) { return opt->get_name() == "--help-all"; }); - EXPECT_EQ(help_opt_list.size(), (size_t)1); + EXPECT_EQ(help_opt_list.size(), 1u); +} + +TEST_F(TApp, RequiredPosInSubcommand) { + app.require_subcommand(); + std::string bar; + + CLI::App *fooApp = app.add_subcommand("foo", "Foo a bar"); + fooApp->add_option("bar", bar, "A bar to foo")->required(); + + CLI::App *bazApp = app.add_subcommand("baz", "Baz a bar"); + bazApp->add_option("bar", bar, "A bar a baz")->required(); + + args = {"foo", "abc"}; + run(); + EXPECT_EQ(bar, "abc"); + args = {"baz", "cba"}; + run(); + EXPECT_EQ(bar, "cba"); + + args = {}; + EXPECT_THROW(run(), CLI::RequiredError); } struct SubcommandProgram : public TApp { @@ -475,7 +709,7 @@ TEST_F(SubcommandProgram, Multiple) { args = {"-d", "start", "-ffilename", "stop"}; run(); - EXPECT_EQ((size_t)2, app.get_subcommands().size()); + EXPECT_EQ(2u, app.get_subcommands().size()); EXPECT_EQ(1, dummy); EXPECT_EQ("filename", file); } @@ -491,7 +725,7 @@ TEST_F(SubcommandProgram, MultipleArgs) { run(); - EXPECT_EQ((size_t)2, app.get_subcommands().size()); + EXPECT_EQ(2u, app.get_subcommands().size()); } TEST_F(SubcommandProgram, CaseCheck) { @@ -514,23 +748,60 @@ TEST_F(TApp, SubcomInheritCaseCheck) { auto sub2 = app.add_subcommand("sub2"); run(); - EXPECT_EQ((size_t)0, app.get_subcommands().size()); - EXPECT_EQ((size_t)2, app.get_subcommands({}).size()); - EXPECT_EQ((size_t)1, app.get_subcommands([](const CLI::App *s) { return s->get_name() == "sub1"; }).size()); + EXPECT_EQ(0u, app.get_subcommands().size()); + EXPECT_EQ(2u, app.get_subcommands({}).size()); + EXPECT_EQ(1u, app.get_subcommands([](const CLI::App *s) { return s->get_name() == "sub1"; }).size()); args = {"SuB1"}; run(); EXPECT_EQ(sub1, app.get_subcommands().at(0)); - EXPECT_EQ((size_t)1, app.get_subcommands().size()); + EXPECT_EQ(1u, app.get_subcommands().size()); app.clear(); - EXPECT_EQ((size_t)0, app.get_subcommands().size()); + EXPECT_EQ(0u, app.get_subcommands().size()); args = {"sUb2"}; run(); EXPECT_EQ(sub2, app.get_subcommands().at(0)); } +TEST_F(SubcommandProgram, UnderscoreCheck) { + args = {"start_"}; + EXPECT_THROW(run(), CLI::ExtrasError); + + args = {"start"}; + run(); + + start->ignore_underscore(); + run(); + + args = {"_start_"}; + run(); +} + +TEST_F(TApp, SubcomInheritUnderscoreCheck) { + app.ignore_underscore(); + auto sub1 = app.add_subcommand("sub_option1"); + auto sub2 = app.add_subcommand("sub_option2"); + + run(); + EXPECT_EQ(0u, app.get_subcommands().size()); + EXPECT_EQ(2u, app.get_subcommands({}).size()); + EXPECT_EQ(1u, app.get_subcommands([](const CLI::App *s) { return s->get_name() == "sub_option1"; }).size()); + + args = {"suboption1"}; + run(); + EXPECT_EQ(sub1, app.get_subcommands().at(0)); + EXPECT_EQ(1u, app.get_subcommands().size()); + + app.clear(); + EXPECT_EQ(0u, app.get_subcommands().size()); + + args = {"_suboption2"}; + run(); + EXPECT_EQ(sub2, app.get_subcommands().at(0)); +} + TEST_F(SubcommandProgram, HelpOrder) { args = {"-h"}; @@ -614,9 +885,9 @@ TEST_F(SubcommandProgram, OrderedExtras) { run(); - EXPECT_EQ(app.remaining(), std::vector<std::string>({"one", "two"})); - EXPECT_EQ(start->remaining(), std::vector<std::string>({"three", "--", "four"})); - EXPECT_EQ(app.remaining(true), std::vector<std::string>({"one", "two", "three", "--", "four"})); + EXPECT_EQ(app.remaining(), std::vector<std::string>({"one", "two", "four"})); + EXPECT_EQ(start->remaining(), std::vector<std::string>({"three"})); + EXPECT_EQ(app.remaining(true), std::vector<std::string>({"one", "two", "four", "three"})); } TEST_F(SubcommandProgram, MixedOrderExtras) { @@ -658,6 +929,22 @@ TEST_F(SubcommandProgram, CallbackOrder) { EXPECT_EQ(callback_order, std::vector<int>({2, 1})); } +TEST_F(SubcommandProgram, CallbackOrderImmediate) { + std::vector<int> callback_order; + start->callback([&callback_order]() { callback_order.push_back(1); })->immediate_callback(); + stop->callback([&callback_order]() { callback_order.push_back(2); }); + + args = {"start", "stop", "start"}; + run(); + EXPECT_EQ(callback_order, std::vector<int>({1, 1, 2})); + + callback_order.clear(); + + args = {"stop", "start", "stop", "start"}; + run(); + EXPECT_EQ(callback_order, std::vector<int>({1, 1, 2})); +} + struct ManySubcommands : public TApp { CLI::App *sub1; @@ -696,6 +983,48 @@ TEST_F(ManySubcommands, Required4Failure) { EXPECT_THROW(run(), CLI::RequiredError); } +TEST_F(ManySubcommands, RemoveSub) { + run(); + EXPECT_EQ(app.remaining_size(true), 0u); + app.remove_subcommand(sub1); + app.allow_extras(); + run(); + EXPECT_EQ(app.remaining_size(true), 1u); +} + +TEST_F(ManySubcommands, RemoveSubFail) { + auto sub_sub = sub1->add_subcommand("subsub"); + EXPECT_FALSE(app.remove_subcommand(sub_sub)); + EXPECT_TRUE(sub1->remove_subcommand(sub_sub)); + EXPECT_FALSE(app.remove_subcommand(nullptr)); +} + +TEST_F(ManySubcommands, manyIndexQuery) { + auto s1 = app.get_subcommand(0); + auto s2 = app.get_subcommand(1); + auto s3 = app.get_subcommand(2); + auto s4 = app.get_subcommand(3); + EXPECT_EQ(s1, sub1); + EXPECT_EQ(s2, sub2); + EXPECT_EQ(s3, sub3); + EXPECT_EQ(s4, sub4); + EXPECT_THROW(app.get_subcommand(4), CLI::OptionNotFound); + auto s0 = app.get_subcommand(); + EXPECT_EQ(s0, sub1); +} + +TEST_F(ManySubcommands, manyIndexQueryPtr) { + auto s1 = app.get_subcommand_ptr(0); + auto s2 = app.get_subcommand_ptr(1); + auto s3 = app.get_subcommand_ptr(2); + auto s4 = app.get_subcommand_ptr(3); + EXPECT_EQ(s1.get(), sub1); + EXPECT_EQ(s2.get(), sub2); + EXPECT_EQ(s3.get(), sub3); + EXPECT_EQ(s4.get(), sub4); + EXPECT_THROW(app.get_subcommand_ptr(4), CLI::OptionNotFound); +} + TEST_F(ManySubcommands, Required1Fuzzy) { app.require_subcommand(0, 1); @@ -736,3 +1065,356 @@ TEST_F(ManySubcommands, Unlimited) { run(); EXPECT_EQ(app.remaining(true), vs_t()); } + +TEST_F(ManySubcommands, HelpFlags) { + + args = {"-h"}; + + EXPECT_THROW(run(), CLI::CallForHelp); + + args = {"sub2", "-h"}; + + EXPECT_THROW(run(), CLI::CallForHelp); + + args = {"-h", "sub2"}; + + EXPECT_THROW(run(), CLI::CallForHelp); +} + +TEST_F(ManySubcommands, MaxCommands) { + + app.require_subcommand(2); + + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + // The extra subcommand counts as an extra + args = {"sub1", "sub2", "sub3"}; + EXPECT_NO_THROW(run()); + EXPECT_EQ(sub2->remaining().size(), 1u); + EXPECT_EQ(app.count_all(), 2u); + + // Currently, setting sub2 to throw causes an extras error + // In the future, would passing on up to app's extras be better? + + app.allow_extras(false); + sub1->allow_extras(false); + sub2->allow_extras(false); + + args = {"sub1", "sub2"}; + + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub2", "sub3"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(ManySubcommands, SubcommandExclusion) { + + sub1->excludes(sub3); + sub2->excludes(sub3); + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub2", "sub3"}; + EXPECT_THROW(run(), CLI::ExcludesError); + + args = {"sub1", "sub2", "sub4"}; + EXPECT_NO_THROW(run()); + EXPECT_EQ(app.count_all(), 3u); + + args = {"sub3", "sub4"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManySubcommands, SubcommandOptionExclusion) { + + auto excluder_flag = app.add_flag("--exclude"); + sub1->excludes(excluder_flag)->fallthrough(); + sub2->excludes(excluder_flag)->fallthrough(); + sub3->fallthrough(); + sub4->fallthrough(); + args = {"sub3", "sub4", "--exclude"}; + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub3", "--exclude"}; + EXPECT_THROW(run(), CLI::ExcludesError); + EXPECT_TRUE(sub1->remove_excludes(excluder_flag)); + EXPECT_NO_THROW(run()); + EXPECT_FALSE(sub1->remove_excludes(excluder_flag)); + + args = {"--exclude", "sub2", "sub4"}; + EXPECT_THROW(run(), CLI::ExcludesError); + EXPECT_EQ(sub1->excludes(excluder_flag), sub1); + args = {"sub1", "--exclude", "sub2", "sub4"}; + try { + run(); + } catch(const CLI::ExcludesError &ee) { + EXPECT_NE(std::string(ee.what()).find("sub1"), std::string::npos); + } +} + +TEST_F(ManySubcommands, SubcommandRequired) { + + sub1->required(); + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub2", "sub3"}; + EXPECT_NO_THROW(run()); + + args = {"sub3", "sub4"}; + EXPECT_THROW(run(), CLI::RequiredError); +} + +TEST_F(ManySubcommands, SubcommandDisabled) { + + sub3->disabled(); + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub2", "sub3"}; + app.allow_extras(false); + sub2->allow_extras(false); + EXPECT_THROW(run(), CLI::ExtrasError); + args = {"sub3", "sub4"}; + EXPECT_THROW(run(), CLI::ExtrasError); + sub3->disabled(false); + args = {"sub3", "sub4"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManySubcommands, SubcommandTriggeredOff) { + + app.allow_extras(false); + sub1->allow_extras(false); + sub2->allow_extras(false); + CLI::TriggerOff(sub1, sub2); + args = {"sub1", "sub2"}; + EXPECT_THROW(run(), CLI::ExtrasError); + + args = {"sub2", "sub1", "sub3"}; + EXPECT_NO_THROW(run()); + CLI::TriggerOff(sub1, {sub3, sub4}); + EXPECT_THROW(run(), CLI::ExtrasError); + args = {"sub1", "sub2", "sub4"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(ManySubcommands, SubcommandTriggeredOn) { + + app.allow_extras(false); + sub1->allow_extras(false); + sub2->allow_extras(false); + CLI::TriggerOn(sub1, sub2); + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub2", "sub1", "sub4"}; + EXPECT_THROW(run(), CLI::ExtrasError); + CLI::TriggerOn(sub1, {sub3, sub4}); + sub2->disabled_by_default(false); + sub2->disabled(false); + EXPECT_NO_THROW(run()); + args = {"sub3", "sub1", "sub2"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(TApp, UnnamedSub) { + double val; + auto sub = app.add_subcommand("", "empty name"); + auto opt = sub->add_option("-v,--value", val); + args = {"-v", "4.56"}; + + run(); + EXPECT_EQ(val, 4.56); + // make sure unnamed sub options can be found from the main app + auto opt2 = app.get_option("-v"); + EXPECT_EQ(opt, opt2); + + EXPECT_THROW(app.get_option("--vvvv"), CLI::OptionNotFound); + // now test in the constant context + const auto &appC = app; + auto opt3 = appC.get_option("-v"); + EXPECT_EQ(opt3->get_name(), "--value"); + EXPECT_THROW(appC.get_option("--vvvv"), CLI::OptionNotFound); +} + +TEST_F(TApp, UnnamedSubMix) { + double val, val2, val3; + app.add_option("-t", val2); + auto sub1 = app.add_subcommand("", "empty name"); + sub1->add_option("-v,--value", val); + auto sub2 = app.add_subcommand("", "empty name2"); + sub2->add_option("-m,--mix", val3); + args = {"-m", "4.56", "-t", "5.93", "-v", "-3"}; + + run(); + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(val3, 4.56); + EXPECT_EQ(app.count_all(), 3u); +} + +TEST_F(TApp, UnnamedSubMixExtras) { + double val, val2; + app.add_option("-t", val2); + auto sub = app.add_subcommand("", "empty name"); + sub->add_option("-v,--value", val); + args = {"-m", "4.56", "-t", "5.93", "-v", "-3"}; + app.allow_extras(); + run(); + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(app.remaining_size(), 2u); + EXPECT_EQ(sub->remaining_size(), 0u); +} + +TEST_F(TApp, UnnamedSubNoExtras) { + double val, val2; + app.add_option("-t", val2); + auto sub = app.add_subcommand(); + sub->add_option("-v,--value", val); + args = {"-t", "5.93", "-v", "-3"}; + run(); + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(app.remaining_size(), 0u); + EXPECT_EQ(sub->remaining_size(), 0u); +} + +TEST(SharedSubTests, SharedSubcommand) { + double val, val2, val3, val4; + CLI::App app1{"test program1"}; + + app1.add_option("-t", val2); + auto sub = app1.add_subcommand("", "empty name"); + sub->add_option("-v,--value", val); + sub->add_option("-g", val4); + CLI::App app2{"test program2"}; + app2.add_option("-m", val3); + // extract an owning ptr from app1 and add it to app2 + auto subown = app1.get_subcommand_ptr(sub); + // add the extracted subcommand to a different app + app2.add_subcommand(std::move(subown)); + EXPECT_THROW(app2.add_subcommand(CLI::App_p{}), CLI::IncorrectConstruction); + input_t args1 = {"-m", "4.56", "-t", "5.93", "-v", "-3"}; + input_t args2 = {"-m", "4.56", "-g", "8.235"}; + std::reverse(std::begin(args1), std::end(args1)); + std::reverse(std::begin(args2), std::end(args2)); + app1.allow_extras(); + app1.parse(args1); + + app2.parse(args2); + + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(val3, 4.56); + EXPECT_EQ(val4, 8.235); +} + +TEST(SharedSubTests, SharedSubIndependent) { + double val, val2, val4; + CLI::App_p app1 = std::make_shared<CLI::App>("test program1"); + app1->allow_extras(); + app1->add_option("-t", val2); + auto sub = app1->add_subcommand("", "empty name"); + sub->add_option("-v,--value", val); + sub->add_option("-g", val4); + + // extract an owning ptr from app1 and add it to app2 + auto subown = app1->get_subcommand_ptr(sub); + + input_t args1 = {"-m", "4.56", "-t", "5.93", "-v", "-3"}; + input_t args2 = {"-m", "4.56", "-g", "8.235"}; + std::reverse(std::begin(args1), std::end(args1)); + std::reverse(std::begin(args2), std::end(args2)); + + app1->parse(args1); + // destroy the first parser + app1 = nullptr; + // parse with the extracted subcommand + subown->parse(args2); + + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(val4, 8.235); +} + +TEST(SharedSubTests, SharedSubIndependentReuse) { + double val, val2, val4; + CLI::App_p app1 = std::make_shared<CLI::App>("test program1"); + app1->allow_extras(); + app1->add_option("-t", val2); + auto sub = app1->add_subcommand("", "empty name"); + sub->add_option("-v,--value", val); + sub->add_option("-g", val4); + + // extract an owning ptr from app1 and add it to app2 + auto subown = app1->get_subcommand_ptr(sub); + + input_t args1 = {"-m", "4.56", "-t", "5.93", "-v", "-3"}; + std::reverse(std::begin(args1), std::end(args1)); + auto args2 = args1; + app1->parse(args1); + + // parse with the extracted subcommand + subown->parse("program1 -m 4.56 -g 8.235", true); + + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); + EXPECT_EQ(val4, 8.235); + val = 0.0; + val2 = 0.0; + EXPECT_EQ(subown->get_name(), "program1"); + // this tests the name reset in subcommand since it was automatic + app1->parse(args2); + EXPECT_EQ(val, -3.0); + EXPECT_EQ(val2, 5.93); +} + +TEST_F(ManySubcommands, getSubtests) { + CLI::App_p sub2p = app.get_subcommand_ptr(sub2); + EXPECT_EQ(sub2p.get(), sub2); + EXPECT_THROW(app.get_subcommand_ptr(nullptr), CLI::OptionNotFound); + EXPECT_THROW(app.get_subcommand(nullptr), CLI::OptionNotFound); + CLI::App_p sub3p = app.get_subcommand_ptr(2); + EXPECT_EQ(sub3p.get(), sub3); +} + +TEST_F(ManySubcommands, defaultDisabledSubcommand) { + + sub1->fallthrough(); + sub2->disabled_by_default(); + run(); + auto rem = app.remaining(); + EXPECT_EQ(rem.size(), 1u); + EXPECT_EQ(rem[0], "sub2"); + EXPECT_TRUE(sub2->get_disabled_by_default()); + sub2->disabled(false); + EXPECT_FALSE(sub2->get_disabled()); + run(); + // this should disable it again even though it was disabled + rem = app.remaining(); + EXPECT_EQ(rem.size(), 1u); + EXPECT_EQ(rem[0], "sub2"); + EXPECT_TRUE(sub2->get_disabled_by_default()); + EXPECT_TRUE(sub2->get_disabled()); +} + +TEST_F(ManySubcommands, defaultEnabledSubcommand) { + + sub2->enabled_by_default(); + run(); + auto rem = app.remaining(); + EXPECT_EQ(rem.size(), 0u); + EXPECT_TRUE(sub2->get_enabled_by_default()); + sub2->disabled(); + EXPECT_TRUE(sub2->get_disabled()); + run(); + // this should disable it again even though it was disabled + rem = app.remaining(); + EXPECT_EQ(rem.size(), 0u); + EXPECT_TRUE(sub2->get_enabled_by_default()); + EXPECT_FALSE(sub2->get_disabled()); +} diff --git a/packages/CLI11/tests/TransformTest.cpp b/packages/CLI11/tests/TransformTest.cpp new file mode 100644 index 000000000..ba0e11a99 --- /dev/null +++ b/packages/CLI11/tests/TransformTest.cpp @@ -0,0 +1,433 @@ +#include "app_helper.hpp" + +#include <unordered_map> + +TEST_F(TApp, SimpleTransform) { + int value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer({{"one", std::string("1")}})); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, SimpleTransformInitList) { + int value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer({{"one", "1"}})); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, SimpleNumericalTransform) { + int value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer(CLI::TransformPairs<int>{{"one", 1}})); + args = {"-s", "one"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, EnumTransform) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17 }; + test value; + auto opt = app.add_option("-s", value) + ->transform(CLI::Transformer( + CLI::TransformPairs<test>{{"val1", test::val1}, {"val2", test::val2}, {"val3", test::val3}})); + args = {"-s", "val1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "val2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "val3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val4"}; + EXPECT_THROW(run(), CLI::ConversionError); + + // transformer doesn't do any checking so this still works + args = {"-s", "5"}; + run(); + EXPECT_EQ(static_cast<int16_t>(value), int16_t(5)); +} + +TEST_F(TApp, EnumCheckedTransform) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17 }; + test value; + auto opt = app.add_option("-s", value) + ->transform(CLI::CheckedTransformer( + CLI::TransformPairs<test>{{"val1", test::val1}, {"val2", test::val2}, {"val3", test::val3}})); + args = {"-s", "val1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "val2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "val3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "17"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val4"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-s", "5"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, SimpleTransformFn) { + int value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer({{"one", "1"}}, CLI::ignore_case)); + args = {"-s", "ONE"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, SimpleNumericalTransformFn) { + int value; + auto opt = + app.add_option("-s", value) + ->transform(CLI::Transformer(std::vector<std::pair<std::string, int>>{{"one", 1}}, CLI::ignore_case)); + args = {"-s", "ONe"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, 1); +} + +TEST_F(TApp, EnumTransformFn) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17 }; + test value; + auto opt = app.add_option("-s", value) + ->transform(CLI::Transformer( + CLI::TransformPairs<test>{{"val1", test::val1}, {"val2", test::val2}, {"val3", test::val3}}, + CLI::ignore_case, + CLI::ignore_underscore)); + args = {"-s", "val_1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "VAL_2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "VAL3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val_4"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, EnumTransformFnMap) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17 }; + std::map<std::string, test> map{{"val1", test::val1}, {"val2", test::val2}, {"val3", test::val3}}; + test value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer(map, CLI::ignore_case, CLI::ignore_underscore)); + args = {"-s", "val_1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "VAL_2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "VAL3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val_4"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + +TEST_F(TApp, EnumTransformFnPtrMap) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17, val4 = 37 }; + std::map<std::string, test> map{{"val1", test::val1}, {"val2", test::val2}, {"val3", test::val3}}; + test value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer(&map, CLI::ignore_case, CLI::ignore_underscore)); + args = {"-s", "val_1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "VAL_2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "VAL3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val_4"}; + EXPECT_THROW(run(), CLI::ConversionError); + + map["val4"] = test::val4; + run(); + EXPECT_EQ(value, test::val4); +} + +TEST_F(TApp, EnumTransformFnSharedPtrMap) { + enum class test : int16_t { val1 = 3, val2 = 4, val3 = 17, val4 = 37 }; + auto map = std::make_shared<std::unordered_map<std::string, test>>(); + auto &mp = *map; + mp["val1"] = test::val1; + mp["val2"] = test::val2; + mp["val3"] = test::val3; + + test value; + auto opt = app.add_option("-s", value)->transform(CLI::Transformer(map, CLI::ignore_case, CLI::ignore_underscore)); + args = {"-s", "val_1"}; + run(); + EXPECT_EQ(1u, app.count("-s")); + EXPECT_EQ(1u, opt->count()); + EXPECT_EQ(value, test::val1); + + args = {"-s", "VAL_2"}; + run(); + EXPECT_EQ(value, test::val2); + + args = {"-s", "VAL3"}; + run(); + EXPECT_EQ(value, test::val3); + + args = {"-s", "val_4"}; + EXPECT_THROW(run(), CLI::ConversionError); + + mp["val4"] = test::val4; + run(); + EXPECT_EQ(value, test::val4); +} + +// Test a cascade of transform functions +TEST_F(TApp, TransformCascade) { + + std::string output; + auto opt = app.add_option("-s", output); + opt->transform(CLI::Transformer({{"abc", "abcd"}, {"bbc", "bbcd"}, {"cbc", "cbcd"}}, CLI::ignore_case)); + opt->transform( + CLI::Transformer({{"ab", "abc"}, {"bc", "bbc"}, {"cb", "cbc"}}, CLI::ignore_case, CLI::ignore_underscore)); + opt->transform(CLI::Transformer({{"a", "ab"}, {"b", "bb"}, {"c", "cb"}}, CLI::ignore_case)); + opt->check(CLI::IsMember({"abcd", "bbcd", "cbcd"})); + args = {"-s", "abcd"}; + run(); + EXPECT_EQ(output, "abcd"); + + args = {"-s", "Bbc"}; + run(); + EXPECT_EQ(output, "bbcd"); + + args = {"-s", "C_B"}; + run(); + EXPECT_EQ(output, "cbcd"); + + args = {"-s", "A"}; + run(); + EXPECT_EQ(output, "abcd"); +} + +// Test a cascade of transform functions +TEST_F(TApp, TransformCascadeDeactivate) { + + std::string output; + auto opt = app.add_option("-s", output); + opt->transform( + CLI::Transformer({{"abc", "abcd"}, {"bbc", "bbcd"}, {"cbc", "cbcd"}}, CLI::ignore_case).name("tform1")); + opt->transform( + CLI::Transformer({{"ab", "abc"}, {"bc", "bbc"}, {"cb", "cbc"}}, CLI::ignore_case, CLI::ignore_underscore) + .name("tform2") + .active(false)); + opt->transform(CLI::Transformer({{"a", "ab"}, {"b", "bb"}, {"c", "cb"}}, CLI::ignore_case).name("tform3")); + opt->check(CLI::IsMember({"abcd", "bbcd", "cbcd"}).name("check")); + args = {"-s", "abcd"}; + run(); + EXPECT_EQ(output, "abcd"); + + args = {"-s", "Bbc"}; + run(); + EXPECT_EQ(output, "bbcd"); + + args = {"-s", "C_B"}; + EXPECT_THROW(run(), CLI::ValidationError); + + auto validator = opt->get_validator("tform2"); + EXPECT_FALSE(validator->get_active()); + EXPECT_EQ(validator->get_name(), "tform2"); + validator->active(); + EXPECT_TRUE(validator->get_active()); + args = {"-s", "C_B"}; + run(); + EXPECT_EQ(output, "cbcd"); + + opt->get_validator("check")->active(false); + args = {"-s", "gsdgsgs"}; + run(); + EXPECT_EQ(output, "gsdgsgs"); + + EXPECT_THROW(opt->get_validator("sdfsdf"), CLI::OptionNotFound); +} + +TEST_F(TApp, IntTransformFn) { + std::string value; + app.add_option("-s", value) + ->transform( + CLI::CheckedTransformer(std::map<int, int>{{15, 5}, {18, 6}, {21, 7}}, [](int in) { return in - 10; })); + args = {"-s", "25"}; + run(); + EXPECT_EQ(value, "5"); + + args = {"-s", "6"}; + run(); + EXPECT_EQ(value, "6"); + + args = {"-s", "45"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-s", "val_4"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + +TEST_F(TApp, IntTransformNonConvertible) { + std::string value; + app.add_option("-s", value)->transform(CLI::Transformer(std::map<int, int>{{15, 5}, {18, 6}, {21, 7}})); + args = {"-s", "15"}; + run(); + EXPECT_EQ(value, "5"); + + args = {"-s", "18"}; + run(); + EXPECT_EQ(value, "6"); + + // value can't be converted to int so it is just ignored + args = {"-s", "abcd"}; + run(); + EXPECT_EQ(value, "abcd"); +} + +TEST_F(TApp, IntTransformNonMerge) { + std::string value; + app.add_option("-s", value) + ->transform(CLI::Transformer(std::map<int, int>{{15, 5}, {18, 6}, {21, 7}}) & + CLI::Transformer(std::map<int, int>{{25, 5}, {28, 6}, {31, 7}}), + "merge"); + args = {"-s", "15"}; + run(); + EXPECT_EQ(value, "5"); + + args = {"-s", "18"}; + run(); + EXPECT_EQ(value, "6"); + + // value can't be converted to int so it is just ignored + args = {"-s", "abcd"}; + run(); + EXPECT_EQ(value, "abcd"); + + args = {"-s", "25"}; + run(); + EXPECT_EQ(value, "5"); + + args = {"-s", "31"}; + run(); + EXPECT_EQ(value, "7"); + + auto help = app.help(); + EXPECT_TRUE(help.find("15->5") != std::string::npos); + EXPECT_TRUE(help.find("25->5") != std::string::npos); + + auto validator = app.get_option("-s")->get_validator(); + help = validator->get_description(); + EXPECT_TRUE(help.find("15->5") != std::string::npos); + EXPECT_TRUE(help.find("25->5") != std::string::npos); + + auto validator2 = app.get_option("-s")->get_validator("merge"); + EXPECT_EQ(validator2, validator); +} + +TEST_F(TApp, IntTransformMergeWithCustomValidator) { + std::string value; + auto opt = app.add_option("-s", value) + ->transform(CLI::Transformer(std::map<int, int>{{15, 5}, {18, 6}, {21, 7}}) | + CLI::Validator( + [](std::string &element) { + if(element == "frog") { + element = "hops"; + } + return std::string{}; + }, + std::string{}), + "check"); + args = {"-s", "15"}; + run(); + EXPECT_EQ(value, "5"); + + args = {"-s", "18"}; + run(); + EXPECT_EQ(value, "6"); + + // value can't be converted to int so it is just ignored + args = {"-s", "frog"}; + run(); + EXPECT_EQ(value, "hops"); + + args = {"-s", "25"}; + run(); + EXPECT_EQ(value, "25"); + + auto help = app.help(); + EXPECT_TRUE(help.find("15->5") != std::string::npos); + EXPECT_TRUE(help.find("OR") == std::string::npos); + + auto validator = opt->get_validator("check"); + EXPECT_EQ(validator->get_name(), "check"); + validator->active(false); + help = app.help(); + EXPECT_TRUE(help.find("15->5") == std::string::npos); +} + +TEST_F(TApp, BoundTests) { + double value; + app.add_option("-s", value)->transform(CLI::Bound(3.4, 5.9)); + args = {"-s", "15"}; + run(); + EXPECT_EQ(value, 5.9); + + args = {"-s", "3.689"}; + run(); + EXPECT_EQ(value, std::stod("3.689")); + + // value can't be converted to int so it is just ignored + args = {"-s", "abcd"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"-s", "2.5"}; + run(); + EXPECT_EQ(value, 3.4); + + auto help = app.help(); + EXPECT_TRUE(help.find("bounded to") != std::string::npos); + EXPECT_TRUE(help.find("[3.4 - 5.9]") != std::string::npos); +} diff --git a/packages/CLI11/tests/TrueFalseTest.cpp b/packages/CLI11/tests/TrueFalseTest.cpp new file mode 100644 index 000000000..de634094b --- /dev/null +++ b/packages/CLI11/tests/TrueFalseTest.cpp @@ -0,0 +1,30 @@ +#include "app_helper.hpp" + +/// This allows a set of strings to be run over by a test +struct TApp_TBO : public TApp, public ::testing::WithParamInterface<const char *> {}; + +TEST_P(TApp_TBO, TrueBoolOption) { + bool value = false; // Not used, but set just in case + app.add_option("-b,--bool", value); + args = {"--bool", GetParam()}; + run(); + EXPECT_EQ(1u, app.count("--bool")); + EXPECT_TRUE(value); +} + +// Change to INSTANTIATE_TEST_SUITE_P in GTest master +INSTANTIATE_TEST_CASE_P(TrueBoolOptions, TApp_TBO, ::testing::Values("true", "on", "True", "ON"), ); + +/// This allows a set of strings to be run over by a test +struct TApp_FBO : public TApp, public ::testing::WithParamInterface<const char *> {}; + +TEST_P(TApp_FBO, FalseBoolOptions) { + bool value = true; // Not used, but set just in case + app.add_option("-b,--bool", value); + args = {"--bool", GetParam()}; + run(); + EXPECT_EQ(1u, app.count("--bool")); + EXPECT_FALSE(value); +} + +INSTANTIATE_TEST_CASE_P(FalseBoolOptions, TApp_FBO, ::testing::Values("false", "off", "False", "OFF"), ); diff --git a/packages/CLI11/tests/WindowsTest.cpp b/packages/CLI11/tests/WindowsTest.cpp index bae812668..58a1faf73 100644 --- a/packages/CLI11/tests/WindowsTest.cpp +++ b/packages/CLI11/tests/WindowsTest.cpp @@ -8,6 +8,6 @@ TEST_F(TApp, WindowsTestSimple) { app.add_flag("-c,--count"); args = {"-c"}; run(); - EXPECT_EQ((size_t)1, app.count("-c")); - EXPECT_EQ((size_t)1, app.count("--count")); + EXPECT_EQ(1u, app.count("-c")); + EXPECT_EQ(1u, app.count("--count")); } diff --git a/packages/CLI11/tests/app_helper.hpp b/packages/CLI11/tests/app_helper.hpp index e2ae3aba0..25f6224d8 100644 --- a/packages/CLI11/tests/app_helper.hpp +++ b/packages/CLI11/tests/app_helper.hpp @@ -16,6 +16,7 @@ struct TApp : public ::testing::Test { input_t args; void run() { + // It is okay to re-parse - clear is called automatically before a parse. input_t newargs = args; std::reverse(std::begin(newargs), std::end(newargs)); app.parse(newargs); @@ -40,7 +41,7 @@ class TempFile { }; inline void put_env(std::string name, std::string value) { -#ifdef _MSC_VER +#ifdef _WIN32 _putenv_s(name.c_str(), value.c_str()); #else setenv(name.c_str(), value.c_str(), 1); @@ -48,7 +49,7 @@ inline void put_env(std::string name, std::string value) { } inline void unset_env(std::string name) { -#ifdef _MSC_VER +#ifdef _WIN32 _putenv_s(name.c_str(), ""); #else unsetenv(name.c_str()); -- GitLab