diff --git a/packages/CLI11/.clang-format b/packages/CLI11/.clang-format index 666a2ba79df8a4ec89c7a2a8c3e59ed7327050e8..caaefd2df1bc8908e9623385409e9739648c27d6 100644 --- a/packages/CLI11/.clang-format +++ b/packages/CLI11/.clang-format @@ -19,7 +19,7 @@ BasedOnStyle: LLVM # AlwaysBreakTemplateDeclarations: false BinPackArguments: false BinPackParameters: false -# BraceWrapping: +# BraceWrapping: # AfterClass: false # AfterControlStatement: false # AfterEnum: false @@ -69,7 +69,7 @@ IndentWidth: 4 # PenaltyReturnTypeOnItsOwnLine: 60 # PointerAlignment: Right # ReflowComments: true -SortIncludes: false +SortIncludes: true # SpaceAfterCStyleCast: false # SpaceAfterTemplateKeyword: true # SpaceBeforeAssignmentOperators: true diff --git a/packages/CLI11/.github/CONTRIBUTING.md b/packages/CLI11/.github/CONTRIBUTING.md index f09e1f2870afb277009a8116c92c767b7cb2580d..c97edaa281dec4e1a84e6461c59679f5a1229d7b 100644 --- a/packages/CLI11/.github/CONTRIBUTING.md +++ b/packages/CLI11/.github/CONTRIBUTING.md @@ -16,16 +16,32 @@ In general, make sure the addition is well thought out and does not increase the * Once you make the PR, tests will run to make sure your code works on all supported platforms * The test coverage is also measured, and that should remain 100% -* Formatting should be done with clang-format, otherwise the format check will not pass. However, it is trivial to apply this to your PR, so don't worry about this check. If you do have clang-format, just run `scripts/check_style.sh` +* Formatting should be done with pre-commit, otherwise the format check will not pass. However, it is trivial to apply this to your PR, so don't worry about this check. If you do want to run it, see below. * Everything must pass clang-tidy as well, run with `-DCLANG_TIDY_FIX-ON` (make sure you use a single threaded build process!) -Note that the style check is really just: + +## Pre-commit + +Format is handled by pre-commit. You should install it: ```bash -git ls-files -- '.cpp' '.hpp' | xargs clang-format -i -style=file +python3 -m pip install pre-commit +``` + +Then, you can run it on the items you've added to your staging area, or all files: + ``` +pre-commit run +# OR +pre-commit run --all-files +``` + + +And, if you want to always use it, you can install it as a git hook (hence the name, pre-commit): -And, if you want to always use it, feel free to install the git hook provided in scripts. +```bash +pre-commit install +``` ## For developers releasing to Conan.io diff --git a/packages/CLI11/.github/workflows/tests.yml b/packages/CLI11/.github/workflows/tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..ac5067405aaf06930f09d299b3f41293b27384da --- /dev/null +++ b/packages/CLI11/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests +on: + push: + branches: + - master + - v* + pull_request: + branches: + - master + +jobs: + formatting: + name: Formatting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.7' + architecture: 'x64' + + - name: Install pre-commit + run: python -m pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all + + - name: Display format changes + run: git diff --exit-code + if: always() + diff --git a/packages/CLI11/.gitrepo b/packages/CLI11/.gitrepo index f47496a8e90c246c228d8424405bcd7f367433b4..92186b6fd81abdd1a47638fd8708a0f045df4e6a 100644 --- a/packages/CLI11/.gitrepo +++ b/packages/CLI11/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:CLIUtils/CLI11.git branch = master - commit = 8ecce8fd2c49f64c80e5757cb12d2fd1fa62f242 - parent = 7de34a384b9d2e24dbbf3707aa01fad53b7df4e6 + commit = 41d3c967d7e22df534e8584db67f18489694a89b + parent = a0ecb7d8ebee7f34714cde913b3d8b358a1fee59 cmdver = 0.4.0 method = merge diff --git a/packages/CLI11/.pre-commit-config.yaml b/packages/CLI11/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..937cce329034da202d16066ee915f94780d7bc25 --- /dev/null +++ b/packages/CLI11/.pre-commit-config.yaml @@ -0,0 +1,27 @@ + +repos: +- repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-added-large-files + - id: mixed-line-ending + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-case-conflict + - id: check-symlinks + - id: check-yaml +- repo: local + hooks: + - id: docker-clang-format + name: Docker Clang Format + language: docker_image + types: + - c++ + entry: unibeautify/clang-format:latest + args: + - -style=file + - -i diff --git a/packages/CLI11/.pre-commit-nodocker.yaml b/packages/CLI11/.pre-commit-nodocker.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5fcaf3e53ef45d86dd497fe7240e46a2023d8a5b --- /dev/null +++ b/packages/CLI11/.pre-commit-nodocker.yaml @@ -0,0 +1,27 @@ + +repos: +- repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-added-large-files + - id: mixed-line-ending + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-case-conflict + - id: check-symlinks + - id: check-yaml +- repo: local + hooks: + - id: clang-format + name: Clang Format + language: system + types: + - c++ + entry: clang-format + args: + - -style=file + - -i diff --git a/packages/CLI11/.travis.yml b/packages/CLI11/.travis.yml index abf69084389972ab39346dc4e98bf64bcada5bf0..831aa36421956d643f82fd1c8307d1c9fa21e5df 100644 --- a/packages/CLI11/.travis.yml +++ b/packages/CLI11/.travis.yml @@ -49,19 +49,13 @@ matrix: # GCC 7 and coverage (8 does not support lcov, wait till 9 and new lcov) - compiler: gcc - env: - - GCC_VER=7 + dist: bionic addons: apt: - sources: - - ubuntu-toolchain-r-test packages: - - g++-7 - curl - lcov install: - - export CC=gcc-7 - - export CXX=g++-7 - DEPS_DIR="${TRAVIS_BUILD_DIR}/deps" - cd $TRAVIS_BUILD_DIR - ". .ci/build_lcov.sh" @@ -73,16 +67,15 @@ matrix: # GCC 4.8 and Conan - compiler: gcc - env: - - GCC_VER=4.8 + dist: bionic addons: apt: packages: - - g++-4.8 + - python3-pip + - python3-setuptools install: - - export CC=gcc-4.8 - - export CXX=g++-4.8 - - python -m pip install --user conan + - python3 -VV + - python3 -m pip install --user conan - conan user script: - .ci/make_and_test.sh 11 diff --git a/packages/CLI11/README.md b/packages/CLI11/README.md index 4f0b1c7641a3d76bd98de1a2cac6a870a0a7e90d..bbaa21badbcf05e6bbd22227513377fe3b25adb0 100644 --- a/packages/CLI11/README.md +++ b/packages/CLI11/README.md @@ -414,21 +414,21 @@ After specifying a set of options, you can also specify "filter" functions of th 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. +- `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({{"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>>` +- `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. @@ -535,6 +535,7 @@ There are several options that are supported on the main app and subcommands and - `.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. +- `.needs(option_or_subcommand)`: 🚧 If given an option pointer or pointer to another subcommand, the subcommands will require the given option to have been given before this subcommand is validated which occurs prior to execution of any callback or after parsing is completed. - `.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. @@ -555,6 +556,7 @@ There are several options that are supported on the main app and subcommands and - `.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. +- `.alias(str)`:🚧 set an alias for the subcommand, this allows subcommands to be called by more than one name. - `.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. diff --git a/packages/CLI11/azure-pipelines.yml b/packages/CLI11/azure-pipelines.yml index d5ac217b049d6f3720577af513d3f13f84823e70..180e29735f8a8abbe41e78ba8404993e3044d345 100644 --- a/packages/CLI11/azure-pipelines.yml +++ b/packages/CLI11/azure-pipelines.yml @@ -15,7 +15,7 @@ variables: jobs: -- job: ClangFormatTidy +- job: ClangTidy variables: CXX_FLAGS: "-Werror -Wcast-align -Wfloat-equal -Wimplicit-atomic-properties -Wmissing-declarations -Woverlength-strings -Wshadow -Wstrict-selector-match -Wundeclared-selector -Wunreachable-code -std=c++11" cli11.options: -DCLANG_TIDY_FIX=ON @@ -26,8 +26,6 @@ jobs: vmImage: 'ubuntu-16.04' container: silkeh/clang:5 steps: - - script: scripts/check_style.sh - displayName: Check format - template: .ci/azure-cmake.yml - template: .ci/azure-build.yml - script: git diff --exit-code --color diff --git a/packages/CLI11/book/chapters/config.md b/packages/CLI11/book/chapters/config.md index d83278a251e058f476ceb59b776449060278514a..85c5b187a86801bc8ba5e6db7ed2bba848c22eb8 100644 --- a/packages/CLI11/book/chapters/config.md +++ b/packages/CLI11/book/chapters/config.md @@ -44,10 +44,10 @@ std::vector<CLI::ConfigItem> from_config(std::istream &input) const; The `CLI::ConfigItem`s that you return are simple structures with a name, a vector of parents, and a vector of results. A optionally customizable `to_flag` method on the formatter lets you change what happens when a ConfigItem turns into a flag. -Finally, set your new class as new config formatter: +Finally, set your new class as new config formatter: ```cpp app.config_formatter(std::make_shared<NewConfig>()); ``` -See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example. +See [`examples/json.cpp`](https://github.com/CLIUtils/CLI11/blob/master/examples/json.cpp) for a complete JSON config example. diff --git a/packages/CLI11/book/chapters/flags.md b/packages/CLI11/book/chapters/flags.md index ef68b860a578332bc7400e91a6e571d78b769655..abab30672905affc0976b9412aaface5461c5c08 100644 --- a/packages/CLI11/book/chapters/flags.md +++ b/packages/CLI11/book/chapters/flags.md @@ -73,7 +73,7 @@ The values would be used like this: [include:"usage"](../code/flags.cpp) -[Source code](https://gitlab.com/CLIUtils/CLI11Tutorial/blob/master/code/flags.cpp) +[Source code](https://gitlab.com/CLIUtils/CLI11Tutorial/blob/master/code/flags.cpp) If you compile and run: diff --git a/packages/CLI11/book/chapters/formatting.md b/packages/CLI11/book/chapters/formatting.md index 8c854f6b11ec9161d8159f721a1a406aa5d7fb61..2af98fae7f31390025e89e58762ec9e87416b477 100644 --- a/packages/CLI11/book/chapters/formatting.md +++ b/packages/CLI11/book/chapters/formatting.md @@ -51,7 +51,7 @@ This is a normal printout, with `<>` indicating the methods used to produce each <make_group(app, "Option Group 2"), false, filter> ... <make_subcommands(app)> - <make_subcommand(sub1, Mode::Normal)> + <make_subcommand(sub1, Mode::Normal)> <make_subcommand(sub2, Mode::Normal)> <make_footer(app)> ``` @@ -61,10 +61,10 @@ This is a normal printout, with `<>` indicating the methods used to produce each The `make_groups` print the group name then call `make_option(o)` on the options listed in that group. The normal printout for an option looks like this: ``` - make_option_opts(o) - ┌───┴────┐ - -n,--name (REQUIRED) This is a description -└────┬────┘ └──────────┬──────────┘ + make_option_opts(o) + ┌───┴────┐ + -n,--name (REQUIRED) This is a description +└────┬────┘ └──────────┬──────────┘ make_option_name(o,p) make_option_desc(o) ``` diff --git a/packages/CLI11/book/chapters/installation.md b/packages/CLI11/book/chapters/installation.md index 4efdfa1bb18b70c98ede2019d6a031f02079ffed..22870740080878da165aa73b2da1370e3d40508d 100644 --- a/packages/CLI11/book/chapters/installation.md +++ b/packages/CLI11/book/chapters/installation.md @@ -1,4 +1,4 @@ -# Installation +# Installation ## Single file edition @@ -50,7 +50,7 @@ root:build # cmake .. root:build # make Scanning dependencies ... root:build # make test -[warning]Running tests... +[warning]Running tests... Test project /CLI11/build Start 1: HelpersTest 1/10 Test #1: HelpersTest ...................... Passed 0.01 sec diff --git a/packages/CLI11/book/chapters/options.md b/packages/CLI11/book/chapters/options.md index 52483157c2558ee430a28a3f67e6c96dca76763d..112959809d1001740015817cab9c64ed6c5944cd 100644 --- a/packages/CLI11/book/chapters/options.md +++ b/packages/CLI11/book/chapters/options.md @@ -20,7 +20,7 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi | Type | CLI11 | |-------------|-------| -| int-like | Integer conversion up to 64-bit, can be unsigned | +| int-like | Integer conversion up to 64-bit, can be unsigned | | float-like | Floating point conversions | | string-like | Anything else that can be shifted into a StringStream | | vector-like | A vector of the above three types (see below) | @@ -54,7 +54,7 @@ If you use a vector instead of a plain option, you can accept more than one valu |-------------------|-----------------| | `--vec 1 --vec 2` | `--vec 1 2` | -The original version did allow the option system to access information on the grouping of options received, but was removed for simplicity. +The original version did allow the option system to access information on the grouping of options received, but was removed for simplicity. An example of setting up a vector option: diff --git a/packages/CLI11/book/chapters/toolkits.md b/packages/CLI11/book/chapters/toolkits.md index c03de23285e207809f55f96f1effbed6eb806422..3b8e93a1ff78f6421f287bb4ce350e36dd2ca68e 100644 --- a/packages/CLI11/book/chapters/toolkits.md +++ b/packages/CLI11/book/chapters/toolkits.md @@ -27,4 +27,4 @@ You can call anything you would like to configure in the constructor, like `opti # Virtual functions provided -You are given a few virtual functions that you can change (only on the main App). `pre_callback` runs right before the callbacks run, letting you print out custom messages at the top of your app. +You are given a few virtual functions that you can change (only on the main App). `pre_callback` runs right before the callbacks run, letting you print out custom messages at the top of your app. diff --git a/packages/CLI11/cmake/CodeCoverage.cmake b/packages/CLI11/cmake/CodeCoverage.cmake index a1b7b86074b8ab1cf6b16665fce4c53541b54ed8..5c3d41c9960ce92d7aaaeae50e5cc11ac94c8b97 100644 --- a/packages/CLI11/cmake/CodeCoverage.cmake +++ b/packages/CLI11/cmake/CodeCoverage.cmake @@ -170,7 +170,7 @@ function(SETUP_TARGET_FOR_COVERAGE) DEPENDS ${Coverage_DEPENDENCIES} COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." ) - + # Show where to find the lcov info report add_custom_command(TARGET ${Coverage_NAME} POST_BUILD COMMAND ; diff --git a/packages/CLI11/conanfile.py b/packages/CLI11/conanfile.py index 13fc77acad7d41da67e08f7eb8b754613c0ecfb3..54c1d5adfdc5b12d431bf81c0f2f5599c6423dfe 100644 --- a/packages/CLI11/conanfile.py +++ b/packages/CLI11/conanfile.py @@ -2,6 +2,7 @@ from conans import ConanFile, CMake from conans.tools import load import re + def get_version(): try: content = load("include/CLI/Version.hpp") @@ -10,6 +11,7 @@ def get_version(): except Exception: return None + class CLI11Conan(ConanFile): name = "CLI11" version = get_version() @@ -21,9 +23,17 @@ class CLI11Conan(ConanFile): license = "BSD-3-Clause" settings = "os", "compiler", "arch", "build_type" - exports_sources = "LICENSE", "README.md", "include/*", "extern/*", "cmake/*", "CMakeLists.txt", "tests/*" + exports_sources = ( + "LICENSE", + "README.md", + "include/*", + "extern/*", + "cmake/*", + "CMakeLists.txt", + "tests/*", + ) - def build(self): # this is not building a library, just tests + def build(self): # this is not building a library, just tests cmake = CMake(self) cmake.definitions["CLI11_EXAMPLES"] = "OFF" cmake.definitions["CLI11_SINGLE_FILE"] = "OFF" diff --git a/packages/CLI11/include/CLI/App.hpp b/packages/CLI11/include/CLI/App.hpp index 3364d00d508bdaa3a6a0a353ddc4aba94691087a..1c238c26efc90254d6cd7781ab1b693a87028c40 100644 --- a/packages/CLI11/include/CLI/App.hpp +++ b/packages/CLI11/include/CLI/App.hpp @@ -159,6 +159,14 @@ class App { /// not be std::set<Option *> exclude_options_; + /// this is a list of subcommands or option groups that are required by this one, the list is not mutual, the + /// listed subcommands do not require this one + std::set<App *> need_subcommands_; + + /// This is a list of options which are required by this app, the list is not mutual, listed options do not need the + /// subcommand not be + std::set<Option *> need_options_; + ///@} /// @name Subcommands ///@{ @@ -213,6 +221,9 @@ class App { /// The group membership INHERITABLE std::string group_{"Subcommands"}; + /// Alias names for the subcommand + std::vector<std::string> aliases_; + ///@} /// @name Config ///@{ @@ -315,11 +326,42 @@ class App { /// Set a name for the app (empty will use parser to set the name) App *name(std::string app_name = "") { - name_ = app_name; + + if(parent_ != nullptr) { + auto oname = name_; + name_ = app_name; + auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent()); + if(!res.empty()) { + name_ = oname; + throw(OptionAlreadyAdded(app_name + " conflicts with existing subcommand names")); + } + } else { + name_ = app_name; + } has_automatic_name_ = false; return this; } + /// Set an alias for the app + App *alias(std::string app_name) { + if(!detail::valid_name_string(app_name)) { + throw(IncorrectConstruction("alias is not a valid name string")); + } + + if(parent_ != nullptr) { + aliases_.push_back(app_name); + auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent()); + if(!res.empty()) { + aliases_.pop_back(); + throw(OptionAlreadyAdded("alias already matches an existing subcommand: " + app_name)); + } + } else { + aliases_.push_back(app_name); + } + + return this; + } + /// Remove the error when extras are left over on the command line. App *allow_extras(bool allow = true) { allow_extras_ = allow; @@ -386,13 +428,16 @@ class App { /// Ignore case. Subcommands inherit value. App *ignore_case(bool value = true) { - ignore_case_ = 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_); + if(value && !ignore_case_) { + ignore_case_ = true; + auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this; + auto &match = _compare_subcommand_names(*this, *p); + if(!match.empty()) { + ignore_case_ = false; // we are throwing so need to be exception invariant + throw OptionAlreadyAdded("ignore case would cause subcommand name conflicts: " + match); } } + ignore_case_ = value; return this; } @@ -411,13 +456,16 @@ class App { /// 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_); + if(value && !ignore_underscore_) { + ignore_underscore_ = true; + auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this; + auto &match = _compare_subcommand_names(*this, *p); + if(!match.empty()) { + ignore_underscore_ = false; + throw OptionAlreadyAdded("ignore underscore would cause subcommand name conflicts: " + match); } } + ignore_underscore_ = value; return this; } @@ -493,7 +541,17 @@ class App { return option.get(); } - throw OptionAlreadyAdded(myopt.get_name()); + // we know something matches now find what it is so we can produce more error information + for(auto &opt : options_) { + auto &matchname = opt->matching_name(myopt); + if(!matchname.empty()) { + throw(OptionAlreadyAdded("added option matched existing option name: " + matchname)); + } + } + // this line should not be reached the above loop should trigger the throw + // LCOV_EXCL_START + throw(OptionAlreadyAdded("added option matched existing option name")); + // LCOV_EXCL_END } /// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`) @@ -1033,6 +1091,9 @@ class App { /// Add a subcommand. Inherits INHERITABLE and OptionDefaults, and help flag App *add_subcommand(std::string subcommand_name = "", std::string subcommand_description = "") { + if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) { + throw IncorrectConstruction("subcommand name is not valid"); + } CLI::App_p subcom = std::shared_ptr<App>(new App(std::move(subcommand_description), subcommand_name, this)); return add_subcommand(std::move(subcom)); } @@ -1041,10 +1102,10 @@ class App { 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_); + auto ckapp = (name_.empty() && parent_ != nullptr) ? _get_fallthrough_parent() : this; + auto &mstrg = _compare_subcommand_names(*subcom, *ckapp); + if(!mstrg.empty()) { + throw(OptionAlreadyAdded("subcommand name or alias matches existing subcommand: " + mstrg)); } subcom->parent_ = this; subcommands_.push_back(std::move(subcom)); @@ -1056,6 +1117,7 @@ class App { // Make sure no links exist for(App_p &sub : subcommands_) { sub->remove_excludes(subcom); + sub->remove_needs(subcom); } auto iterator = std::find_if( @@ -1440,9 +1502,12 @@ class App { /// Sets excluded subcommands for the subcommand App *excludes(App *app) { - if((app == this) || (app == nullptr)) { + if(app == nullptr) { throw OptionNotFound("nullptr passed"); } + if(app == this) { + throw OptionNotFound("cannot self reference in needs"); + } auto res = exclude_subcommands_.insert(app); // subcommand exclusion should be symmetric if(res.second) { @@ -1451,6 +1516,25 @@ class App { return this; } + App *needs(Option *opt) { + if(opt == nullptr) { + throw OptionNotFound("nullptr passed"); + } + need_options_.insert(opt); + return this; + } + + App *needs(App *app) { + if(app == nullptr) { + throw OptionNotFound("nullptr passed"); + } + if(app == this) { + throw OptionNotFound("cannot self reference in needs"); + } + need_subcommands_.insert(app); + return this; + } + /// 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); @@ -1461,7 +1545,7 @@ class App { return true; } - /// Removes a subcommand from this excludes list of this subcommand + /// Removes a subcommand from the 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_)) { @@ -1473,6 +1557,26 @@ class App { return true; } + /// Removes an option from the needs list of this subcommand + bool remove_needs(Option *opt) { + auto iterator = std::find(std::begin(need_options_), std::end(need_options_), opt); + if(iterator == std::end(need_options_)) { + return false; + } + need_options_.erase(iterator); + return true; + } + + /// Removes a subcommand from the needs list of this subcommand + bool remove_needs(App *app) { + auto iterator = std::find(std::begin(need_subcommands_), std::end(need_subcommands_), app); + if(iterator == std::end(need_subcommands_)) { + return false; + } + need_subcommands_.erase(iterator); + return true; + } + ///@} /// @name Help ///@{ @@ -1545,6 +1649,22 @@ class App { return options; } + /// Non-const version of the above + std::vector<Option *> get_options(const std::function<bool(Option *)> filter = {}) { + std::vector<Option *> options(options_.size()); + std::transform(std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) { + return val.get(); + }); + + if(filter) { + options.erase( + std::remove_if(std::begin(options), std::end(options), [&filter](Option *opt) { return !filter(opt); }), + std::end(options)); + } + + return options; + } + /// Get an option by name (noexcept non-const version) Option *get_option_no_throw(std::string option_name) noexcept { for(Option_p &opt : options_) { @@ -1688,7 +1808,16 @@ class App { const App *get_parent() const { return parent_; } /// Get the name of the current app - std::string get_name() const { return name_; } + const std::string &get_name() const { return name_; } + + /// Get the aliases of the current app + const std::vector<std::string> &get_aliases() const { return aliases_; } + + /// clear all the aliases of the current App + App *clear_aliases() { + aliases_.clear(); + return this; + } /// Get a display name for an app std::string get_display_name() const { return (!name_.empty()) ? name_ : "[Option Group: " + get_group() + "]"; } @@ -1705,7 +1834,21 @@ class App { name_to_check = detail::to_lower(name_to_check); } - return local_name == name_to_check; + if(local_name == name_to_check) { + return true; + } + for(auto les : aliases_) { + if(ignore_underscore_) { + les = detail::remove_underscore(les); + } + if(ignore_case_) { + les = detail::to_lower(les); + } + if(les == name_to_check) { + return true; + } + } + return false; } /// Get the groups available directly from this option (in order) @@ -2026,6 +2169,30 @@ class App { // if we are excluded but didn't receive anything, just return return; } + + // check excludes + bool missing_needed{false}; + std::string missing_need; + for(auto &opt : need_options_) { + if(opt->count() == 0) { + missing_needed = true; + missing_need = opt->get_name(); + } + } + for(auto &subc : need_subcommands_) { + if(subc->count_all() == 0) { + missing_needed = true; + missing_need = subc->get_display_name(); + } + } + if(missing_needed) { + if(count_all() > 0) { + throw RequiresError(get_display_name(), missing_need); + } + // if we missing something but didn't have any options, just return + return; + } + size_t used_options = 0; for(const Option_p &opt : options_) { @@ -2287,7 +2454,7 @@ class App { break; case detail::Classifier::NONE: // Probably a positional or something for a parent (sub)command - retval = _parse_positional(args); + retval = _parse_positional(args, false); if(retval && positionals_at_end_) { positional_only = true; } @@ -2327,8 +2494,9 @@ class App { } /// Parse a positional, go up the tree to check + /// @param haltOnSubcommand if set to true the operation will not process subcommands merely return false /// Return true if the positional was used false otherwise - bool _parse_positional(std::vector<std::string> &args) { + bool _parse_positional(std::vector<std::string> &args, bool haltOnSubcommand) { const std::string &positional = args.back(); @@ -2377,7 +2545,7 @@ class App { for(auto &subc : subcommands_) { if((subc->name_.empty()) && (!subc->disabled_)) { - if(subc->_parse_positional(args)) { + if(subc->_parse_positional(args, false)) { if(!subc->pre_parse_called_) { subc->_trigger_pre_parse(args.size()); } @@ -2387,11 +2555,14 @@ class App { } // let the parent deal with it if possible if(parent_ != nullptr && fallthrough_) - return _get_fallthrough_parent()->_parse_positional(args); + return _get_fallthrough_parent()->_parse_positional(args, static_cast<bool>(parse_complete_callback_)); /// 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())) { + if(haltOnSubcommand) { + return false; + } args.pop_back(); com->_parse(args); return true; @@ -2436,7 +2607,8 @@ class App { if(subc != nullptr) { return subc; } - } else if(com->check_name(subc_name)) { + } + if(com->check_name(subc_name)) { if((!*com) || !ignore_used) return com.get(); } @@ -2450,7 +2622,7 @@ class App { /// 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); + _parse_positional(args, false); return true; } auto com = _find_subcommand(args.back(), true, true); @@ -2645,6 +2817,56 @@ class App { return fallthrough_parent; } + /// Helper function to run through all possible comparisons of subcommand names to check there is no overlap + const std::string &_compare_subcommand_names(const App &subcom, const App &base) const { + static const std::string estring; + if(subcom.disabled_) { + return estring; + } + for(auto &subc : base.subcommands_) { + if(subc.get() != &subcom) { + if(subc->disabled_) { + continue; + } + if(!subcom.get_name().empty()) { + if(subc->check_name(subcom.get_name())) { + return subcom.get_name(); + } + } + if(!subc->get_name().empty()) { + if(subcom.check_name(subc->get_name())) { + return subc->get_name(); + } + } + for(const auto &les : subcom.aliases_) { + if(subc->check_name(les)) { + return les; + } + } + // this loop is needed in case of ignore_underscore or ignore_case on one but not the other + for(const auto &les : subc->aliases_) { + if(subcom.check_name(les)) { + return les; + } + } + // if the subcommand is an option group we need to check deeper + if(subc->get_name().empty()) { + auto &cmpres = _compare_subcommand_names(subcom, *subc); + if(!cmpres.empty()) { + return cmpres; + } + } + // if the test subcommand is an option group we need to check deeper + if(subcom.get_name().empty()) { + auto &cmpres = _compare_subcommand_names(*subc, subcom); + if(!cmpres.empty()) { + return cmpres; + } + } + } + } + return estring; + } /// 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()) { @@ -2696,10 +2918,10 @@ class App { app->options_.push_back(std::move(*iterator)); options_.erase(iterator); } else { - throw OptionAlreadyAdded(opt->get_name()); + throw OptionAlreadyAdded("option was not located: " + opt->get_name()); } } else { - throw OptionNotFound("could not locate the given App"); + throw OptionNotFound("could not locate the given Option"); } } }; // namespace CLI diff --git a/packages/CLI11/include/CLI/Option.hpp b/packages/CLI11/include/CLI/Option.hpp index 1a20395ba8d6d9fdbd6a0b546a79ce13f507bc8e..e2123abb379f714877bd3eff24e033730af4834c 100644 --- a/packages/CLI11/include/CLI/Option.hpp +++ b/packages/CLI11/include/CLI/Option.hpp @@ -421,18 +421,19 @@ class Option : public OptionBase<Option> { /// Sets required options Option *needs(Option *opt) { - auto tup = needs_.insert(opt); - if(!tup.second) - throw OptionAlreadyAdded::Requires(get_name(), opt->get_name()); + if(opt != this) { + needs_.insert(opt); + } return this; } /// Can find a string if needed template <typename T = App> Option *needs(std::string opt_name) { - for(const Option_p &opt : dynamic_cast<T *>(parent_)->options_) - if(opt.get() != this && opt->check_name(opt_name)) - return needs(opt.get()); - throw IncorrectConstruction::MissingOption(opt_name); + auto opt = dynamic_cast<T *>(parent_)->get_option_no_throw(opt_name); + if(opt == nullptr) { + throw IncorrectConstruction::MissingOption(opt_name); + } + return needs(opt); } /// Any number supported, any mix of string and Opt @@ -454,6 +455,9 @@ class Option : public OptionBase<Option> { /// Sets excluded options Option *excludes(Option *opt) { + if(opt == this) { + throw(IncorrectConstruction("and option cannot exclude itself")); + } excludes_.insert(opt); // Help text should be symmetric - excluding a should exclude b @@ -467,10 +471,11 @@ class Option : public OptionBase<Option> { /// Can find a string if needed template <typename T = App> Option *excludes(std::string opt_name) { - for(const Option_p &opt : dynamic_cast<T *>(parent_)->options_) - if(opt.get() != this && opt->check_name(opt_name)) - return excludes(opt.get()); - throw IncorrectConstruction::MissingOption(opt_name); + auto opt = dynamic_cast<T *>(parent_)->get_option_no_throw(opt_name); + if(opt == nullptr) { + throw IncorrectConstruction::MissingOption(opt_name); + } + return excludes(opt); } /// Any number supported, any mix of string and Opt @@ -501,13 +506,22 @@ class Option : public OptionBase<Option> { /// 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_case(bool value = true) { - ignore_case_ = 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)); - + if(!ignore_case_ && value) { + ignore_case_ = value; + auto *parent = dynamic_cast<T *>(parent_); + for(const Option_p &opt : parent->options_) { + if(opt.get() == this) { + continue; + } + auto &omatch = opt->matching_name(*this); + if(!omatch.empty()) { + ignore_case_ = false; + throw OptionAlreadyAdded("adding ignore case caused a name conflict with " + omatch); + } + } + } else { + ignore_case_ = value; + } return this; } @@ -516,12 +530,23 @@ class Option : public OptionBase<Option> { /// 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)); + if(!ignore_underscore_ && value) { + ignore_underscore_ = value; + auto *parent = dynamic_cast<T *>(parent_); + for(const Option_p &opt : parent->options_) { + if(opt.get() == this) { + continue; + } + auto &omatch = opt->matching_name(*this); + if(!omatch.empty()) { + ignore_underscore_ = false; + throw OptionAlreadyAdded("adding ignore underscore caused a name conflict with " + omatch); + } + } + } else { + ignore_underscore_ = value; + } return this; } @@ -627,6 +652,8 @@ class Option : public OptionBase<Option> { std::string get_name(bool positional = false, //<[input] Show the positional name bool all_options = false //<[input] Show every option ) const { + if(get_group().empty()) + return {}; // Hidden if(all_options) { @@ -746,26 +773,29 @@ class Option : public OptionBase<Option> { throw ConversionError(get_name(), results_); } - /// If options share any of the same names, they are equal (not counting positional) - bool operator==(const Option &other) const { + /// If options share any of the same names, find it + const std::string &matching_name(const Option &other) const { + static const std::string estring; for(const std::string &sname : snames_) if(other.check_sname(sname)) - return true; + return sname; for(const std::string &lname : lnames_) if(other.check_lname(lname)) - return true; + return lname; 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; + return sname; for(const std::string &lname : other.lnames_) if(check_lname(lname)) - return true; + return lname; } - return false; + return estring; } + /// If options share any of the same names, they are equal (not counting positional) + bool operator==(const Option &other) const { return !matching_name(other).empty(); } /// Check a name. Requires "-" or "--" for short / long, supports positional name bool check_name(std::string name) const { diff --git a/packages/CLI11/include/CLI/Validators.hpp b/packages/CLI11/include/CLI/Validators.hpp index b4586ed11348c1b5fbf0e076fcfd5f75f44bf337..5f68423724c743346c8cf26e18b340a4a9201ae9 100644 --- a/packages/CLI11/include/CLI/Validators.hpp +++ b/packages/CLI11/include/CLI/Validators.hpp @@ -474,9 +474,10 @@ 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.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; } @@ -486,17 +487,18 @@ template <typename T> std::string generate_map(const T &map, bool key_only = fal 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), - [key_only](const iteration_type_t &v) { - std::string res{detail::to_string(detail::pair_adaptor<element_t>::first(v))}; - - if(!key_only) { - res.append("->"); - res += detail::to_string(detail::pair_adaptor<element_t>::second(v)); - } - return res; - }, - ",")); + out.append(detail::join( + detail::smart_deref(map), + [key_only](const iteration_type_t &v) { + std::string res{detail::to_string(detail::pair_adaptor<element_t>::first(v))}; + + if(!key_only) { + res.append("->"); + res += detail::to_string(detail::pair_adaptor<element_t>::second(v)); + } + return res; + }, + ",")); out.push_back('}'); return out; } @@ -657,9 +659,10 @@ class IsMember : public Validator { /// 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...) {} + : 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 @@ -717,9 +720,10 @@ class Transformer : public Validator { /// 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...) {} + : 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 @@ -793,9 +797,10 @@ class CheckedTransformer : public Validator { /// 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...) {} + : CheckedTransformer( + std::forward<T>(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} }; /// Helper function to allow ignore_case to be passed to IsMember or Transform diff --git a/packages/CLI11/scripts/ExtractVersion.py b/packages/CLI11/scripts/ExtractVersion.py index 42d82cd267eb45125cc9e3afc8f6ed3749aa4647..adfd18d4820b69b096e5af3cb3b2f109be7befb6 100755 --- a/packages/CLI11/scripts/ExtractVersion.py +++ b/packages/CLI11/scripts/ExtractVersion.py @@ -3,15 +3,15 @@ import os import re -base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -config_h = os.path.join(base_path, 'include', 'CLI', 'Version.hpp') -data = {'MAJOR': 0, 'MINOR': 0, 'PATCH': 0} -reg = re.compile(r'^\s*#define\s+CLI11_VERSION_([A-Z]+)\s+([0-9]+).*$') +base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +config_h = os.path.join(base_path, "include", "CLI", "Version.hpp") +data = {"MAJOR": 0, "MINOR": 0, "PATCH": 0} +reg = re.compile(r"^\s*#define\s+CLI11_VERSION_([A-Z]+)\s+([0-9]+).*$") -with open(config_h, 'r') as fp: - for l in fp: - m = reg.match(l) - if m: - data[m.group(1)] = int(m.group(2)) +with open(config_h, "r") as fp: + for l in fp: + m = reg.match(l) + if m: + data[m.group(1)] = int(m.group(2)) -print('{}.{}.{}'.format(data['MAJOR'], data['MINOR'], data['PATCH'])) +print("{}.{}.{}".format(data["MAJOR"], data["MINOR"], data["PATCH"])) diff --git a/packages/CLI11/scripts/MakeSingleHeader.py b/packages/CLI11/scripts/MakeSingleHeader.py index ab16799d2321a30984dd454fe843d59c4c1c8276..1163485a3de63706543f73182681bbc083cd21ba 100755 --- a/packages/CLI11/scripts/MakeSingleHeader.py +++ b/packages/CLI11/scripts/MakeSingleHeader.py @@ -24,11 +24,13 @@ CLI11:verbatim # The tag [^\n]* # Up to end of line $ # End of a line """ -verbatim_all = re.compile(verbatim_tag_str + "(.*)" + verbatim_tag_str, - re.MULTILINE | re.DOTALL | re.VERBOSE) +verbatim_all = re.compile( + verbatim_tag_str + "(.*)" + verbatim_tag_str, re.MULTILINE | re.DOTALL | re.VERBOSE +) DIR = os.path.dirname(os.path.abspath(__file__)) + class HeaderFile(object): TAG = "Unknown git revision" LICENSE = "// BSD 3 clause" @@ -43,7 +45,7 @@ class HeaderFile(object): self.__class__.VERSION = version.groups()[0] # add self.verbatim - if 'CLI11:verbatim' in inner: + if "CLI11:verbatim" in inner: self.verbatim = ["\n\n// Verbatim copy from {0}:".format(inc)] self.verbatim += verbatim_all.findall(inner) inner = verbatim_all.sub("", inner) @@ -52,7 +54,7 @@ class HeaderFile(object): self.headers = set(includes_system.findall(inner)) - self.body = '\n// From {0}:\n\n'.format(inc) + inner[inner.find('namespace'):] + self.body = "\n// From {0}:\n\n".format(inc) + inner[inner.find("namespace") :] self.namespace = None @@ -65,11 +67,11 @@ class HeaderFile(object): @property def header_str(self): - return '\n'.join('#include <'+h+'>' for h in sorted(self.headers)) + return "\n".join("#include <" + h + ">" for h in sorted(self.headers)) @property def verbatim_str(self): - return '\n'.join(self.verbatim) + return "\n".join(self.verbatim) def insert_namespace(self, namespace): self.namespace = namespace @@ -79,7 +81,7 @@ class HeaderFile(object): self.body = self.body.replace(before, after) def __str__(self): - result = '''\ + result = """\ #pragma once // CLI11: Version {self.VERSION} @@ -96,21 +98,27 @@ class HeaderFile(object): // Standard combined includes: {self.header_str} -'''.format(self=self) +""".format( + self=self + ) if self.namespace: - result += '\nnamespace ' + self.namespace + ' {\n\n' - result += '{self.verbatim_str}\n{self.body}\n'.format(self=self) + result += "\nnamespace " + self.namespace + " {\n\n" + result += "{self.verbatim_str}\n{self.body}\n".format(self=self) if self.namespace: - result += '} // namespace ' + self.namespace + '\n\n' + result += "} // namespace " + self.namespace + "\n\n" return result -def MakeHeader(output, main_header, include_dir = '../include', namespace=None, macro=None): +def MakeHeader( + output, main_header, include_dir="../include", namespace=None, macro=None +): # Set tag if possible to class variable try: - proc = Popen(['git', 'describe', '--tags', '--always'], cwd=str(DIR), stdout=PIPE) + proc = Popen( + ["git", "describe", "--tags", "--always"], cwd=str(DIR), stdout=PIPE + ) out, _ = proc.communicate() except OSError: pass @@ -120,10 +128,10 @@ def MakeHeader(output, main_header, include_dir = '../include', namespace=None, base_dir = os.path.abspath(os.path.join(DIR, include_dir)) main_header = os.path.join(base_dir, main_header) - licence_file = os.path.abspath(os.path.join(DIR, '../LICENSE')) + licence_file = os.path.abspath(os.path.join(DIR, "../LICENSE")) with open(licence_file) as f: - HeaderFile.LICENSE = ''.join('// ' + line for line in f) + HeaderFile.LICENSE = "".join("// " + line for line in f) with open(main_header) as f: header = f.read() @@ -134,7 +142,7 @@ def MakeHeader(output, main_header, include_dir = '../include', namespace=None, single_header = reduce(add, headers) if macro is not None: - before = 'CLI11_' + before = "CLI11_" print("Converting macros", before, "->", macro) single_header.macro_replacement(before, macro) @@ -142,19 +150,25 @@ def MakeHeader(output, main_header, include_dir = '../include', namespace=None, print("Adding namespace", namespace) single_header.insert_namespace(namespace) - with open(output, 'w') as f: + with open(output, "w") as f: f.write(str(single_header)) print("Created", output) -if __name__ == '__main__': - parser = ArgumentParser(usage='Convert source to single header include. Can optionally add namespace and search-replace replacements (for macros).') + +if __name__ == "__main__": + parser = ArgumentParser( + usage="Convert source to single header include. Can optionally add namespace and search-replace replacements (for macros)." + ) parser.add_argument("output", help="Single header file output") - parser.add_argument("--main", default='CLI/CLI.hpp', help="The main include file that defines the other files") - parser.add_argument("--include", default='../include', help="The include directory") + parser.add_argument( + "--main", + default="CLI/CLI.hpp", + help="The main include file that defines the other files", + ) + parser.add_argument("--include", default="../include", help="The include directory") parser.add_argument("--namespace", help="Add an optional namespace") parser.add_argument("--macro", help="Replaces CLI11_ with NEW_PREFIX_") args = parser.parse_args() MakeHeader(args.output, args.main, args.include, args.namespace, args.macro) - diff --git a/packages/CLI11/scripts/UpdateDownloadProj.py b/packages/CLI11/scripts/UpdateDownloadProj.py index c35743b61f67fa5a81f028177fee7a7c9eca9493..e20797f4f6c942d9dcca25a2ae8afe6069e56269 100755 --- a/packages/CLI11/scripts/UpdateDownloadProj.py +++ b/packages/CLI11/scripts/UpdateDownloadProj.py @@ -5,20 +5,25 @@ from __future__ import print_function, division from plumbum import local, cli, FG from plumbum.cmd import curl -FILES = [ 'https://raw.githubusercontent.com/Crascit/DownloadProject/master/DownloadProject.cmake', - 'https://raw.githubusercontent.com/Crascit/DownloadProject/master/DownloadProject.CMakeLists.cmake.in'] +FILES = [ + "https://raw.githubusercontent.com/Crascit/DownloadProject/master/DownloadProject.cmake", + "https://raw.githubusercontent.com/Crascit/DownloadProject/master/DownloadProject.CMakeLists.cmake.in", +] DIR = local.path(__file__).dirname + def download_file(path): - name = path.split('/')[-1] + name = path.split("/")[-1] (curl[path] > name) & FG + class UpdateDownloadProj(cli.Application): def main(self): - with local.cwd(DIR / '../cmake'): + with local.cwd(DIR / "../cmake"): for f in FILES: download_file(f) + if __name__ == "__main__": UpdateDownloadProj() diff --git a/packages/CLI11/test_package/conanfile.py b/packages/CLI11/test_package/conanfile.py index 0202a8a828e7578bbc0de4afef5bbe0c7728975a..91b91ddd563245284ff96e244c0e70c587f1111e 100644 --- a/packages/CLI11/test_package/conanfile.py +++ b/packages/CLI11/test_package/conanfile.py @@ -1,6 +1,7 @@ from conans import ConanFile, CMake import os + class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "cmake" diff --git a/packages/CLI11/tests/AppTest.cpp b/packages/CLI11/tests/AppTest.cpp index 8037cae2f47f84850f17a341914036db891b5a5b..63a2f67b4aa55b49f4a1dbd981c6d9b63da19df9 100644 --- a/packages/CLI11/tests/AppTest.cpp +++ b/packages/CLI11/tests/AppTest.cpp @@ -1792,6 +1792,8 @@ TEST_F(TApp, NeedsFlags) { args = {"--both"}; EXPECT_THROW(run(), CLI::RequiresError); + + EXPECT_NO_THROW(opt->needs(opt)); } TEST_F(TApp, ExcludesFlags) { @@ -1811,6 +1813,8 @@ TEST_F(TApp, ExcludesFlags) { args = {"--string", "--nostr"}; EXPECT_THROW(run(), CLI::ExcludesError); + + EXPECT_THROW(opt->excludes(opt), CLI::IncorrectConstruction); } TEST_F(TApp, ExcludesMixedFlags) { diff --git a/packages/CLI11/tests/CreationTest.cpp b/packages/CLI11/tests/CreationTest.cpp index 682a0cd57186652abee840a78f1143a59ec887bf..5e428860276b81ad8af8cfdbb890cfab44676295 100644 --- a/packages/CLI11/tests/CreationTest.cpp +++ b/packages/CLI11/tests/CreationTest.cpp @@ -227,14 +227,16 @@ TEST_F(TApp, IncorrectConstructionDuplicateNeeds) { auto cat = app.add_flag("--cat"); auto other = app.add_flag("--other"); ASSERT_NO_THROW(cat->needs(other)); - EXPECT_THROW(cat->needs(other), CLI::OptionAlreadyAdded); + // duplicated needs is redundant but not an error + EXPECT_NO_THROW(cat->needs(other)); } TEST_F(TApp, IncorrectConstructionDuplicateNeedsTxt) { auto cat = app.add_flag("--cat"); app.add_flag("--other"); ASSERT_NO_THROW(cat->needs("--other")); - EXPECT_THROW(cat->needs("--other"), CLI::OptionAlreadyAdded); + // duplicate needs is redundant but not an error + EXPECT_NO_THROW(cat->needs("--other")); } // Now allowed @@ -562,11 +564,17 @@ TEST_F(TApp, GetOptionList) { auto flag = app.add_flag("--one"); auto opt = app.add_option("--two", two); - auto opt_list = app.get_options(); + const CLI::App &const_app = app; // const alias to force use of const-methods + std::vector<const CLI::Option *> opt_list = const_app.get_options(); ASSERT_EQ(opt_list.size(), static_cast<size_t>(3)); EXPECT_EQ(opt_list.at(1), flag); EXPECT_EQ(opt_list.at(2), opt); + + std::vector<CLI::Option *> nonconst_opt_list = app.get_options(); + for(size_t i = 0; i < opt_list.size(); ++i) { + EXPECT_EQ(nonconst_opt_list.at(i), opt_list.at(i)); + } } TEST(ValidatorTests, TestValidatorCreation) { diff --git a/packages/CLI11/tests/HelpTest.cpp b/packages/CLI11/tests/HelpTest.cpp index 229d441b06d98d50ff9ab0fe01325783665c1888..9be442ef19175267488d45d37adf6765aa7e7653 100644 --- a/packages/CLI11/tests/HelpTest.cpp +++ b/packages/CLI11/tests/HelpTest.cpp @@ -92,7 +92,7 @@ TEST(THelp, Hidden) { EXPECT_THAT(help, HasSubstr("My prog")); EXPECT_THAT(help, HasSubstr("-h,--help")); EXPECT_THAT(help, HasSubstr("Options:")); - EXPECT_THAT(help, HasSubstr("[something]")); + EXPECT_THAT(help, Not(HasSubstr("[something]"))); EXPECT_THAT(help, Not(HasSubstr("something "))); EXPECT_THAT(help, Not(HasSubstr("another"))); } diff --git a/packages/CLI11/tests/OptionGroupTest.cpp b/packages/CLI11/tests/OptionGroupTest.cpp index 2620e1b9aa6fe145a05e2a4a8e119350051acfad..e109c3bf26675226a789e3d9faddc887c84446d2 100644 --- a/packages/CLI11/tests/OptionGroupTest.cpp +++ b/packages/CLI11/tests/OptionGroupTest.cpp @@ -438,6 +438,52 @@ TEST_F(ManyGroups, ExcludesGroup) { EXPECT_FALSE(g1->remove_excludes(g2)); } +TEST_F(ManyGroups, NeedsGroup) { + remove_required(); + // all groups needed if g1 is used + g1->needs(g2); + g1->needs(g3); + args = {"--name1", "test"}; + EXPECT_THROW(run(), CLI::RequiresError); + // other groups should run fine + args = {"--name2", "test2"}; + + run(); + // all three groups should be fine + args = {"--name1", "test", "--name2", "test2", "--name3", "test3"}; + + EXPECT_NO_THROW(run()); +} + +// test adding an option group with existing subcommands to an app +TEST_F(TApp, ExistingSubcommandMatch) { + auto sshared = std::make_shared<CLI::Option_group>("documenting the subcommand", "sub1g", nullptr); + auto s1 = sshared->add_subcommand("sub1"); + auto o1 = sshared->add_option_group("opt1"); + o1->add_subcommand("sub3")->alias("sub4"); + + app.add_subcommand("sub1"); + + try { + app.add_subcommand(sshared); + // this should throw the next line should never be reached + EXPECT_FALSE(true); + } catch(const CLI::OptionAlreadyAdded &oaa) { + EXPECT_THAT(oaa.what(), HasSubstr("sub1")); + } + sshared->remove_subcommand(s1); + + app.add_subcommand("sub3"); + // now check that the subsubcommand overlaps + try { + app.add_subcommand(sshared); + // this should throw the next line should never be reached + EXPECT_FALSE(true); + } catch(const CLI::OptionAlreadyAdded &oaa) { + EXPECT_THAT(oaa.what(), HasSubstr("sub3")); + } +} + TEST_F(ManyGroups, SingleGroupError) { // only 1 group can be used main->require_option(1); @@ -503,7 +549,7 @@ TEST_F(ManyGroups, RequiredFirst) { } TEST_F(ManyGroups, DisableFirst) { - // only 1 group can be used + // only 1 group can be used if remove_required not used remove_required(); g1->disabled(); @@ -521,12 +567,15 @@ TEST_F(ManyGroups, DisableFirst) { } TEST_F(ManyGroups, SameSubcommand) { - // only 1 group can be used + // only 1 group can be used if remove_required not used remove_required(); - auto sub1 = g1->add_subcommand("sub1"); - auto sub2 = g2->add_subcommand("sub1"); + auto sub1 = g1->add_subcommand("sub1")->disabled(); + auto sub2 = g2->add_subcommand("sub1")->disabled(); auto sub3 = g3->add_subcommand("sub1"); - + // so when the subcommands are disabled they can have the same name + sub1->disabled(false); + sub2->disabled(false); + // if they are reenabled they are not checked for overlap on enabling so they can have the same name args = {"sub1", "sub1", "sub1"}; run(); @@ -534,7 +583,6 @@ TEST_F(ManyGroups, SameSubcommand) { 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); @@ -556,7 +604,7 @@ TEST_F(ManyGroups, SameSubcommand) { EXPECT_EQ(subs[2], sub3); } TEST_F(ManyGroups, CallbackOrder) { - // only 1 group can be used + // only 1 group can be used if remove_required not used remove_required(); std::vector<int> callback_order; g1->callback([&callback_order]() { callback_order.push_back(1); }); @@ -582,6 +630,7 @@ TEST_F(ManyGroups, CallbackOrder) { // Test the fallthrough for extra arguments TEST_F(ManyGroups, ExtrasFallDown) { + // only 1 group can be used if remove_required not used remove_required(); args = {"--test1", "--flag", "extra"}; diff --git a/packages/CLI11/tests/OptionalTest.cpp b/packages/CLI11/tests/OptionalTest.cpp index 32c29d303f2a3d5f1ffb8424f33d93f5fd60bec7..2710061d0d5b36d125a261a173477a9979e2ee25 100644 --- a/packages/CLI11/tests/OptionalTest.cpp +++ b/packages/CLI11/tests/OptionalTest.cpp @@ -162,7 +162,8 @@ TEST_F(TApp, BoostOptionalEnumTest) { 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") + 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); diff --git a/packages/CLI11/tests/SubcommandTest.cpp b/packages/CLI11/tests/SubcommandTest.cpp index fa6309365f81032e5628ee31c69fd21055a36e9f..336dd924bbcf1a11e39b3b0e933ec456d28e14e7 100644 --- a/packages/CLI11/tests/SubcommandTest.cpp +++ b/packages/CLI11/tests/SubcommandTest.cpp @@ -93,6 +93,17 @@ TEST_F(TApp, MultiSubFallthrough) { EXPECT_THROW(app.got_subcommand("sub3"), CLI::OptionNotFound); } +TEST_F(TApp, CrazyNameSubcommand) { + auto sub1 = app.add_subcommand("sub1"); + // name can be set to whatever + EXPECT_NO_THROW(sub1->name("crazy name with spaces")); + args = {"crazy name with spaces"}; + run(); + + EXPECT_TRUE(app.got_subcommand("crazy name with spaces")); + EXPECT_EQ(sub1->count(), 1u); +} + TEST_F(TApp, RequiredAndSubcoms) { // #23 std::string baz; @@ -272,6 +283,46 @@ TEST_F(TApp, CallbackOrder) { EXPECT_EQ(cb[6], "c2"); EXPECT_EQ(cb[7], "ac2"); } + +TEST_F(TApp, CallbackOrder2) { + + std::vector<std::string> cb; + app.add_subcommand("sub1")->parse_complete_callback([&cb]() { cb.push_back("sub1"); }); + app.add_subcommand("sub2")->parse_complete_callback([&cb]() { cb.push_back("sub2"); }); + app.add_subcommand("sub3")->parse_complete_callback([&cb]() { cb.push_back("sub3"); }); + + args = {"sub1", "sub2", "sub3", "sub1", "sub1", "sub2", "sub1"}; + run(); + EXPECT_EQ(cb.size(), 7u); + EXPECT_EQ(cb[0], "sub1"); + EXPECT_EQ(cb[1], "sub2"); + EXPECT_EQ(cb[2], "sub3"); + EXPECT_EQ(cb[3], "sub1"); + EXPECT_EQ(cb[4], "sub1"); + EXPECT_EQ(cb[5], "sub2"); + EXPECT_EQ(cb[6], "sub1"); +} + +TEST_F(TApp, CallbackOrder2_withFallthrough) { + + std::vector<std::string> cb; + + app.add_subcommand("sub1")->parse_complete_callback([&cb]() { cb.push_back("sub1"); })->fallthrough(); + app.add_subcommand("sub2")->parse_complete_callback([&cb]() { cb.push_back("sub2"); }); + app.add_subcommand("sub3")->parse_complete_callback([&cb]() { cb.push_back("sub3"); }); + + args = {"sub1", "sub2", "sub3", "sub1", "sub1", "sub2", "sub1"}; + run(); + EXPECT_EQ(cb.size(), 7u); + EXPECT_EQ(cb[0], "sub1"); + EXPECT_EQ(cb[1], "sub2"); + EXPECT_EQ(cb[2], "sub3"); + EXPECT_EQ(cb[3], "sub1"); + EXPECT_EQ(cb[4], "sub1"); + EXPECT_EQ(cb[5], "sub2"); + EXPECT_EQ(cb[6], "sub1"); +} + TEST_F(TApp, RuntimeErrorInCallback) { auto sub1 = app.add_subcommand("sub1"); sub1->callback([]() { throw CLI::RuntimeError(); }); @@ -420,7 +471,7 @@ TEST_F(TApp, Nameless4LayerDeep) { } /// Put subcommands in some crazy pattern and make everything still works -TEST_F(TApp, Nameless4LayerDeepMulit) { +TEST_F(TApp, Nameless4LayerDeepMulti) { auto sub1 = app.add_subcommand(); auto sub2 = app.add_subcommand(); @@ -1248,6 +1299,93 @@ TEST_F(ManySubcommands, SubcommandOptionExclusion) { } } +TEST_F(ManySubcommands, SubcommandNeeds) { + + sub1->needs(sub2); + args = {"sub1", "sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub2"}; + EXPECT_NO_THROW(run()); + + args = {"sub1"}; + EXPECT_THROW(run(), CLI::RequiresError); + + sub1->needs(sub3); + args = {"sub1", "sub2", "sub3"}; + EXPECT_NO_THROW(run()); + + args = {"sub1", "sub2", "sub4"}; + EXPECT_THROW(run(), CLI::RequiresError); + + args = {"sub1", "sub2", "sub4"}; + sub1->remove_needs(sub3); + EXPECT_NO_THROW(run()); +} + +TEST_F(ManySubcommands, SubcommandNeedsOptions) { + + auto opt = app.add_flag("--subactive"); + sub1->needs(opt); + sub1->fallthrough(); + args = {"sub1", "--subactive"}; + EXPECT_NO_THROW(run()); + + args = {"sub1"}; + EXPECT_THROW(run(), CLI::RequiresError); + + args = {"--subactive"}; + EXPECT_NO_THROW(run()); + + auto opt2 = app.add_flag("--subactive2"); + + sub1->needs(opt2); + args = {"sub1", "--subactive"}; + EXPECT_THROW(run(), CLI::RequiresError); + + args = {"--subactive", "--subactive2", "sub1"}; + EXPECT_NO_THROW(run()); + + sub1->remove_needs(opt2); + args = {"sub1", "--subactive"}; + EXPECT_NO_THROW(run()); +} + +TEST_F(ManySubcommands, SubcommandNeedsOptionsCallbackOrdering) { + int count = 0; + auto opt = app.add_flag("--subactive"); + app.add_flag("--flag1"); + sub1->needs(opt); + sub1->fallthrough(); + sub1->parse_complete_callback([&count]() { ++count; }); + args = {"sub1", "--flag1", "sub1", "--subactive"}; + EXPECT_THROW(run(), CLI::RequiresError); + // the subcommand has to pass validation by the first callback + sub1->immediate_callback(false); + // now since the callback executes after + + EXPECT_NO_THROW(run()); + EXPECT_EQ(count, 1); + sub1->immediate_callback(); + args = {"--subactive", "sub1"}; + // now the required is processed first + EXPECT_NO_THROW(run()); +} + +TEST_F(ManySubcommands, SubcommandNeedsFail) { + + auto opt = app.add_flag("--subactive"); + auto opt2 = app.add_flag("--dummy"); + sub1->needs(opt); + EXPECT_THROW(sub1->needs((CLI::Option *)nullptr), CLI::OptionNotFound); + EXPECT_THROW(sub1->needs((CLI::App *)nullptr), CLI::OptionNotFound); + EXPECT_THROW(sub1->needs(sub1), CLI::OptionNotFound); + + EXPECT_TRUE(sub1->remove_needs(opt)); + EXPECT_FALSE(sub1->remove_needs(opt2)); + EXPECT_FALSE(sub1->remove_needs(sub1)); +} + TEST_F(ManySubcommands, SubcommandRequired) { sub1->required(); @@ -1377,6 +1515,176 @@ TEST_F(TApp, UnnamedSubNoExtras) { EXPECT_EQ(sub->remaining_size(), 0u); } +TEST_F(TApp, SubcommandAlias) { + double val; + auto sub = app.add_subcommand("sub1"); + sub->alias("sub2"); + sub->alias("sub3"); + sub->add_option("-v,--value", val); + args = {"sub1", "-v", "-3"}; + run(); + EXPECT_EQ(val, -3.0); + + args = {"sub2", "--value", "-5"}; + run(); + EXPECT_EQ(val, -5.0); + + args = {"sub3", "-v", "7"}; + run(); + EXPECT_EQ(val, 7); + + auto &al = sub->get_aliases(); + ASSERT_GE(al.size(), 2U); + + EXPECT_EQ(al[0], "sub2"); + EXPECT_EQ(al[1], "sub3"); + + sub->clear_aliases(); + EXPECT_TRUE(al.empty()); +} + +TEST_F(TApp, SubcommandAliasIgnoreCaseUnderscore) { + double val; + auto sub = app.add_subcommand("sub1"); + sub->alias("sub2"); + sub->alias("sub3"); + sub->ignore_case(); + sub->add_option("-v,--value", val); + args = {"sub1", "-v", "-3"}; + run(); + EXPECT_EQ(val, -3.0); + + args = {"SUB2", "--value", "-5"}; + run(); + EXPECT_EQ(val, -5.0); + + args = {"sUb3", "-v", "7"}; + run(); + EXPECT_EQ(val, 7); + sub->ignore_underscore(); + args = {"sub_1", "-v", "-3"}; + run(); + EXPECT_EQ(val, -3.0); + + args = {"SUB_2", "--value", "-5"}; + run(); + EXPECT_EQ(val, -5.0); + + args = {"sUb_3", "-v", "7"}; + run(); + EXPECT_EQ(val, 7); + + sub->ignore_case(false); + args = {"sub_1", "-v", "-3"}; + run(); + EXPECT_EQ(val, -3.0); + + args = {"SUB_2", "--value", "-5"}; + EXPECT_THROW(run(), CLI::ExtrasError); + + args = {"sUb_3", "-v", "7"}; + EXPECT_THROW(run(), CLI::ExtrasError); +} + +TEST_F(TApp, OptionGroupAlias) { + double val; + auto sub = app.add_option_group("sub1"); + sub->alias("sub2"); + sub->alias("sub3"); + sub->add_option("-v,--value", val); + args = {"sub1", "-v", "-3"}; + EXPECT_THROW(run(), CLI::ExtrasError); + + args = {"sub2", "--value", "-5"}; + run(); + EXPECT_EQ(val, -5.0); + + args = {"sub3", "-v", "7"}; + run(); + EXPECT_EQ(val, 7); + + args = {"-v", "-3"}; + run(); + EXPECT_EQ(val, -3); +} + +TEST_F(TApp, AliasErrors) { + auto sub1 = app.add_subcommand("sub1"); + auto sub2 = app.add_subcommand("sub2"); + + EXPECT_THROW(sub2->alias("this is a not a valid alias"), CLI::IncorrectConstruction); + EXPECT_THROW(sub2->alias("-alias"), CLI::IncorrectConstruction); + EXPECT_THROW(sub2->alias("alia$"), CLI::IncorrectConstruction); + + EXPECT_THROW(app.add_subcommand("--bad_subcommand_name", "documenting the bad subcommand"), + CLI::IncorrectConstruction); + + EXPECT_THROW(app.add_subcommand("documenting a subcommand", "sub3"), CLI::IncorrectConstruction); + // cannot alias to an existing subcommand + EXPECT_THROW(sub2->alias("sub1"), CLI::OptionAlreadyAdded); + EXPECT_THROW(sub1->alias("sub2"), CLI::OptionAlreadyAdded); + // aliasing to an existing name should be allowed + EXPECT_NO_THROW(sub1->alias(sub1->get_name())); + + sub1->alias("les1")->alias("les2")->alias("les_3"); + sub2->alias("s2les1")->alias("s2les2")->alias("s2les3"); + + EXPECT_THROW(sub2->alias("les2"), CLI::OptionAlreadyAdded); + EXPECT_THROW(sub1->alias("s2les2"), CLI::OptionAlreadyAdded); + + EXPECT_THROW(sub2->name("sub1"), CLI::OptionAlreadyAdded); + sub2->ignore_underscore(); + EXPECT_THROW(sub2->alias("les3"), CLI::OptionAlreadyAdded); +} +// test adding a subcommand via the pointer +TEST_F(TApp, ExistingSubcommandMatch) { + auto sshared = std::make_shared<CLI::App>("documenting the subcommand", "sub1"); + sshared->alias("sub2")->alias("sub3"); + + EXPECT_EQ(sshared->get_name(), "sub1"); + app.add_subcommand("sub1"); + + try { + app.add_subcommand(sshared); + // this should throw the next line should never be reached + EXPECT_FALSE(true); + } catch(const CLI::OptionAlreadyAdded &oaa) { + EXPECT_THAT(oaa.what(), HasSubstr("sub1")); + } + sshared->name("osub"); + app.add_subcommand("sub2"); + // now check that the aliases don't overlap + try { + app.add_subcommand(sshared); + // this should throw the next line should never be reached + EXPECT_FALSE(true); + } catch(const CLI::OptionAlreadyAdded &oaa) { + EXPECT_THAT(oaa.what(), HasSubstr("sub2")); + } + // now check that disabled subcommands can be added regardless of name + sshared->name("sub1"); + sshared->disabled(); + EXPECT_NO_THROW(app.add_subcommand(sshared)); +} + +TEST_F(TApp, AliasErrorsInOptionGroup) { + auto sub1 = app.add_subcommand("sub1"); + auto g2 = app.add_option_group("g1"); + auto sub2 = g2->add_subcommand("sub2"); + + // cannot alias to an existing subcommand even if it is in an option group + EXPECT_THROW(sub2->alias("sub1"), CLI::OptionAlreadyAdded); + EXPECT_THROW(sub1->alias("sub2"), CLI::OptionAlreadyAdded); + + sub1->alias("les1")->alias("les2")->alias("les3"); + sub2->alias("s2les1")->alias("s2les2")->alias("s2les3"); + + EXPECT_THROW(sub2->alias("les2"), CLI::OptionAlreadyAdded); + EXPECT_THROW(sub1->alias("s2les2"), CLI::OptionAlreadyAdded); + + EXPECT_THROW(sub2->name("sub1"), CLI::OptionAlreadyAdded); +} + TEST(SharedSubTests, SharedSubcommand) { double val, val2, val3, val4; CLI::App app1{"test program1"};