list(TRANSFORM): Add PREDICATE selector

Add a new `PREDICATE` selector to `list(TRANSFORM)` that uses a
user-defined function or macro to decide which elements to transform.

The predicate callable receives each element value and an output
variable name.  It must set the output variable to a truthy or falsy
value in PARENT_SCOPE; only elements for which the predicate returns
true are passed to the transform action.

Adds a shared PredicateEvaluator helper class used by both TRANSFORM
PREDICATE and (in a subsequent commit) FILTER PREDICATE.

Includes parser wiring, error and success tests covering all existing
actions (TOUPPER, TOLOWER, REPLACE, STRIP, GENEX_STRIP, APPEND,
PREPEND, APPLY) combined with the PREDICATE selector.

Issue: #27761
This commit is contained in:
Mickaël Germain
2026-04-08 14:02:22 -07:00
parent 0f30e0fe1c
commit c7af6e94d8
19 changed files with 304 additions and 1 deletions

View File

@@ -316,6 +316,39 @@ Modification
list(TRANSFORM <list> <ACTION> REGEX <regular_expression> ...)
``PREDICATE``
Specify a user-defined callable as a predicate.
Only elements for which the callable returns a true value will be
transformed.
.. code-block:: cmake
list(TRANSFORM <list> <ACTION> PREDICATE <function> ...)
.. versionadded:: 4.4
``<function>`` is a user-defined :command:`function` with exactly
two formal parameters: the input value and the name of an output
variable. The callable must set the output variable to a boolean
value. Standard CMake boolean evaluation is used.
If the callable does not set the output variable, it is an error.
Example:
.. code-block:: cmake
function(is_relative path result)
if(NOT IS_ABSOLUTE "${path}")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
set(search_paths /usr/include src lib /opt/lib)
list(TRANSFORM search_paths PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/"
PREDICATE is_relative)
Ordering
^^^^^^^^

View File

@@ -81,6 +81,61 @@ private:
cmsys::RegularExpression& Regex;
bool const IncludeMatches;
};
class PredicateEvaluator
{
public:
PredicateEvaluator(std::string const& functionName, cmMakefile& makefile,
std::string errorPrefix = "sub-command TRANSFORM, "
"selector PREDICATE")
: FunctionName(functionName)
, Makefile(&makefile)
, ErrorPrefix(std::move(errorPrefix))
{
if (!makefile.GetState()->GetCommand(this->FunctionName)) {
throw cmList::transform_error(cmStrCat(this->ErrorPrefix,
": unknown function \"",
this->FunctionName, "\"."));
}
}
bool operator()(std::string const& value)
{
std::string const outputVar = "_list_predicate_out_";
this->Makefile->RemoveDefinition(outputVar);
cmListFileContext context = this->Makefile->GetBacktrace().Top();
std::vector<cmListFileArgument> funcArgs;
funcArgs.emplace_back(value, cmListFileArgument::Quoted, context.Line);
funcArgs.emplace_back(outputVar, cmListFileArgument::Quoted, context.Line);
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
cmExecutionStatus status(*this->Makefile);
if (!this->Makefile->ExecuteCommand(func, status) ||
status.GetNestedError()) {
throw cmList::transform_error(
cmStrCat(this->ErrorPrefix, ": function \"", this->FunctionName,
"\" failed during execution."));
}
cmValue result = this->Makefile->GetDefinition(outputVar);
if (!result) {
throw cmList::transform_error(
cmStrCat(this->ErrorPrefix, ": function \"", this->FunctionName,
"\" did not set the output variable."));
}
bool boolResult = cmIsOn(*result);
this->Makefile->RemoveDefinition(outputVar);
return boolResult;
}
private:
std::string FunctionName;
cmMakefile* Makefile = nullptr;
std::string ErrorPrefix;
};
}
cmList& cmList::filter(cm::string_view pattern, FilterMode mode)
@@ -270,6 +325,26 @@ public:
cmsys::RegularExpression Regex;
};
class TransformSelectorPredicate : public TransformSelector
{
public:
TransformSelectorPredicate(std::string const& functionName,
cmMakefile& makefile)
: TransformSelector("PREDICATE")
, Evaluator(functionName, makefile)
{
}
bool Validate(std::size_t) override { return true; }
bool InSelection(std::string const& value) override
{
return this->Evaluator(value);
}
private:
PredicateEvaluator Evaluator;
};
class TransformSelectorIndexes : public TransformSelector
{
public:
@@ -827,6 +902,14 @@ std::unique_ptr<cmList::TransformSelector> cmList::TransformSelector::NewREGEX(
return std::unique_ptr<cmList::TransformSelector>(selector.release());
}
std::unique_ptr<cmList::TransformSelector>
cmList::TransformSelector::NewPREDICATE(std::string const& functionName,
cmMakefile& makefile)
{
return std::unique_ptr<cmList::TransformSelector>(
new TransformSelectorPredicate(functionName, makefile));
}
cmList& cmList::transform(TransformAction action,
std::unique_ptr<TransformSelector> selector)
{

View File

@@ -886,6 +886,7 @@ public:
struct AT;
struct FOR;
struct REGEX;
struct PREDICATE;
virtual ~TransformSelector() = default;
@@ -911,6 +912,9 @@ public:
template <typename Type>
static std::unique_ptr<TransformSelector> New(std::string&&);
static std::unique_ptr<TransformSelector> NewPREDICATE(
std::string const& functionName, cmMakefile& makefile);
cmMakefile* Makefile = nullptr;
private:

View File

@@ -520,6 +520,7 @@ bool HandleTransformCommand(std::vector<std::string> const& args,
std::string const REGEX{ "REGEX" };
std::string const AT{ "AT" };
std::string const FOR{ "FOR" };
std::string const PREDICATE{ "PREDICATE" };
std::string const OUTPUT_VARIABLE{ "OUTPUT_VARIABLE" };
std::unique_ptr<cmList::TransformSelector> selector;
std::string outputName = listName;
@@ -527,7 +528,8 @@ bool HandleTransformCommand(std::vector<std::string> const& args,
try {
// handle optional arguments
while (args.size() > index) {
if ((args[index] == REGEX || args[index] == AT || args[index] == FOR) &&
if ((args[index] == REGEX || args[index] == AT || args[index] == FOR ||
args[index] == PREDICATE) &&
selector) {
status.SetError(
cmStrCat("sub-command TRANSFORM, selector already specified (",
@@ -653,6 +655,21 @@ bool HandleTransformCommand(std::vector<std::string> const& args,
continue;
}
// PREDICATE selector
if (args[index] == PREDICATE) {
if (args.size() == ++index) {
status.SetError("sub-command TRANSFORM, selector PREDICATE expects "
"'function name' argument.");
return false;
}
selector = cmList::TransformSelector::NewPREDICATE(
args[index], status.GetMakefile());
index += 1;
continue;
}
// output variable
if (args[index] == OUTPUT_VARIABLE) {
if (args.size() == ++index) {

View File

@@ -65,6 +65,7 @@ run_cmake(TRANSFORM-REPLACE-InvalidRegex)
run_cmake(TRANSFORM-REPLACE-InvalidReplace1)
run_cmake(TRANSFORM-REPLACE-InvalidReplace2)
run_cmake(TRANSFORM-APPLY-NoFunction)
run_cmake(TRANSFORM-APPLY-UnknownFunction)
run_cmake(TRANSFORM-APPLY-NoOutput)
# 'selector' oriented tests
run_cmake(TRANSFORM-Selector-REGEX-NoArguments)
@@ -81,6 +82,9 @@ run_cmake(TRANSFORM-Selector-FOR-InvalidIndex)
run_cmake(TRANSFORM-Selector-FOR-ZeroStepArgument)
run_cmake(TRANSFORM-Selector-FOR-NegativeStepArgument)
run_cmake(TRANSFORM-Selector-FOR-BackwardsRange)
run_cmake(TRANSFORM-Selector-PREDICATE-NoArguments)
run_cmake(TRANSFORM-Selector-PREDICATE-UnknownFunction)
run_cmake(TRANSFORM-Selector-PREDICATE-NoOutput)
# 'output' oriented tests
run_cmake(TRANSFORM-Output-OUTPUT_VARIABLE-NoArguments)
run_cmake(TRANSFORM-Output-OUTPUT_VARIABLE-TooManyArguments)
@@ -93,6 +97,7 @@ run_cmake(TRANSFORM-APPEND)
run_cmake(TRANSFORM-PREPEND)
run_cmake(TRANSFORM-REPLACE)
run_cmake(TRANSFORM-APPLY)
run_cmake(TRANSFORM-PREDICATE)
run_cmake(CMP0186)
# argument tests

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
^CMake Error at TRANSFORM-APPLY-UnknownFunction\.cmake:2 \(list\):
list sub-command TRANSFORM, action APPLY: unknown function
"no_such_function"\.
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)$

View File

@@ -0,0 +1,2 @@
set(mylist alpha bravo charlie)
list(TRANSFORM mylist APPLY no_such_function)

View File

@@ -104,3 +104,28 @@ list(TRANSFORM mylist APPLY make_empty)
if(NOT mylist STREQUAL ";;")
message(FATAL_ERROR "TRANSFORM(APPLY make_empty) is \"${mylist}\", expected is \";;\"")
endif()
# Recursive APPLY: an APPLY function that itself calls list(TRANSFORM APPLY).
# The inner function returns the output variable name it was given.
function(return_out_var_name in out)
set(${out} "${out}" PARENT_SCOPE)
endfunction()
# The outer function triggers a nested APPLY and verifies that the inner
# output variable name differs from its own.
function(inner_name_is_different in out)
set(_inner x)
list(TRANSFORM _inner APPLY return_out_var_name OUTPUT_VARIABLE _inner_out)
# _inner_out now holds the inner output variable name
if("${out}" STREQUAL "${_inner_out}")
set(${out} "FALSE" PARENT_SCOPE)
else()
set(${out} "TRUE" PARENT_SCOPE)
endif()
endfunction()
set(mylist a b c)
list(TRANSFORM mylist APPLY inner_name_is_different OUTPUT_VARIABLE output)
if(NOT output STREQUAL "TRUE;TRUE;TRUE")
message(FATAL_ERROR "TRANSFORM(APPLY nested smoke) is \"${output}\", expected \"TRUE;TRUE;TRUE\"")
endif()

View File

@@ -0,0 +1,100 @@
# Predicate: returns TRUE for items starting with "b"
function(starts_with_b input result)
if(input MATCHES "^b")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
# Predicate as macro
macro(is_short input result)
string(LENGTH "${input}" _len)
if(_len LESS 4)
set(${result} TRUE)
else()
set(${result} FALSE)
endif()
endmacro()
set(mylist alpha bravo charlie bravo_two delta)
# Basic PREDICATE with TOUPPER - only items starting with "b" are uppercased
list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;BRAVO;charlie;BRAVO_TWO;delta")
message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE) is \"${output}\", expected \"alpha;BRAVO;charlie;BRAVO_TWO;delta\"")
endif()
# Verify original list unchanged (OUTPUT_VARIABLE)
if(NOT mylist STREQUAL "alpha;bravo;charlie;bravo_two;delta")
message(FATAL_ERROR "Original list modified: \"${mylist}\"")
endif()
# PREDICATE in-place
list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b)
if(NOT mylist STREQUAL "alpha;BRAVO;charlie;BRAVO_TWO;delta")
message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE in-place) is \"${mylist}\", expected \"alpha;BRAVO;charlie;BRAVO_TWO;delta\"")
endif()
# PREDICATE with macro
set(mylist ab cde fg hijklm no)
list(TRANSFORM mylist TOUPPER PREDICATE is_short OUTPUT_VARIABLE output)
if(NOT output STREQUAL "AB;CDE;FG;hijklm;NO")
message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE macro) is \"${output}\", expected \"AB;CDE;FG;hijklm;NO\"")
endif()
# PREDICATE combined with APPLY
function(add_prefix in out)
set(${out} "prefix_${in}" PARENT_SCOPE)
endfunction()
set(mylist alpha bravo charlie delta)
list(TRANSFORM mylist APPLY add_prefix PREDICATE starts_with_b OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;prefix_bravo;charlie;delta")
message(FATAL_ERROR "TRANSFORM(APPLY PREDICATE) is \"${output}\", expected \"alpha;prefix_bravo;charlie;delta\"")
endif()
# PREDICATE on empty list
set(empty_list "")
list(TRANSFORM empty_list TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
if(NOT output STREQUAL "")
message(FATAL_ERROR "TRANSFORM(PREDICATE empty) is \"${output}\", expected \"\"")
endif()
# PREDICATE where nothing matches (all elements unchanged)
set(mylist alpha charlie delta)
list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;charlie;delta")
message(FATAL_ERROR "TRANSFORM(PREDICATE no-match) is \"${output}\", expected \"alpha;charlie;delta\"")
endif()
# PREDICATE where everything matches
set(mylist bravo bronze bull)
list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
if(NOT output STREQUAL "BRAVO;BRONZE;BULL")
message(FATAL_ERROR "TRANSFORM(PREDICATE all-match) is \"${output}\", expected \"BRAVO;BRONZE;BULL\"")
endif()
# Recursive PREDICATE: a PREDICATE function that itself calls list(TRANSFORM APPLY).
# The inner function returns the output variable name it was given.
function(return_pred_var_name in out)
set(${out} "${out}" PARENT_SCOPE)
endfunction()
# The outer predicate triggers a nested APPLY and verifies that the inner
# output variable name differs from its own result variable.
function(inner_pred_name_is_different input result)
set(_inner x)
list(TRANSFORM _inner APPLY return_pred_var_name OUTPUT_VARIABLE _inner_names)
if("${result}" STREQUAL "${_inner_names}")
set(${result} FALSE PARENT_SCOPE)
else()
set(${result} TRUE PARENT_SCOPE)
endif()
endfunction()
set(mylist x y z)
list(TRANSFORM mylist TOUPPER PREDICATE inner_pred_name_is_different OUTPUT_VARIABLE output)
if(NOT output STREQUAL "X;Y;Z")
message(FATAL_ERROR "TRANSFORM(PREDICATE nested smoke) is \"${output}\", expected \"X;Y;Z\"")
endif()

View File

@@ -0,0 +1,5 @@
^CMake Error at TRANSFORM-Selector-PREDICATE-NoArguments\.cmake:2 \(list\):
list sub-command TRANSFORM, selector PREDICATE expects 'function name'
argument\.
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)$

View File

@@ -0,0 +1,2 @@
set(mylist alpha bravo charlie)
list(TRANSFORM mylist TOUPPER PREDICATE)

View File

@@ -0,0 +1,5 @@
^CMake Error at TRANSFORM-Selector-PREDICATE-NoOutput\.cmake:6 \(list\):
list sub-command TRANSFORM, selector PREDICATE: function "bad_predicate"
did not set the output variable\.
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)$

View File

@@ -0,0 +1,6 @@
function(bad_predicate in out)
# Deliberately does NOT set ${out}
endfunction()
set(mylist alpha bravo charlie)
list(TRANSFORM mylist TOUPPER PREDICATE bad_predicate)

View File

@@ -0,0 +1,5 @@
^CMake Error at TRANSFORM-Selector-PREDICATE-UnknownFunction\.cmake:2 \(list\):
list sub-command TRANSFORM, selector PREDICATE: unknown function
"no_such_function"\.
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)$

View File

@@ -0,0 +1,2 @@
set(mylist alpha bravo charlie)
list(TRANSFORM mylist TOUPPER PREDICATE no_such_function)