diff --git a/Help/command/list.rst b/Help/command/list.rst index abae8d2009..88516f0e90 100644 --- a/Help/command/list.rst +++ b/Help/command/list.rst @@ -203,8 +203,7 @@ For more information on regular expressions look under the other ones will remain the same as before the transformation. ```` 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. ```` must be one of the following: + ```` must be one of the following: :command:`APPEND `, :command:`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 APPLY ...) + :target: TRANSFORM_APPLY + + .. versionadded:: 4.4 + + ```` is a :command:`function` with exactly two formal parameters. + Set the output variable via + :command:`set(\ \ PARENT_SCOPE) `: + + .. code-block:: cmake + + function( ) + # Transform , store result in ${} + set(${} "" 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 + ```` determines which elements of the list will be transformed. Only one type of selector can be specified at a time. When given, ```` must be one of the following: diff --git a/Help/release/dev/list-TRANSFORM-APPLY.rst b/Help/release/dev/list-TRANSFORM-APPLY.rst new file mode 100644 index 0000000000..8807669e84 --- /dev/null +++ b/Help/release/dev/list-TRANSFORM-APPLY.rst @@ -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. diff --git a/Source/cmList.cxx b/Source/cmList.cxx index af1d89afc5..1c76664d4e 100644 --- a/Source/cmList.cxx +++ b/Source/cmList.cxx @@ -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 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 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 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()); Descriptors.emplace(cmList::TransformAction::REPLACE, "REPLACE", 2, cm::make_unique()); + Descriptors.emplace(cmList::TransformAction::APPLY, "APPLY", 1, + cm::make_unique()); } 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 selector) +{ + auto descriptor = TransformConfigure(action, selector, 1); + + auto* applyAction = + static_cast(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()) { diff --git a/Source/cmList.h b/Source/cmList.h index 74a3420611..febc2c5ffc 100644 --- a/Source/cmList.h +++ b/Source/cmList.h @@ -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 const& args, std::unique_ptr = {}); + cmList& transform(TransformAction action, std::string const& arg, + cmMakefile& makefile, + std::unique_ptr = {}); std::string join(cm::string_view glue) const { diff --git a/Source/cmListCommand.cxx b/Source/cmListCommand.cxx index 60bd08304c..fb202b37fb 100644 --- a/Source/cmListCommand.cxx +++ b/Source/cmListCommand.cxx @@ -481,7 +481,8 @@ bool HandleTransformCommand(std::vector 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 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) { diff --git a/Tests/RunCMake/list/RunCMakeTest.cmake b/Tests/RunCMake/list/RunCMakeTest.cmake index e428cd008b..dd4264cb6a 100644 --- a/Tests/RunCMake/list/RunCMakeTest.cmake +++ b/Tests/RunCMake/list/RunCMakeTest.cmake @@ -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 diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-result.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-stderr.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-stderr.txt new file mode 100644 index 0000000000..0e3ca8535e --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction-stderr.txt @@ -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\)$ diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction.cmake b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction.cmake new file mode 100644 index 0000000000..4e6ca060d9 --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoFunction.cmake @@ -0,0 +1,2 @@ +set(mylist alpha bravo charlie) +list(TRANSFORM mylist APPLY) diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-result.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-stderr.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-stderr.txt new file mode 100644 index 0000000000..8c31e5c284 --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput-stderr.txt @@ -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\)$ diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput.cmake b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput.cmake new file mode 100644 index 0000000000..b8d02ec343 --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY-NoOutput.cmake @@ -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) diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY.cmake b/Tests/RunCMake/list/TRANSFORM-APPLY.cmake new file mode 100644 index 0000000000..14fe360220 --- /dev/null +++ b/Tests/RunCMake/list/TRANSFORM-APPLY.cmake @@ -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 ";;") + message(FATAL_ERROR "TRANSFORM(APPLY macro) is \"${output}\", expected is \";;\"") +endif() + +# APPLY with macro and selector +list(TRANSFORM mylist APPLY wrap_angles AT 0 2 OUTPUT_VARIABLE output) +if(NOT output STREQUAL ";bravo;") + message(FATAL_ERROR "TRANSFORM(APPLY macro AT) is \"${output}\", expected is \";bravo;\"") +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()