list(FILTER): Add PREDICATE mode

This commit is contained in:
Mickaël Germain
2026-05-01 18:55:07 -07:00
parent c7af6e94d8
commit 15340d6a96
19 changed files with 312 additions and 44 deletions

View File

@@ -19,7 +19,7 @@ Synopsis
`Modification`_
list(`APPEND`_ <list> [<element>...])
list(`FILTER`_ <list> {INCLUDE | EXCLUDE} REGEX <regex>)
list(`FILTER`_ <list> <INCLUDE|EXCLUDE> <MODE>)
list(`INSERT`_ <list> <index> [<element>...])
list(`POP_BACK`_ <list> [<out-var>...])
list(`POP_FRONT`_ <list> [<out-var>...])
@@ -120,15 +120,54 @@ Modification
that empty list.
.. signature::
list(FILTER <list> <INCLUDE|EXCLUDE> REGEX <regular_expression>)
list(FILTER <list> <INCLUDE|EXCLUDE> <MODE>)
.. versionadded:: 3.6
Includes or removes items from the list that match the mode's pattern.
In ``REGEX`` mode, items will be matched against the given regular expression.
For more information on regular expressions look under
:ref:`string(REGEX) <Regex Specification>`.
``<MODE>`` must be one of the following:
``REGEX``
Items will be matched against the given regular expression.
.. code-block:: cmake
list(FILTER <list> <INCLUDE|EXCLUDE> REGEX <regular_expression>)
For more information on regular expressions look under
:ref:`string(REGEX) <Regex Specification>`.
``PREDICATE``
Specify a user-defined callable as a predicate.
.. code-block:: cmake
list(FILTER <list> <INCLUDE|EXCLUDE> PREDICATE <function>)
.. versionadded:: 4.4
``<function>`` is a user-defined :command:`function` that acts as a
unary predicate. The callable must accept exactly two parameters: the
input value and the name of an output variable. The callable must set the
output variable to a boolean value in the calling scope.
The output variable is interpreted using standard CMake boolean evaluation.
If the callable does not set the output variable, it is an error.
Example:
.. code-block:: cmake
function(file_exists path result)
if(EXISTS "${path}")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
set(candidate_files main.c missing.c utils.c)
list(FILTER candidate_files INCLUDE PREDICATE file_exists)
.. signature::
list(INSERT <list> <element_index> <element> [<element> ...])

View File

@@ -0,0 +1,10 @@
list-PREDICATE
--------------
* The :command:`list(TRANSFORM)` command gained a new ``PREDICATE`` selector
that invokes a user-defined callable to decide which elements are
transformed.
* The :command:`list(FILTER)` command gained a new ``PREDICATE`` mode
that invokes a user-defined callable to decide which elements are
included or excluded, complementing the existing ``REGEX`` mode.

View File

@@ -82,15 +82,26 @@ private:
bool const IncludeMatches;
};
// Hash of call site (FilePath:Line) for unique variable names across recursive
// calls.
std::string OutputVarFor(cm::string_view prefix, cmMakefile& makefile)
{
cmListFileContext context = makefile.GetBacktrace().Top();
std::size_t hash =
std::hash<std::string>{}(cmStrCat(context.FilePath, ":", context.Line));
return cmStrCat(prefix, hash, "_");
}
class PredicateEvaluator
{
public:
PredicateEvaluator(std::string const& functionName, cmMakefile& makefile,
PredicateEvaluator(std::string functionName, cmMakefile& makefile,
std::string errorPrefix = "sub-command TRANSFORM, "
"selector PREDICATE")
: FunctionName(functionName)
: FunctionName(std::move(functionName))
, Makefile(&makefile)
, ErrorPrefix(std::move(errorPrefix))
, OutputVar(OutputVarFor("_cmake_predicate_out_", makefile))
{
if (!makefile.GetState()->GetCommand(this->FunctionName)) {
throw cmList::transform_error(cmStrCat(this->ErrorPrefix,
@@ -101,13 +112,13 @@ public:
bool operator()(std::string const& value)
{
std::string const outputVar = "_list_predicate_out_";
this->Makefile->RemoveDefinition(outputVar);
this->Makefile->RemoveDefinition(this->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);
funcArgs.emplace_back(this->OutputVar, cmListFileArgument::Quoted,
context.Line);
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
@@ -119,7 +130,7 @@ public:
"\" failed during execution."));
}
cmValue result = this->Makefile->GetDefinition(outputVar);
cmValue result = this->Makefile->GetDefinition(this->OutputVar);
if (!result) {
throw cmList::transform_error(
cmStrCat(this->ErrorPrefix, ": function \"", this->FunctionName,
@@ -127,7 +138,7 @@ public:
}
bool boolResult = cmIsOn(*result);
this->Makefile->RemoveDefinition(outputVar);
this->Makefile->RemoveDefinition(this->OutputVar);
return boolResult;
}
@@ -135,6 +146,26 @@ private:
std::string FunctionName;
cmMakefile* Makefile = nullptr;
std::string ErrorPrefix;
std::string OutputVar;
};
class MatchesPredicate
{
public:
MatchesPredicate(PredicateEvaluator& evaluator, cmList::FilterMode mode)
: Evaluator(evaluator)
, IncludeMatches(mode == cmList::FilterMode::INCLUDE)
{
}
bool operator()(std::string const& target)
{
return this->Evaluator(target) ^ this->IncludeMatches;
}
private:
PredicateEvaluator& Evaluator;
bool IncludeMatches;
};
}
@@ -154,6 +185,23 @@ cmList& cmList::filter(cm::string_view pattern, FilterMode mode)
return *this;
}
cmList& cmList::filter(std::string const& functionName, FilterMode mode,
cmMakefile& makefile)
{
try {
PredicateEvaluator evaluator(functionName, makefile,
"sub-command FILTER, mode PREDICATE");
auto it = std::remove_if(this->Values.begin(), this->Values.end(),
MatchesPredicate{ evaluator, mode });
this->Values.erase(it, this->Values.end());
} catch (transform_error& e) {
throw std::invalid_argument(e.what());
}
return *this;
}
namespace {
class StringSorter
{
@@ -655,6 +703,7 @@ public:
TransformAction::Initialize(selector);
this->FunctionName = functionName;
this->Makefile = &makefile;
this->OutputVar = OutputVarFor("_cmake_transform_apply_out_", makefile);
// Validate: command must exist
if (!makefile.GetState()->GetCommand(this->FunctionName)) {
@@ -678,17 +727,15 @@ public:
return s;
}
// Use a unique output variable name to avoid collisions
std::string const outputVar = "_list_transform_apply_out_";
// Unset the output variable before calling
this->Makefile->RemoveDefinition(outputVar);
this->Makefile->RemoveDefinition(this->OutputVar);
// Build the function call: functionName(s, outputVar)
cmListFileContext context = this->Makefile->GetBacktrace().Top();
std::vector<cmListFileArgument> funcArgs;
funcArgs.emplace_back(s, cmListFileArgument::Quoted, context.Line);
funcArgs.emplace_back(outputVar, cmListFileArgument::Quoted, context.Line);
funcArgs.emplace_back(this->OutputVar, cmListFileArgument::Quoted,
context.Line);
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
@@ -701,7 +748,7 @@ public:
}
// Read back the output variable
cmValue result = this->Makefile->GetDefinition(outputVar);
cmValue result = this->Makefile->GetDefinition(this->OutputVar);
if (!result) {
throw transform_error(
cmStrCat("sub-command TRANSFORM, action APPLY: function \"",
@@ -713,7 +760,7 @@ public:
std::string output = *result;
// Clean up
this->Makefile->RemoveDefinition(outputVar);
this->Makefile->RemoveDefinition(this->OutputVar);
return output;
}
@@ -721,6 +768,7 @@ public:
private:
std::string FunctionName;
cmMakefile* Makefile = nullptr;
std::string OutputVar;
};
// Descriptor of action
@@ -980,16 +1028,19 @@ cmList& cmList::transform(TransformAction action, std::string const& arg,
cmMakefile& makefile,
std::unique_ptr<TransformSelector> selector)
{
auto descriptor = TransformConfigure(action, selector, 1);
// Validate action and arity via the static registry.
TransformConfigure(action, selector, 1);
auto* applyAction =
static_cast<TransformActionApply*>(descriptor->Transform.get());
applyAction->Initialize(static_cast<::TransformSelector*>(selector.get()),
arg, makefile);
// Create a local instance rather than reusing the singleton from
// Descriptors. A user function invoked by APPLY may itself call
// list(TRANSFORM ... APPLY ...), which would clobber a shared instance.
TransformActionApply applyAction;
applyAction.Initialize(static_cast<::TransformSelector*>(selector.get()),
arg, makefile);
static_cast<::TransformSelector&>(*selector).Transform(
this->Values, [&descriptor](std::string const& s) -> std::string {
return (*descriptor->Transform)(s);
this->Values, [&applyAction](std::string const& s) -> std::string {
return applyAction(s);
});
return *this;

View File

@@ -821,8 +821,11 @@ public:
EXCLUDE
};
// Includes or removes items from the list
// Throw std::invalid_argument if regular expression is invalid
// Throw std::invalid_argument if regular expression is invalid or predicate
// function is unknown / does not set its output variable
cmList& filter(cm::string_view regex, FilterMode mode);
cmList& filter(std::string const& functionName, FilterMode mode,
cmMakefile& makefile);
cmList& reverse()
{
@@ -912,6 +915,9 @@ public:
template <typename Type>
static std::unique_ptr<TransformSelector> New(std::string&&);
// NewPREDICATE is public (unlike NewAT/NewFOR/NewREGEX) because it takes
// a cmMakefile& parameter that cannot be dispatched through the existing
// New<Type>() templates.
static std::unique_ptr<TransformSelector> NewPREDICATE(
std::string const& functionName, cmMakefile& makefile);

View File

@@ -968,25 +968,46 @@ bool HandleFilterCommand(std::vector<std::string> const& args,
}
std::string const& mode = args[3];
if (mode != "REGEX") {
status.SetError("sub-command FILTER does not recognize mode " + mode);
return false;
}
if (args.size() != 5) {
status.SetError("sub-command FILTER, mode REGEX "
"requires five arguments.");
return false;
}
std::string const& pattern = args[4];
if (mode == "REGEX") {
if (args.size() != 5) {
status.SetError("sub-command FILTER, mode REGEX "
"requires five arguments.");
return false;
}
std::string const& pattern = args[4];
try {
status.GetMakefile().AddDefinition(
listName, list->filter(pattern, filterMode).to_string());
return true;
} catch (std::invalid_argument& e) {
status.SetError(e.what());
return false;
try {
status.GetMakefile().AddDefinition(
listName, list->filter(pattern, filterMode).to_string());
return true;
} catch (std::invalid_argument& e) {
status.SetError(e.what());
return false;
}
}
if (mode == "PREDICATE") {
if (args.size() != 5) {
status.SetError("sub-command FILTER, mode PREDICATE "
"requires five arguments.");
return false;
}
std::string const& functionName = args[4];
try {
status.GetMakefile().AddDefinition(
listName,
list->filter(functionName, filterMode, status.GetMakefile())
.to_string());
return true;
} catch (std::invalid_argument& e) {
status.SetError(e.what());
return false;
}
}
status.SetError("sub-command FILTER does not recognize mode " + mode);
return false;
}
} // namespace

View File

@@ -0,0 +1,2 @@
^mylist was: FILTER_THIS_BIT;DO_NOT_FILTER_THIS;thisisanitem;FILTER_THIS_THING
mylist is: DO_NOT_FILTER_THIS;thisisanitem$

View File

@@ -0,0 +1,50 @@
# Predicate: returns TRUE for items starting with "FILTER_THIS_"
function(starts_with_filter input result)
if(input MATCHES "^FILTER_THIS_")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
set(mylist FILTER_THIS_BIT DO_NOT_FILTER_THIS thisisanitem FILTER_THIS_THING)
message("mylist was: ${mylist}")
list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
message("mylist is: ${mylist}")
# EXCLUDE with macro predicate
macro(is_short input result)
string(LENGTH "${input}" _len)
if(_len LESS 6)
set(${result} TRUE)
else()
set(${result} FALSE)
endif()
endmacro()
set(mylist ab cdefgh ij klmnop qr)
list(FILTER mylist EXCLUDE PREDICATE is_short)
if(NOT mylist STREQUAL "cdefgh;klmnop")
message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE macro) is \"${mylist}\", expected \"cdefgh;klmnop\"")
endif()
# EXCLUDE on empty list
set(empty_list "")
list(FILTER empty_list EXCLUDE PREDICATE starts_with_filter)
if(NOT empty_list STREQUAL "")
message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE empty) is \"${empty_list}\", expected \"\"")
endif()
# EXCLUDE where nothing matches (all elements kept)
set(mylist alpha bravo charlie)
list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
if(NOT mylist STREQUAL "alpha;bravo;charlie")
message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE no-match) is \"${mylist}\", expected \"alpha;bravo;charlie\"")
endif()
# EXCLUDE where everything matches (all elements removed)
set(mylist FILTER_THIS_A FILTER_THIS_B FILTER_THIS_C)
list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
if(NOT mylist STREQUAL "")
message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE all-match) is \"${mylist}\", expected \"\"")
endif()

View File

@@ -0,0 +1,2 @@
^mylist was: FILTER_THIS_BIT;DO_NOT_FILTER_THIS;thisisanitem;FILTER_THIS_THING
mylist is: FILTER_THIS_BIT;FILTER_THIS_THING$

View File

@@ -0,0 +1,51 @@
# Predicate: returns TRUE for items starting with "FILTER_THIS_"
function(starts_with_filter input result)
if(input MATCHES "^FILTER_THIS_")
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 6)
set(${result} TRUE)
else()
set(${result} FALSE)
endif()
endmacro()
set(mylist FILTER_THIS_BIT DO_NOT_FILTER_THIS thisisanitem FILTER_THIS_THING)
message("mylist was: ${mylist}")
list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
message("mylist is: ${mylist}")
# INCLUDE with macro predicate
set(mylist ab cdefgh ij klmnop qr)
list(FILTER mylist INCLUDE PREDICATE is_short)
if(NOT mylist STREQUAL "ab;ij;qr")
message(FATAL_ERROR "FILTER(INCLUDE PREDICATE macro) is \"${mylist}\", expected \"ab;ij;qr\"")
endif()
# INCLUDE on empty list
set(empty_list "")
list(FILTER empty_list INCLUDE PREDICATE starts_with_filter)
if(NOT empty_list STREQUAL "")
message(FATAL_ERROR "FILTER(INCLUDE PREDICATE empty) is \"${empty_list}\", expected \"\"")
endif()
# INCLUDE where nothing matches (all elements removed)
set(mylist alpha bravo charlie)
list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
if(NOT mylist STREQUAL "")
message(FATAL_ERROR "FILTER(INCLUDE PREDICATE no-match) is \"${mylist}\", expected \"\"")
endif()
# INCLUDE where everything matches
set(mylist FILTER_THIS_A FILTER_THIS_B FILTER_THIS_C)
list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
if(NOT mylist STREQUAL "FILTER_THIS_A;FILTER_THIS_B;FILTER_THIS_C")
message(FATAL_ERROR "FILTER(INCLUDE PREDICATE all-match) is \"${mylist}\", expected \"FILTER_THIS_A;FILTER_THIS_B;FILTER_THIS_C\"")
endif()

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
^CMake Error at FILTER-PREDICATE-NoOutput\.cmake:6 \(list\):
list sub-command FILTER, mode 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(FILTER mylist INCLUDE PREDICATE bad_predicate)

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,4 @@
^CMake Error at FILTER-PREDICATE-TooManyArguments\.cmake:6 \(list\):
list sub-command FILTER, mode PREDICATE requires five arguments\.
Call Stack \(most recent call first\):
CMakeLists\.txt:3 \(include\)$

View File

@@ -0,0 +1,6 @@
function(my_predicate in out)
set(${out} TRUE PARENT_SCOPE)
endfunction()
set(mylist alpha bravo charlie)
list(FILTER mylist INCLUDE PREDICATE my_predicate extra_arg)

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
^CMake Error at FILTER-PREDICATE-UnknownFunction\.cmake:2 \(list\):
list sub-command FILTER, mode 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(FILTER mylist INCLUDE PREDICATE no_such_function)

View File

@@ -37,6 +37,11 @@ run_cmake(FILTER-REGEX-InvalidMode)
run_cmake(FILTER-REGEX-InvalidOperator)
run_cmake(FILTER-REGEX-Valid0)
run_cmake(FILTER-REGEX-Valid1)
run_cmake(FILTER-PREDICATE-UnknownFunction)
run_cmake(FILTER-PREDICATE-NoOutput)
run_cmake(FILTER-PREDICATE-TooManyArguments)
run_cmake(FILTER-PREDICATE-Include)
run_cmake(FILTER-PREDICATE-Exclude)
run_cmake(JOIN-NoArguments)
run_cmake(JOIN-NoVariable)