mirror of
https://github.com/Kitware/CMake.git
synced 2026-08-04 14:50:23 +00:00
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:
@@ -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:
|
||||
|
||||
6
Help/release/dev/list-TRANSFORM-APPLY.rst
Normal file
6
Help/release/dev/list-TRANSFORM-APPLY.rst
Normal 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.
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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\)$
|
||||
2
Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction.cmake
Normal file
2
Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction.cmake
Normal file
@@ -0,0 +1,2 @@
|
||||
set(mylist alpha bravo charlie)
|
||||
list(TRANSFORM mylist APPLY)
|
||||
1
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-result.txt
Normal file
1
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-result.txt
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
5
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-stderr.txt
Normal file
5
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-stderr.txt
Normal 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\)$
|
||||
6
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput.cmake
Normal file
6
Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput.cmake
Normal 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)
|
||||
106
Tests/RunCMake/list/TRANSFORM-APPLY.cmake
Normal file
106
Tests/RunCMake/list/TRANSFORM-APPLY.cmake
Normal 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()
|
||||
Reference in New Issue
Block a user