Add APPLY action for list(TRANSFORM)

Add a new APPLY action to list(TRANSFORM) that invokes a user-defined
function or macro for each element of the list, enabling arbitrary
per-element transformations.

The callable receives the current element value and an output variable
name, and must set the output variable in PARENT_SCOPE to the
transformed value.

Includes documentation, release notes, and tests for both function
and macro callables, OUTPUT_VARIABLE, error cases, and selector
combinations (AT, FOR, REGEX).
Refs: #27761
This commit is contained in:
Mickaël Germain
2026-04-08 14:01:37 -07:00
parent 367abd8da0
commit 651f82642c
13 changed files with 291 additions and 5 deletions

View File

@@ -203,8 +203,7 @@ For more information on regular expressions look under
the other ones will remain the same as before the transformation.
``<ACTION>`` specifies the action to apply to the elements of the list.
The actions have exactly the same semantics as sub-commands of the
:command:`string` command. ``<ACTION>`` must be one of the following:
``<ACTION>`` must be one of the following:
:command:`APPEND <string(APPEND)>`, :command:`PREPEND <string(PREPEND)>`
Append, prepend specified value to each element of the list.
@@ -251,6 +250,45 @@ For more information on regular expressions look under
element instead of the beginning of each repeated search.
See policy :policy:`CMP0186`.
``APPLY``
Invoke a user-defined callable for each element of the list.
The callable must accept exactly two parameters: the input value and the
name of an output variable. The callable must set the output variable
in the calling scope.
.. signature::
list(TRANSFORM <list> APPLY <function> ...)
:target: TRANSFORM_APPLY
.. versionadded:: 4.4
``<function>`` is a :command:`function` with exactly two formal parameters.
Set the output variable via
:command:`set(\<variable\> \<value\> PARENT_SCOPE) <set>`:
.. code-block:: cmake
function(<function> <input> <output>)
# Transform <input>, store result in ${<output>}
set(${<output>} "<result>" PARENT_SCOPE)
endfunction()
Before each invocation, the output variable is unset in the calling scope
to prevent stale values.
Example:
.. code-block:: cmake
function(make_absolute in out)
cmake_path(ABSOLUTE_PATH in BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
set(${out} "${in}" PARENT_SCOPE)
endfunction()
set(mylist main.c utils.c io.c)
list(TRANSFORM mylist APPLY make_absolute)
# mylist is now absolute paths relative to CMAKE_CURRENT_SOURCE_DIR
``<SELECTOR>`` determines which elements of the list will be transformed.
Only one type of selector can be specified at a time.
When given, ``<SELECTOR>`` must be one of the following:

View File

@@ -0,0 +1,6 @@
list-TRANSFORM-APPLY
--------------------
* The :command:`list(TRANSFORM)` command gained a new ``APPLY`` action that
invokes a user-defined :command:`function` for each element of the list,
enabling arbitrary per-element transformations.

View File

@@ -18,12 +18,16 @@
#include "cmsys/RegularExpression.hxx"
#include "cmAlgorithms.h"
#include "cmExecutionStatus.h"
#include "cmGeneratorExpression.h"
#include "cmListFileCache.h"
#include "cmMakefile.h"
#include "cmRange.h"
#include "cmState.h"
#include "cmStringAlgorithms.h"
#include "cmStringReplaceHelper.h"
#include "cmSystemTools.h"
#include "cmValue.h"
cm::string_view cmList::element_separator{ ";" };
@@ -565,6 +569,85 @@ private:
std::unique_ptr<cmStringReplaceHelper> ReplaceHelper;
};
class TransformActionApply : public TransformAction
{
public:
using TransformAction::Initialize;
void Initialize(TransformSelector* selector, std::string const& functionName,
cmMakefile& makefile)
{
TransformAction::Initialize(selector);
this->FunctionName = functionName;
this->Makefile = &makefile;
// Validate: command must exist
if (!makefile.GetState()->GetCommand(this->FunctionName)) {
throw transform_error(
cmStrCat("sub-command TRANSFORM, action APPLY: unknown function \"",
this->FunctionName, "\"."));
}
}
void Initialize(TransformSelector* /*selector*/,
std::vector<std::string> const& /*args*/) override
{
// This overload must not be used for APPLY — it lacks cmMakefile context.
throw transform_error(
"sub-command TRANSFORM, action APPLY requires cmMakefile context.");
}
std::string operator()(std::string const& s) override
{
if (!this->Selector->InSelection(s)) {
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);
// 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);
cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
std::move(funcArgs) };
cmExecutionStatus status(*this->Makefile);
if (!this->Makefile->ExecuteCommand(func, status) ||
status.GetNestedError()) {
throw transform_error(
cmStrCat("sub-command TRANSFORM, action APPLY: function \"",
this->FunctionName, "\" failed during execution."));
}
// Read back the output variable
cmValue result = this->Makefile->GetDefinition(outputVar);
if (!result) {
throw transform_error(
cmStrCat("sub-command TRANSFORM, action APPLY: function \"",
this->FunctionName, "\" did not set the output variable."));
}
// Copy the result before cleaning up (RemoveDefinition invalidates the
// cmValue pointer).
std::string output = *result;
// Clean up
this->Makefile->RemoveDefinition(outputVar);
return output;
}
private:
std::string FunctionName;
cmMakefile* Makefile = nullptr;
};
// Descriptor of action
// Arity: number of arguments required for the action
// Transform: Object implementing the action
@@ -621,6 +704,8 @@ ActionDescriptorSet::iterator TransformConfigure(
cm::make_unique<TransformActionGenexStrip>());
Descriptors.emplace(cmList::TransformAction::REPLACE, "REPLACE", 2,
cm::make_unique<TransformActionReplace>());
Descriptors.emplace(cmList::TransformAction::APPLY, "APPLY", 1,
cm::make_unique<TransformActionApply>());
}
auto descriptor = Descriptors.find(action);
@@ -808,6 +893,25 @@ cmList& cmList::transform(TransformAction action,
return *this;
}
cmList& cmList::transform(TransformAction action, std::string const& arg,
cmMakefile& makefile,
std::unique_ptr<TransformSelector> selector)
{
auto descriptor = TransformConfigure(action, selector, 1);
auto* applyAction =
static_cast<TransformActionApply*>(descriptor->Transform.get());
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);
});
return *this;
}
std::string& cmList::append(std::string& list, std::string&& value)
{
if (list.empty()) {

View File

@@ -941,7 +941,8 @@ public:
TOUPPER,
STRIP,
GENEX_STRIP,
REPLACE
REPLACE,
APPLY
};
// Transforms the list by applying an action
@@ -956,6 +957,9 @@ public:
cmList& transform(TransformAction action,
std::vector<std::string> const& args,
std::unique_ptr<TransformSelector> = {});
cmList& transform(TransformAction action, std::string const& arg,
cmMakefile& makefile,
std::unique_ptr<TransformSelector> = {});
std::string join(cm::string_view glue) const
{

View File

@@ -481,7 +481,8 @@ bool HandleTransformCommand(std::vector<std::string> const& args,
{ "TOLOWER", cmList::TransformAction::TOLOWER, 0 },
{ "STRIP", cmList::TransformAction::STRIP, 0 },
{ "GENEX_STRIP", cmList::TransformAction::GENEX_STRIP, 0 },
{ "REPLACE", cmList::TransformAction::REPLACE, 2 } },
{ "REPLACE", cmList::TransformAction::REPLACE, 2 },
{ "APPLY", cmList::TransformAction::APPLY, 1 } },
[](std::string const& x, std::string const& y) {
return x < y;
} };
@@ -683,7 +684,12 @@ bool HandleTransformCommand(std::vector<std::string> const& args,
}
selector->Makefile = &status.GetMakefile();
list->transform(descriptor->Action, arguments, std::move(selector));
if (descriptor->Action == cmList::TransformAction::APPLY) {
list->transform(descriptor->Action, arguments.front(),
status.GetMakefile(), std::move(selector));
} else {
list->transform(descriptor->Action, arguments, std::move(selector));
}
status.GetMakefile().AddDefinition(outputName, list->to_string());
return true;
} catch (cmList::transform_error& e) {

View File

@@ -64,6 +64,8 @@ run_cmake(TRANSFORM-REPLACE-TooManyArguments)
run_cmake(TRANSFORM-REPLACE-InvalidRegex)
run_cmake(TRANSFORM-REPLACE-InvalidReplace1)
run_cmake(TRANSFORM-REPLACE-InvalidReplace2)
run_cmake(TRANSFORM-APPLY-NoFunction)
run_cmake(TRANSFORM-APPLY-NoOutput)
# 'selector' oriented tests
run_cmake(TRANSFORM-Selector-REGEX-NoArguments)
run_cmake(TRANSFORM-Selector-REGEX-TooManyArguments)
@@ -90,6 +92,7 @@ run_cmake(TRANSFORM-GENEX_STRIP)
run_cmake(TRANSFORM-APPEND)
run_cmake(TRANSFORM-PREPEND)
run_cmake(TRANSFORM-REPLACE)
run_cmake(TRANSFORM-APPLY)
run_cmake(CMP0186)
# argument tests

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,4 @@
^CMake Error at TRANSFORM-APPLY-NoFunction\.cmake:2 \(list\):
list sub-command TRANSFORM, action APPLY expects 1 argument\(s\)\.
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)

View File

@@ -0,0 +1 @@
1

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
# Define a transform function: add source prefix
function(add_src_prefix in out)
set(${out} "src/${in}" PARENT_SCOPE)
endfunction()
# Define a transform function: add prefix
function(add_prefix in out)
set(${out} "prefix_${in}" PARENT_SCOPE)
endfunction()
# Define a transform macro: wrap in angle brackets
macro(wrap_angles in out)
set(${out} "<${in}>")
endmacro()
set(mylist alpha bravo charlie delta)
# Basic APPLY - all elements
list(TRANSFORM mylist APPLY add_src_prefix OUTPUT_VARIABLE output)
if(NOT output STREQUAL "src/alpha;src/bravo;src/charlie;src/delta")
message(FATAL_ERROR "TRANSFORM(APPLY) is \"${output}\", expected is \"src/alpha;src/bravo;src/charlie;src/delta\"")
endif()
# APPLY with OUTPUT_VARIABLE (verify original unchanged)
if(NOT mylist STREQUAL "alpha;bravo;charlie;delta")
message(FATAL_ERROR "Original list modified: \"${mylist}\", expected \"alpha;bravo;charlie;delta\"")
endif()
# APPLY in-place
list(TRANSFORM mylist APPLY add_src_prefix)
if(NOT mylist STREQUAL "src/alpha;src/bravo;src/charlie;src/delta")
message(FATAL_ERROR "TRANSFORM(APPLY) in-place is \"${mylist}\", expected is \"src/alpha;src/bravo;src/charlie;src/delta\"")
endif()
# APPLY with AT selector
set(mylist alpha bravo charlie delta)
list(TRANSFORM mylist APPLY add_src_prefix AT 1 3 OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;src/bravo;charlie;src/delta")
message(FATAL_ERROR "TRANSFORM(APPLY AT) is \"${output}\", expected is \"alpha;src/bravo;charlie;src/delta\"")
endif()
# APPLY with AT selector and negative index
unset(output)
list(TRANSFORM mylist APPLY add_src_prefix AT 1 -2 OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;src/bravo;src/charlie;delta")
message(FATAL_ERROR "TRANSFORM(APPLY AT neg) is \"${output}\", expected is \"alpha;src/bravo;src/charlie;delta\"")
endif()
# APPLY with FOR selector
unset(output)
list(TRANSFORM mylist APPLY add_src_prefix FOR 1 2 OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;src/bravo;src/charlie;delta")
message(FATAL_ERROR "TRANSFORM(APPLY FOR) is \"${output}\", expected is \"alpha;src/bravo;src/charlie;delta\"")
endif()
# APPLY with FOR selector and step
unset(output)
list(TRANSFORM mylist APPLY add_src_prefix FOR 0 3 2 OUTPUT_VARIABLE output)
if(NOT output STREQUAL "src/alpha;bravo;src/charlie;delta")
message(FATAL_ERROR "TRANSFORM(APPLY FOR step) is \"${output}\", expected is \"src/alpha;bravo;src/charlie;delta\"")
endif()
# APPLY with REGEX selector
unset(output)
list(TRANSFORM mylist APPLY add_src_prefix REGEX "(r|t)a" OUTPUT_VARIABLE output)
if(NOT output STREQUAL "alpha;src/bravo;charlie;src/delta")
message(FATAL_ERROR "TRANSFORM(APPLY REGEX) is \"${output}\", expected is \"alpha;src/bravo;charlie;src/delta\"")
endif()
# APPLY with a different function
unset(output)
list(TRANSFORM mylist APPLY add_prefix OUTPUT_VARIABLE output)
if(NOT output STREQUAL "prefix_alpha;prefix_bravo;prefix_charlie;prefix_delta")
message(FATAL_ERROR "TRANSFORM(APPLY add_prefix) is \"${output}\", expected is \"prefix_alpha;prefix_bravo;prefix_charlie;prefix_delta\"")
endif()
# APPLY with a macro
set(mylist alpha bravo charlie)
list(TRANSFORM mylist APPLY wrap_angles OUTPUT_VARIABLE output)
if(NOT output STREQUAL "<alpha>;<bravo>;<charlie>")
message(FATAL_ERROR "TRANSFORM(APPLY macro) is \"${output}\", expected is \"<alpha>;<bravo>;<charlie>\"")
endif()
# APPLY with macro and selector
list(TRANSFORM mylist APPLY wrap_angles AT 0 2 OUTPUT_VARIABLE output)
if(NOT output STREQUAL "<alpha>;bravo;<charlie>")
message(FATAL_ERROR "TRANSFORM(APPLY macro AT) is \"${output}\", expected is \"<alpha>;bravo;<charlie>\"")
endif()
# APPLY on empty list
set(empty_list "")
list(TRANSFORM empty_list APPLY add_src_prefix OUTPUT_VARIABLE output)
if(NOT output STREQUAL "")
message(FATAL_ERROR "TRANSFORM(APPLY empty) is \"${output}\", expected is \"\"")
endif()
# APPLY with function that returns empty string
function(make_empty in out)
set(${out} "" PARENT_SCOPE)
endfunction()
set(mylist alpha bravo charlie)
list(TRANSFORM mylist APPLY make_empty)
if(NOT mylist STREQUAL ";;")
message(FATAL_ERROR "TRANSFORM(APPLY make_empty) is \"${mylist}\", expected is \";;\"")
endif()