add permission validators as an Extra Validator (#1203)

an update of #250 

Fixes #249

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Philip Top
2025-09-08 05:16:49 -07:00
committed by GitHub
parent ee326d647b
commit c8dc5f627a
7 changed files with 247 additions and 24 deletions

View File

@@ -38,8 +38,9 @@ set with a simple and intuitive interface.
- [Option options](#option-options)
- [Validators](#validators)
- [Default Validators](#default-validators)
- [Validatrs that may be disabled 🚧](#validatrs-that-may-be-disabled-)
- [Validators that may be disabled 🚧](#validators-that-may-be-disabled-)
- [Extra Validators 🚧](#extra-validators-)
- [permission. Requires C++17.](#permission-requires-c17)
- [Validator Usage](#validator-usage)
- [Transforming Validators](#transforming-validators)
- [Validator operations](#validator-operations)
@@ -575,7 +576,9 @@ they can be disabled by using
#### Default Validators
These validators are always available regardless of definitions
These validators are always available regardless of definitions. These are used
internally or are very commonly used, so will always remain available regardless
of flags.
- `CLI::ExistingFile`: Requires that the file exists if given.
- `CLI::ExistingDirectory`: Requires that the directory exists.
@@ -590,11 +593,14 @@ These validators are always available regardless of definitions
- `CLI::NonNegativeNumber`: Requires the number be greater or equal to 0
- `CLI::Number`: Requires the input be a number.
#### Validatrs that may be disabled 🚧
#### Validators that may be disabled 🚧
Validators that may be disabled by setting `CLI11_DISABLE_EXTRA_VALIDATORS` to 1
or enabled by setting `CLI11_ENABLE_EXTRA_VALIDATORS` to 1. By default they are
enabled.
enabled. In version 3.0 these will likely move to be disabled by default and be
controlled solely by the `CLI11_ENABLE_EXTRA_VALIDATORS` option. These
validators are less commonly used or are template heavy and require additional
computation time that may not be valuable for some use cases.
- `CLI::IsMember(...)`: Require an option be a member of a given set. See
[Transforming Validators](#transforming-validators) for more details.
@@ -627,6 +633,14 @@ enabled.
New validators will go into code sections that must be explicitly enabled by
setting `CLI11_ENABLE_EXTRA_VALIDATORS` to 1
- `CLI::ReadPermission`: Requires that the file or folder given exist and have
read permission. Requires C++17.
- `CLI::WritePermission`: Requires that the file or folder given exist and have
write permission. Requires C++17.
- `CLI::ExecPermission`: Requires that the file given exist and have execution
permission. Requires C++17.
-
#### Validator Usage
These Validators once enabled can be used by simply passing the name into the

View File

@@ -34,7 +34,9 @@ jobs:
vmImage: "windows-2025"
cli11.std: 17
cli11.build_type: Debug
cli11.options: -G "Visual Studio 17 2022" -A ARM64
cli11.options:
-G "Visual Studio 17 2022" -A ARM64
-DCLI11_ENABLE_EXTRA_VALIDATORS=1
pool:
vmImage: $(vmImage)
@@ -52,12 +54,15 @@ jobs:
macOS-15_23:
vmImage: "macOS-15"
cli11.std: 23
cli11.options: -DCLI11_ENABLE_EXTRA_VALIDATORS=1
macOS-14_20:
vmImage: "macOS-14"
cli11.std: 20
cli11.options: -DCLI11_ENABLE_EXTRA_VALIDATORS=1
macOS-13_17:
vmImage: "macOS-13"
cli11.std: 17
cli11.options: -DCLI11_ENABLE_EXTRA_VALIDATORS=1
macOS-14_11:
vmImage: "macOS-14"
cli11.std: 11
@@ -68,10 +73,12 @@ jobs:
Windows17:
vmImage: "windows-2022"
cli11.std: 17
cli11.options: -DCLI11_ENABLE_EXTRA_VALIDATORS=1
Windows17PC:
vmImage: "windows-2022"
cli11.std: 17
cli11.precompile: ON
cli11.options: -DCLI11_ENABLE_EXTRA_VALIDATORS=1
Windows11:
vmImage: "windows-2022"
cli11.std: 11
@@ -87,7 +94,8 @@ jobs:
Linux17nortti:
vmImage: "ubuntu-latest"
cli11.std: 17
cli11.options: -DCMAKE_CXX_FLAGS="-fno-rtti"
cli11.options:
-DCMAKE_CXX_FLAGS="-fno-rtti" -DCLI11_ENABLE_EXTRA_VALIDATORS=1
pool:
vmImage: $(vmImage)
steps:
@@ -126,7 +134,9 @@ jobs:
gcc9:
containerImage: gcc:9
cli11.std: 17
cli11.options: -DCMAKE_CXX_FLAGS="-Wstrict-overflow=5"
cli11.options:
-DCMAKE_CXX_FLAGS="-Wstrict-overflow=5"
-DCLI11_ENABLE_EXTRA_VALIDATORS=1
gcc11:
containerImage: gcc:11
cli11.std: 20
@@ -143,19 +153,24 @@ jobs:
clang3.4:
containerImage: silkeh/clang:3.4
cli11.std: 11
cli11.options: -DCLI11_WARNINGS_AS_ERRORS=OFF
cli11.options:
-DCLI11_WARNINGS_AS_ERRORS=OFF -DCLI11_DISABLE_EXTRA_VALIDATORS=1
clang8:
containerImage: silkeh/clang:8
cli11.std: 14
cli11.options: -DCLI11_FORCE_LIBCXX=ON
cli11.options:
-DCLI11_FORCE_LIBCXX=ON -DCLI11_DISABLE_EXTRA_VALIDATORS=1
clang8_17:
containerImage: silkeh/clang:8
cli11.std: 17
cli11.options: -DCLI11_FORCE_LIBCXX=ON
cli11.options:
-DCLI11_FORCE_LIBCXX=ON -DCLI11_ENABLE_EXTRA_VALIDATORS=1
clang10_20:
containerImage: silkeh/clang:10
cli11.std: 20
cli11.options: -DCLI11_FORCE_LIBCXX=ON -DCMAKE_CXX_FLAGS=-std=c++20
cli11.options:
-DCLI11_FORCE_LIBCXX=ON -DCMAKE_CXX_FLAGS=-std=c++20
-DCLI11_ENABLE_EXTRA_VALIDATORS=1
container: $[ variables['containerImage'] ]
steps:
- template: .ci/azure-cmake.yml
@@ -172,19 +187,25 @@ jobs:
gcc13_17:
containerImage: gcc:13
cli11.std: 17
cli11.options: -DCMAKE_CXX_FLAGS="-Wstrict-overflow=5"
cli11.options:
-DCMAKE_CXX_FLAGS="-Wstrict-overflow=5"
-DCLI11_ENABLE_EXTRA_VALIDATORS=1
gcc12_20:
containerImage: gcc:12
cli11.std: 20
cli11.options: -DCMAKE_CXX_FLAGS="-Wredundant-decls -Wconversion"
cli11.options:
-DCMAKE_CXX_FLAGS="-Wredundant-decls -Wconversion"
-DCLI11_ENABLE_EXTRA_VALIDATORS=1
clang17_23:
containerImage: silkeh/clang:17
cli11.std: 23
cli11.options: -DCMAKE_CXX_FLAGS=-std=c++23
cli11.options:
-DCMAKE_CXX_FLAGS=-std=c++23 -DCLI11_ENABLE_EXTRA_VALIDATORS=1
clang20_26:
containerImage: silkeh/clang:20
cli11.std: 26
cli11.options: -DCMAKE_CXX_FLAGS=-std=c++2c
cli11.options:
-DCMAKE_CXX_FLAGS=-std=c++2c -DCLI11_ENABLE_EXTRA_VALIDATORS=1
container: $[ variables['containerImage'] ]
steps:
- template: .ci/azure-cmake-new.yml

View File

@@ -4,6 +4,8 @@
//
// SPDX-License-Identifier: BSD-3-Clause
#define CLI11_ENABLE_EXTRA_VALIDATORS 1
#pragma once
#if (defined(CLI11_ENABLE_EXTRA_VALIDATORS) && CLI11_ENABLE_EXTRA_VALIDATORS == 1) || \
(!defined(CLI11_DISABLE_EXTRA_VALIDATORS) || CLI11_DISABLE_EXTRA_VALIDATORS == 0)
@@ -591,6 +593,24 @@ class AsSizeValue : public AsNumberWithUnit {
#if defined(CLI11_ENABLE_EXTRA_VALIDATORS) && CLI11_ENABLE_EXTRA_VALIDATORS != 0
// new extra validators
#if CLI11_HAS_FILESYSTEM
namespace detail {
enum class Permission : std::uint8_t { none = 0, read = 1, write = 2, exec = 4 };
class PermissionValidator : public Validator {
public:
explicit PermissionValidator(Permission permission);
};
} // namespace detail
/// Check that the file exist and available for read
const detail::PermissionValidator ReadPermissions(detail::Permission::read);
/// Check that the file exist and available for write
const detail::PermissionValidator WritePermissions(detail::Permission::write);
/// Check that the file exist and available for write
const detail::PermissionValidator ExecPermissions(detail::Permission::exec);
#endif
#endif
// [CLI11:extra_validators_hpp:end]

View File

@@ -19,6 +19,7 @@
// [CLI11:public_includes:set]
#include <algorithm>
#include <fstream>
#include <map>
#include <string>
#include <utility>
@@ -94,7 +95,63 @@ CLI11_INLINE std::map<std::string, AsSizeValue::result_t> AsSizeValue::get_mappi
namespace detail {} // namespace detail
/// @}
// [CLI11:extra_validators_inl_hpp:end]
#if defined(CLI11_ENABLE_EXTRA_VALIDATORS) && CLI11_ENABLE_EXTRA_VALIDATORS != 0
// new extra validators
namespace detail {
#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
CLI11_INLINE PermissionValidator::PermissionValidator(Permission permission) {
std::filesystem::perms permission_code = std::filesystem::perms::none;
std::string permission_name;
switch(permission) {
case Permission::read:
permission_code = std::filesystem::perms::owner_read | std::filesystem::perms::group_read |
std::filesystem::perms::others_read;
permission_name = "read";
break;
case Permission::write:
permission_code = std::filesystem::perms::owner_write | std::filesystem::perms::group_write |
std::filesystem::perms::others_write;
permission_name = "write";
break;
case Permission::exec:
permission_code = std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec |
std::filesystem::perms::others_exec;
permission_name = "exec";
break;
case Permission::none:
default:
permission_code = std::filesystem::perms::none;
break;
}
func_ = [permission_code](std::string &path) {
std::error_code ec;
auto p = std::filesystem::path(path);
if(!std::filesystem::exists(p, ec)) {
return std::string("Path does not exist: ") + path;
}
if(ec) {
return std::string("Error checking path: ") + ec.message(); // LCOV_EXCL_LINE
}
if(permission_code == std::filesystem::perms::none) {
return std::string{};
}
auto perms = std::filesystem::status(p, ec).permissions();
if(ec) {
return std::string("Error checking path status: ") + ec.message(); // LCOV_EXCL_LINE
}
if((perms & permission_code) == std::filesystem::perms::none) {
return std::string("Path does not have required permissions: ") + path;
}
return std::string{};
};
description("Path with " + permission_name + " permission");
}
#endif
} // namespace detail
#endif
// [CLI11:extra_validators_inl_hpp:end]
} // namespace CLI
#endif

View File

@@ -640,8 +640,9 @@ CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &origi
}
if(original.size() > num_max) {
if(original.size() == 2 && num_max == 1 && original[1] == "%%" && original[0] == "{}") {
// this condition is a trap for the following empty indicator check on config files
out = original;
// this condition is a trap for the following empty indicator check on config files, it may not be used
// anymore
out = original; // LCOV_EXCL_LINE
} else {
throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size());
}

View File

@@ -19,6 +19,10 @@ endif()
if(CLI11_ENABLE_EXTRA_VALIDATORS)
target_compile_definitions(CLI11 ${PUBLIC_OR_INTERFACE} -DCLI11_ENABLE_EXTRA_VALIDATORS=1)
elseif(CLI11_DISABLE_EXTRA_VALIDATORS)
target_compile_definitions(CLI11 ${PUBLIC_OR_INTERFACE} -DCLI11_DISABLE_EXTRA_VALIDATORS=1)
elseif(DEFINED CLI11_ENABLE_EXTRA_VALIDATORS)
target_compile_definitions(CLI11 ${PUBLIC_OR_INTERFACE} -DCLI11_ENABLE_EXTRA_VALIDATORS=0)
endif()
# Allow IDE's to group targets into folders
add_library(CLI11::CLI11 ALIAS CLI11) # for add_subdirectory calls

View File

@@ -543,4 +543,110 @@ TEST_CASE_METHOD(TApp, "AsSizeValue1024", "[transform]") {
CHECK(ki_value == value);
}
#if (defined(CLI11_ENABLE_EXTRA_VALIDATORS) && CLI11_ENABLE_EXTRA_VALIDATORS == 1)
#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
#include <filesystem>
TEST_CASE_METHOD(TApp, "FileExistsForRead", "[validate]") {
std::string myfile{"TestNonFileNotUsed.txt"};
if(std::filesystem::exists(myfile)) {
std::filesystem::remove(myfile);
}
CHECK(!CLI::ReadPermissions(myfile).empty());
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
CHECK(ok);
std::string filename = "Failed";
app.add_option("--file", filename)->check(CLI::ReadPermissions);
args = {"--file", myfile};
run();
CHECK(myfile == filename);
std::filesystem::permissions(std::filesystem::path(myfile), std::filesystem::perms::owner_exec);
#if !defined(_WIN32)
// not sure how to make a file unreadable on windows in this context
CHECK_THROWS_AS(run(), CLI::ValidationError);
#endif
std::filesystem::permissions(std::filesystem::path(myfile), std::filesystem::perms::owner_write);
std::filesystem::remove(myfile);
}
TEST_CASE_METHOD(TApp, "FileExistsForWrite", "[validate]") {
std::string myfile{"TestNonFileNotUsed.txt"};
if(std::filesystem::exists(myfile)) {
std::filesystem::remove(myfile);
}
CHECK(!CLI::WritePermissions(myfile).empty());
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
CHECK(ok);
std::string filename = "Failed";
app.add_option("--file", filename)->check(CLI::WritePermissions);
args = {"--file", myfile};
run();
CHECK(myfile == filename);
std::filesystem::permissions(std::filesystem::path(myfile), std::filesystem::perms::owner_read);
CHECK_THROWS_AS(run(), CLI::ValidationError);
std::remove(myfile.c_str());
}
TEST_CASE_METHOD(TApp, "FileExistsForExec", "[validate]") {
std::string myfile{"TestNonFileNotUsed.txt"};
if(std::filesystem::exists(myfile)) {
std::filesystem::remove(myfile);
}
CHECK(!CLI::ExecPermissions(myfile).empty());
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
CHECK(ok);
std::string filename = "Failed";
app.add_option("--file", filename)->check(CLI::ExecPermissions);
args = {"--file", myfile};
std::filesystem::permissions(std::filesystem::path(myfile),
std::filesystem::perms::owner_exec | std::filesystem::perms::owner_read);
run();
CHECK(myfile == filename);
#if !defined(_WIN32)
std::filesystem::permissions(std::filesystem::path(myfile), std::filesystem::perms::owner_read);
CHECK_THROWS_AS(run(), CLI::ValidationError);
// exec permission not really a thing on windows
#endif
std::remove(myfile.c_str());
}
TEST_CASE_METHOD(TApp, "noPermissionCheck", "[validate]") {
std::string myfile{"TestNonFileNotUsed.txt"};
if(std::filesystem::exists(myfile)) {
std::filesystem::remove(myfile);
}
CHECK(!CLI::detail::PermissionValidator(CLI::detail::Permission::none)(myfile).empty());
bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
CHECK(ok);
std::string filename = "Failed";
app.add_option("--file", filename)->check(CLI::detail::PermissionValidator(CLI::detail::Permission::none));
args = {"--file", myfile};
run();
CHECK(myfile == filename);
std::remove(myfile.c_str());
}
#endif
#endif
#endif