From f3e51a2b1d1742a21f18717a361338149954728c Mon Sep 17 00:00:00 2001 From: Alex Turbov Date: Sat, 9 Nov 2019 17:42:08 +0200 Subject: foreach: Introduce `IN ZIP_LISTS` mode --- Help/command/foreach.rst | 28 +++ Help/release/dev/foreach-ZIP_LISTS.rst | 5 + Source/cmForEachCommand.cxx | 198 ++++++++++++++++++--- Tests/RunCMake/foreach/RunCMakeTest.cmake | 5 + ...oreach-ITEMS-with-ZIP_LISTS-mix-test-result.txt | 1 + ...oreach-ITEMS-with-ZIP_LISTS-mix-test-stderr.txt | 4 + .../foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake | 2 + ...oreach-LISTS-with-ZIP_LISTS-mix-test-result.txt | 1 + ...oreach-LISTS-with-ZIP_LISTS-mix-test-stderr.txt | 4 + .../foreach-LISTS-with-ZIP_LISTS-mix-test.cmake | 2 + .../foreach/foreach-ZIP_LISTS-test-stdout.txt | 19 ++ .../RunCMake/foreach/foreach-ZIP_LISTS-test.cmake | 68 +++++++ ...oreach-ZIP_LISTS-with-ITEMS-mix-test-result.txt | 1 + ...oreach-ZIP_LISTS-with-ITEMS-mix-test-stderr.txt | 4 + .../foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake | 2 + ...oreach-ZIP_LISTS-with-LISTS-mix-test-result.txt | 1 + ...oreach-ZIP_LISTS-with-LISTS-mix-test-stderr.txt | 4 + .../foreach-ZIP_LISTS-with-LISTS-mix-test.cmake | 2 + 18 files changed, 324 insertions(+), 27 deletions(-) create mode 100644 Help/release/dev/foreach-ZIP_LISTS.rst create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-test-stdout.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test.cmake diff --git a/Help/command/foreach.rst b/Help/command/foreach.rst index ecbfed3..dbd2fac 100644 --- a/Help/command/foreach.rst +++ b/Help/command/foreach.rst @@ -82,3 +82,31 @@ yields -- X=6 -- X=7 -- X=8 + + +.. code-block:: cmake + + foreach( IN ZIP_LISTS ) + +In this variant, ```` is a whitespace or semicolon +separated list of list-valued variables. The ``foreach`` +command iterates over each list simultaneously and set the +``loop_var_N`` variables to the current item from the +corresponding list. If some list is shorter than others, +the corresponding iteration variable is not defined. + +.. code-block:: cmake + + list(APPEND English one two three four) + list(APPEND Bahasa satu dua tiga) + foreach(X IN ZIP_LISTS English Bahasa) + message(STATUS "X_0=${X_0}, X_1=${X_1}") + endforeach() + +yields +:: + + -- X_0=one, X_1=satu + -- X_0=two, X_1=dua + -- X_0=three, X_1=tiga + -- X_0=four, X_1= diff --git a/Help/release/dev/foreach-ZIP_LISTS.rst b/Help/release/dev/foreach-ZIP_LISTS.rst new file mode 100644 index 0000000..d45d9b9 --- /dev/null +++ b/Help/release/dev/foreach-ZIP_LISTS.rst @@ -0,0 +1,5 @@ +foreach-ZIP_LISTS +----------------- + +* The :command:`foreach` learned a new option ``ZIP_LISTS`` to iterate + over multiple lists simultaneously. diff --git a/Source/cmForEachCommand.cxx b/Source/cmForEachCommand.cxx index 91cd4ef..163bb9a 100644 --- a/Source/cmForEachCommand.cxx +++ b/Source/cmForEachCommand.cxx @@ -9,6 +9,8 @@ // ALERT But IWYU used to lint `#include`s do not "understand" // conditional compilation (i.e. `#if __cplusplus >= 201703L`) #include +#include +#include #include #include @@ -29,7 +31,7 @@ namespace { class cmForEachFunctionBlocker : public cmFunctionBlocker { public: - cmForEachFunctionBlocker(cmMakefile* mf); + explicit cmForEachFunctionBlocker(cmMakefile* mf); ~cmForEachFunctionBlocker() override; cm::string_view StartCommandName() const override { return "foreach"_s; } @@ -41,10 +43,28 @@ public: bool Replay(std::vector functions, cmExecutionStatus& inStatus) override; + void SetZipLists() { this->ZipLists = true; } + std::vector Args; private: + struct InvokeResult + { + bool Restore; + bool Break; + }; + + bool ReplayItems(std::vector const& functions, + cmExecutionStatus& inStatus); + + bool ReplayZipLists(std::vector const& functions, + cmExecutionStatus& inStatus); + + InvokeResult invoke(std::vector const& functions, + cmExecutionStatus& inStatus, cmMakefile& mf); + cmMakefile* Makefile; + bool ZipLists = false; }; cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf) @@ -70,46 +90,146 @@ bool cmForEachFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff, bool cmForEachFunctionBlocker::Replay( std::vector functions, cmExecutionStatus& inStatus) { - cmMakefile& mf = inStatus.GetMakefile(); - // at end of for each execute recorded commands + return this->ZipLists ? this->ReplayZipLists(functions, inStatus) + : this->ReplayItems(functions, inStatus); +} + +bool cmForEachFunctionBlocker::ReplayItems( + std::vector const& functions, + cmExecutionStatus& inStatus) +{ + auto& mf = inStatus.GetMakefile(); + + // At end of for each execute recorded commands // store the old value std::string oldDef; if (mf.GetDefinition(this->Args.front())) { oldDef = mf.GetDefinition(this->Args.front()); } + auto restore = false; for (std::string const& arg : cmMakeRange(this->Args).advance(1)) { - // set the variable to the loop value + // Set the variable to the loop value mf.AddDefinition(this->Args.front(), arg); // Invoke all the functions that were collected in the block. - for (cmListFileFunction const& func : functions) { - cmExecutionStatus status(mf); - mf.ExecuteCommand(func, status); - if (status.GetReturnInvoked()) { - inStatus.SetReturnInvoked(); - // restore the variable to its prior value - mf.AddDefinition(this->Args.front(), oldDef); - return true; - } - if (status.GetBreakInvoked()) { - // restore the variable to its prior value - mf.AddDefinition(this->Args.front(), oldDef); - return true; - } - if (status.GetContinueInvoked()) { - break; - } - if (cmSystemTools::GetFatalErrorOccured()) { - return true; + auto r = this->invoke(functions, inStatus, mf); + restore = r.Restore; + if (r.Break) { + break; + } + } + + if (restore) { + // restore the variable to its prior value + mf.AddDefinition(this->Args.front(), oldDef); + } + return true; +} + +bool cmForEachFunctionBlocker::ReplayZipLists( + std::vector const& functions, + cmExecutionStatus& inStatus) +{ + auto& mf = inStatus.GetMakefile(); + + // Expand the list of list-variables into a list of lists of strings + std::vector> values; + values.reserve(this->Args.size() - 1); + // Also track the longest list size + std::size_t max_items = 0u; + for (auto const& var : cmMakeRange(this->Args).advance(1)) { + std::vector items; + auto const& value = mf.GetSafeDefinition(var); + if (!value.empty()) { + cmExpandList(value, items, true); + } + max_items = std::max(max_items, items.size()); + values.emplace_back(std::move(items)); + } + + // Store old values for iteration variables + std::map oldDefs; + // Also, form the vector of iteration variable names + std::vector iteration_vars; + iteration_vars.reserve(values.size()); + const auto iter_var_prefix = this->Args.front(); + for (auto i = 0u; i < values.size(); ++i) { + auto iter_var_name = iter_var_prefix + "_" + std::to_string(i); + if (mf.GetDefinition(iter_var_name)) { + oldDefs.emplace(iter_var_name, mf.GetDefinition(iter_var_name)); + } + iteration_vars.emplace_back(std::move(iter_var_name)); + } + + // Form a vector of current positions in all lists (Ok, vectors) of values + std::vector positions; + positions.reserve(values.size()); + std::transform( + values.begin(), values.end(), std::back_inserter(positions), + // Set the initial position to the beginning of every list + [](decltype(values)::value_type& list) { return list.begin(); }); + + auto restore = false; + // Iterate over all the lists simulateneously + for (auto i = 0u; i < max_items; ++i) { + // Declare iteration variables + for (auto j = 0u; j < values.size(); ++j) { + // Define (or not) the iteration variable if the current position + // still not at the end... + if (positions[j] != values[j].end()) { + mf.AddDefinition(iteration_vars[j], *positions[j]); + ++positions[j]; + } else { + mf.RemoveDefinition(iteration_vars[j]); } } + // Invoke all the functions that were collected in the block. + auto r = this->invoke(functions, inStatus, mf); + restore = r.Restore; + if (r.Break) { + break; + } } - // restore the variable to its prior value - mf.AddDefinition(this->Args.front(), oldDef); + // Restore the variables to its prior value + if (restore) { + for (auto const& p : oldDefs) { + mf.AddDefinition(p.first, p.second); + } + } return true; } +auto cmForEachFunctionBlocker::invoke( + std::vector const& functions, + cmExecutionStatus& inStatus, cmMakefile& mf) -> InvokeResult +{ + InvokeResult result = { true, false }; + // Invoke all the functions that were collected in the block. + for (cmListFileFunction const& func : functions) { + cmExecutionStatus status(mf); + mf.ExecuteCommand(func, status); + if (status.GetReturnInvoked()) { + inStatus.SetReturnInvoked(); + result.Break = true; + break; + } + if (status.GetBreakInvoked()) { + result.Break = true; + break; + } + if (status.GetContinueInvoked()) { + break; + } + if (cmSystemTools::GetFatalErrorOccured()) { + result.Restore = false; + result.Break = true; + break; + } + } + return result; +} + bool HandleInMode(std::vector const& args, cmMakefile& makefile) { auto fb = cm::make_unique(&makefile); @@ -119,21 +239,45 @@ bool HandleInMode(std::vector const& args, cmMakefile& makefile) { DoingNone, DoingLists, - DoingItems + DoingItems, + DoingZipLists }; Doing doing = DoingNone; for (std::string const& arg : cmMakeRange(args).advance(2)) { if (arg == "LISTS") { + if (doing == DoingZipLists) { + makefile.IssueMessage(MessageType::FATAL_ERROR, + "ZIP_LISTS can not be used with LISTS or ITEMS"); + return true; + } doing = DoingLists; + } else if (arg == "ITEMS") { + if (doing == DoingZipLists) { + makefile.IssueMessage(MessageType::FATAL_ERROR, + "ZIP_LISTS can not be used with LISTS or ITEMS"); + return true; + } doing = DoingItems; + + } else if (arg == "ZIP_LISTS") { + if (doing != DoingNone) { + makefile.IssueMessage(MessageType::FATAL_ERROR, + "ZIP_LISTS can not be used with LISTS or ITEMS"); + return true; + } + doing = DoingZipLists; + fb->SetZipLists(); + } else if (doing == DoingLists) { auto const& value = makefile.GetSafeDefinition(arg); if (!value.empty()) { cmExpandList(value, fb->Args, true); } - } else if (doing == DoingItems) { + + } else if (doing == DoingItems || doing == DoingZipLists) { fb->Args.push_back(arg); + } else { makefile.IssueMessage(MessageType::FATAL_ERROR, cmStrCat("Unknown argument:\n", " ", arg, "\n")); diff --git a/Tests/RunCMake/foreach/RunCMakeTest.cmake b/Tests/RunCMake/foreach/RunCMakeTest.cmake index 0f1fdd4..371a0cb 100644 --- a/Tests/RunCMake/foreach/RunCMakeTest.cmake +++ b/Tests/RunCMake/foreach/RunCMakeTest.cmake @@ -2,3 +2,8 @@ include(RunCMake) run_cmake(BadRangeInFunction) run_cmake(foreach-all-test) +run_cmake(foreach-ZIP_LISTS-test) +run_cmake(foreach-ITEMS-with-ZIP_LISTS-mix-test) +run_cmake(foreach-LISTS-with-ZIP_LISTS-mix-test) +run_cmake(foreach-ZIP_LISTS-with-ITEMS-mix-test) +run_cmake(foreach-ZIP_LISTS-with-LISTS-mix-test) diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-result.txt b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-stderr.txt b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-stderr.txt new file mode 100644 index 0000000..f7d5ae7 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake:1 \(foreach\): + ZIP_LISTS can not be used with LISTS or ITEMS +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake new file mode 100644 index 0000000..28099a0 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-with-ZIP_LISTS-mix-test.cmake @@ -0,0 +1,2 @@ +foreach(i IN ITEMS one two three ZIP_LISTS blah) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-result.txt b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-stderr.txt b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-stderr.txt new file mode 100644 index 0000000..42f8d1e --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-LISTS-with-ZIP_LISTS-mix-test.cmake:1 \(foreach\): + ZIP_LISTS can not be used with LISTS or ITEMS +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test.cmake b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test.cmake new file mode 100644 index 0000000..8a919dd --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-with-ZIP_LISTS-mix-test.cmake @@ -0,0 +1,2 @@ +foreach(i IN LISTS one two three ZIP_LISTS blah) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test-stdout.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test-stdout.txt new file mode 100644 index 0000000..25433fd --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test-stdout.txt @@ -0,0 +1,19 @@ +-- foreach\(IN ZIP_LISTS\): +-- <<< empty lists case >>> +-- Begin output +-- --> empty-output <-- +-- End output +-- <<< same lengths lists case >>> +-- Begin output +-- | one, satu, raz +-- | two, dua, dva +-- | three, tiga, tri +-- End output +-- <<< different lengths lists case >>> +-- Begin output +-- | one, satu, raz +-- | two, dua, dva +-- | three, tiga, tri +-- | \[undefiend\], empat, \[undefiend\] +-- End output +-- <<< test variable value restored -- PASSED >>> diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test.cmake new file mode 100644 index 0000000..56cfe64 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-test.cmake @@ -0,0 +1,68 @@ +function(foreachTest result list_var_1 list_var_2 list_var_3) + set(_options MUTE) + set(_one_value_args) + set(_multi_value_args) + cmake_parse_arguments(PARSE_ARGV 3 _arg "${_options}" "${_one_value_args}" "${_multi_value_args}") + + set(_has_any_output FALSE) + list(APPEND CMAKE_MESSAGE_INDENT "| ") + foreach(num IN ZIP_LISTS ${list_var_1} ${list_var_2} ${list_var_3}) + foreach(i RANGE 2) + if(NOT num_${i}) + set(num_${i} "[undefiend]") + endif() + endforeach() + if(NOT _arg_MUTE) + message(STATUS "${num_0}, ${num_1}, ${num_2}") + endif() + set(_has_any_output TRUE) + endforeach() + set(${result} ${_has_any_output} PARENT_SCOPE) +endfunction() + +function(foreachTestDecorated list_var_1 list_var_2 list_var_3) + list(APPEND CMAKE_MESSAGE_INDENT " ") + message(STATUS "Begin output") + foreachTest(_has_any_output ${list_var_1} ${list_var_2} ${list_var_3}) + if(NOT _has_any_output) + message(STATUS "--> empty-output <--") + endif() + message(STATUS "End output") +endfunction() + +message(STATUS "foreach(IN ZIP_LISTS):") +list(APPEND CMAKE_MESSAGE_INDENT " ") + +set(english) +set(bahasa) +set(russian) + +message(STATUS "<<< empty lists case >>>") +foreachTestDecorated(english bahasa russian) + +list(APPEND english one two three) +list(APPEND bahasa satu dua tiga) +list(APPEND russian raz dva tri) + +message(STATUS "<<< same lengths lists case >>>") +foreachTestDecorated(english bahasa russian) + +list(APPEND bahasa empat) + +message(STATUS "<<< different lengths lists case >>>") +foreachTestDecorated(english bahasa russian) + +set(num_0 "old-0") +set(num_1 "old-1") +set(num_2 "old-2") +foreachTest(_ english bahasa russian MUTE) +set(check PASSED) +foreach(i RANGE 2) + if(NOT "${num_${i}}" STREQUAL "old-${i}") + message(SEND_ERROR "num_${i} value is corrupted") + set(check FAILED) + endif() +endforeach() +message(STATUS "<<< test variable value restored -- ${check} >>>") + +list(POP_BACK CMAKE_MESSAGE_INDENT) diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-result.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-stderr.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-stderr.txt new file mode 100644 index 0000000..0dcab01 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake:1 \(foreach\): + ZIP_LISTS can not be used with LISTS or ITEMS +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake new file mode 100644 index 0000000..71ed842 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-ITEMS-mix-test.cmake @@ -0,0 +1,2 @@ +foreach(i IN ZIP_LISTS blah ITEMS blah) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-result.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-stderr.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-stderr.txt new file mode 100644 index 0000000..a6b6e9c --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ZIP_LISTS-with-LISTS-mix-test.cmake:1 \(foreach\): + ZIP_LISTS can not be used with LISTS or ITEMS +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test.cmake new file mode 100644 index 0000000..11a97b2 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-with-LISTS-mix-test.cmake @@ -0,0 +1,2 @@ +foreach(i IN ZIP_LISTS blah LISTS blah) +endforeach() -- cgit v0.12 From d30468a2f69f80c4f7d9d16b68f347bc3815dacc Mon Sep 17 00:00:00 2001 From: Alex Turbov Date: Sat, 9 Nov 2019 17:50:47 +0200 Subject: foreach: Allow multiple iteration variables for `ZIP_LIST` mode --- Help/command/foreach.rst | 37 ++++--- Source/cmForEachCommand.cxx | 107 +++++++++++++++++---- Tests/RunCMake/foreach/RunCMakeTest.cmake | 5 + ...oreach-ITEMS-multiple-iter-vars-test-result.txt | 1 + ...oreach-ITEMS-multiple-iter-vars-test-stderr.txt | 4 + .../foreach-ITEMS-multiple-iter-vars-test.cmake | 2 + ...oreach-LISTS-multiple-iter-vars-test-result.txt | 1 + ...oreach-LISTS-multiple-iter-vars-test-stderr.txt | 4 + .../foreach-LISTS-multiple-iter-vars-test.cmake | 2 + ...-ZIP_LISTS-iter-vars-mismatch-test-1-result.txt | 1 + ...-ZIP_LISTS-iter-vars-mismatch-test-1-stderr.txt | 4 + ...reach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake | 2 + ...-ZIP_LISTS-iter-vars-mismatch-test-2-result.txt | 1 + ...-ZIP_LISTS-iter-vars-mismatch-test-2-stderr.txt | 4 + ...reach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake | 2 + ...ch-ZIP_LISTS-multiple-iter-vars-test-stdout.txt | 6 ++ ...foreach-ZIP_LISTS-multiple-iter-vars-test.cmake | 42 ++++++++ 17 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-result.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-stderr.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test-stdout.txt create mode 100644 Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test.cmake diff --git a/Help/command/foreach.rst b/Help/command/foreach.rst index dbd2fac..a01a104 100644 --- a/Help/command/foreach.rst +++ b/Help/command/foreach.rst @@ -86,27 +86,42 @@ yields .. code-block:: cmake - foreach( IN ZIP_LISTS ) + foreach(... IN ZIP_LISTS ) In this variant, ```` is a whitespace or semicolon separated list of list-valued variables. The ``foreach`` -command iterates over each list simultaneously and set the -``loop_var_N`` variables to the current item from the -corresponding list. If some list is shorter than others, -the corresponding iteration variable is not defined. +command iterates over each list simultaneously setting the +iteration variables as follows: + +- if the only ``loop_var`` given, then it sets a series of + ``loop_var_N`` variables to the current item from the + corresponding list; +- if multiple variable names passed, their count should match + the lists variables count; +- if any of the lists are shorter, the corresponding iteration + variable is not defined for the current iteration. .. code-block:: cmake list(APPEND English one two three four) list(APPEND Bahasa satu dua tiga) - foreach(X IN ZIP_LISTS English Bahasa) - message(STATUS "X_0=${X_0}, X_1=${X_1}") + + foreach(num IN ZIP_LISTS English Bahasa) + message(STATUS "num_0=${num_0}, num_1=${num_1}") + endforeach() + + foreach(en ba IN ZIP_LISTS English Bahasa) + message(STATUS "en=${en}, ba=${ba}") endforeach() yields :: - -- X_0=one, X_1=satu - -- X_0=two, X_1=dua - -- X_0=three, X_1=tiga - -- X_0=four, X_1= + -- num_0=one, num_1=satu + -- num_0=two, num_1=dua + -- num_0=three, num_1=tiga + -- num_0=four, num_1= + -- en=one, ba=satu + -- en=two, ba=dua + -- en=three, ba=tiga + -- en=four, ba= diff --git a/Source/cmForEachCommand.cxx b/Source/cmForEachCommand.cxx index 163bb9a..ac48287 100644 --- a/Source/cmForEachCommand.cxx +++ b/Source/cmForEachCommand.cxx @@ -3,6 +3,7 @@ #include "cmForEachCommand.h" #include +#include #include // NOTE The declaration of `std::abs` has moved to `cmath` since C++17 // See https://en.cppreference.com/w/cpp/numeric/math/abs @@ -43,6 +44,10 @@ public: bool Replay(std::vector functions, cmExecutionStatus& inStatus) override; + void SetIterationVarsCount(const std::size_t varsCount) + { + this->IterationVarsCount = varsCount; + } void SetZipLists() { this->ZipLists = true; } std::vector Args; @@ -64,6 +69,7 @@ private: cmExecutionStatus& inStatus, cmMakefile& mf); cmMakefile* Makefile; + std::size_t IterationVarsCount = 0u; bool ZipLists = false; }; @@ -98,6 +104,9 @@ bool cmForEachFunctionBlocker::ReplayItems( std::vector const& functions, cmExecutionStatus& inStatus) { + assert("Unexpected number of iteration variables" && + this->IterationVarsCount == 1); + auto& mf = inStatus.GetMakefile(); // At end of for each execute recorded commands @@ -130,35 +139,55 @@ bool cmForEachFunctionBlocker::ReplayZipLists( std::vector const& functions, cmExecutionStatus& inStatus) { + assert("Unexpected number of iteration variables" && + this->IterationVarsCount >= 1); + auto& mf = inStatus.GetMakefile(); // Expand the list of list-variables into a list of lists of strings std::vector> values; - values.reserve(this->Args.size() - 1); + values.reserve(this->Args.size() - this->IterationVarsCount); // Also track the longest list size - std::size_t max_items = 0u; - for (auto const& var : cmMakeRange(this->Args).advance(1)) { + std::size_t maxItems = 0u; + for (auto const& var : + cmMakeRange(this->Args).advance(this->IterationVarsCount)) { std::vector items; auto const& value = mf.GetSafeDefinition(var); if (!value.empty()) { cmExpandList(value, items, true); } - max_items = std::max(max_items, items.size()); + maxItems = std::max(maxItems, items.size()); values.emplace_back(std::move(items)); } + // Form the list of iteration variables + std::vector iterationVars; + if (this->IterationVarsCount > 1) { + // If multiple iteration variables has given, + // just copy them to the `iterationVars` list. + iterationVars.reserve(values.size()); + std::copy(this->Args.begin(), + this->Args.begin() + this->IterationVarsCount, + std::back_inserter(iterationVars)); + } else { + // In case of the only iteration variable, + // generate names as `var_name_N`, + // where `N` is the count of lists to zip + iterationVars.resize(values.size()); + const auto iter_var_prefix = this->Args.front() + "_"; + auto i = 0u; + std::generate( + iterationVars.begin(), iterationVars.end(), + [&]() -> std::string { return iter_var_prefix + std::to_string(i++); }); + } + assert("Sanity check" && iterationVars.size() == values.size()); + // Store old values for iteration variables std::map oldDefs; - // Also, form the vector of iteration variable names - std::vector iteration_vars; - iteration_vars.reserve(values.size()); - const auto iter_var_prefix = this->Args.front(); for (auto i = 0u; i < values.size(); ++i) { - auto iter_var_name = iter_var_prefix + "_" + std::to_string(i); - if (mf.GetDefinition(iter_var_name)) { - oldDefs.emplace(iter_var_name, mf.GetDefinition(iter_var_name)); + if (mf.GetDefinition(iterationVars[i])) { + oldDefs.emplace(iterationVars[i], mf.GetDefinition(iterationVars[i])); } - iteration_vars.emplace_back(std::move(iter_var_name)); } // Form a vector of current positions in all lists (Ok, vectors) of values @@ -168,19 +197,20 @@ bool cmForEachFunctionBlocker::ReplayZipLists( values.begin(), values.end(), std::back_inserter(positions), // Set the initial position to the beginning of every list [](decltype(values)::value_type& list) { return list.begin(); }); + assert("Sanity check" && positions.size() == values.size()); auto restore = false; // Iterate over all the lists simulateneously - for (auto i = 0u; i < max_items; ++i) { + for (auto i = 0u; i < maxItems; ++i) { // Declare iteration variables for (auto j = 0u; j < values.size(); ++j) { // Define (or not) the iteration variable if the current position // still not at the end... if (positions[j] != values[j].end()) { - mf.AddDefinition(iteration_vars[j], *positions[j]); + mf.AddDefinition(iterationVars[j], *positions[j]); ++positions[j]; } else { - mf.RemoveDefinition(iteration_vars[j]); + mf.RemoveDefinition(iterationVars[j]); } } // Invoke all the functions that were collected in the block. @@ -230,10 +260,19 @@ auto cmForEachFunctionBlocker::invoke( return result; } -bool HandleInMode(std::vector const& args, cmMakefile& makefile) +bool HandleInMode(std::vector const& args, + std::vector::const_iterator kwInIter, + cmMakefile& makefile) { + assert("A valid iterator expected" && kwInIter != args.end()); + auto fb = cm::make_unique(&makefile); - fb->Args.push_back(args.front()); + + // Copy iteration variable names first + std::copy(args.begin(), kwInIter, std::back_inserter(fb->Args)); + // Remember the count of given iteration variable names + const auto varsCount = fb->Args.size(); + fb->SetIterationVarsCount(varsCount); enum Doing { @@ -243,13 +282,20 @@ bool HandleInMode(std::vector const& args, cmMakefile& makefile) DoingZipLists }; Doing doing = DoingNone; - for (std::string const& arg : cmMakeRange(args).advance(2)) { + // Iterate over arguments past the "IN" keyword + for (std::string const& arg : cmMakeRange(++kwInIter, args.end())) { if (arg == "LISTS") { if (doing == DoingZipLists) { makefile.IssueMessage(MessageType::FATAL_ERROR, "ZIP_LISTS can not be used with LISTS or ITEMS"); return true; } + if (varsCount != 1u) { + makefile.IssueMessage( + MessageType::FATAL_ERROR, + "ITEMS or LISTS require exactly one iteration variable"); + return true; + } doing = DoingLists; } else if (arg == "ITEMS") { @@ -258,6 +304,12 @@ bool HandleInMode(std::vector const& args, cmMakefile& makefile) "ZIP_LISTS can not be used with LISTS or ITEMS"); return true; } + if (varsCount != 1u) { + makefile.IssueMessage( + MessageType::FATAL_ERROR, + "ITEMS or LISTS require exactly one iteration variable"); + return true; + } doing = DoingItems; } else if (arg == "ZIP_LISTS") { @@ -285,6 +337,18 @@ bool HandleInMode(std::vector const& args, cmMakefile& makefile) } } + // If `ZIP_LISTS` given and variables count more than 1, + // make sure the given lists count matches variables... + if (doing == DoingZipLists && varsCount > 1u && + (2u * varsCount) != fb->Args.size()) { + makefile.IssueMessage( + MessageType::FATAL_ERROR, + cmStrCat("Expected ", std::to_string(varsCount), + " list variables, but given ", + std::to_string(fb->Args.size() - varsCount))); + return true; + } + makefile.AddFunctionBlocker(std::move(fb)); return true; @@ -299,8 +363,9 @@ bool cmForEachCommand(std::vector const& args, status.SetError("called with incorrect number of arguments"); return false; } - if (args.size() > 1 && args[1] == "IN") { - return HandleInMode(args, status.GetMakefile()); + auto kwInIter = std::find(args.begin(), args.end(), "IN"); + if (kwInIter != args.end()) { + return HandleInMode(args, kwInIter, status.GetMakefile()); } // create a function blocker @@ -360,6 +425,8 @@ bool cmForEachCommand(std::vector const& args, } else { fb->Args = args; } + + fb->SetIterationVarsCount(1u); status.GetMakefile().AddFunctionBlocker(std::move(fb)); return true; diff --git a/Tests/RunCMake/foreach/RunCMakeTest.cmake b/Tests/RunCMake/foreach/RunCMakeTest.cmake index 371a0cb..8f50203 100644 --- a/Tests/RunCMake/foreach/RunCMakeTest.cmake +++ b/Tests/RunCMake/foreach/RunCMakeTest.cmake @@ -2,8 +2,13 @@ include(RunCMake) run_cmake(BadRangeInFunction) run_cmake(foreach-all-test) +run_cmake(foreach-ITEMS-multiple-iter-vars-test) +run_cmake(foreach-LISTS-multiple-iter-vars-test) run_cmake(foreach-ZIP_LISTS-test) run_cmake(foreach-ITEMS-with-ZIP_LISTS-mix-test) run_cmake(foreach-LISTS-with-ZIP_LISTS-mix-test) run_cmake(foreach-ZIP_LISTS-with-ITEMS-mix-test) run_cmake(foreach-ZIP_LISTS-with-LISTS-mix-test) +run_cmake(foreach-ZIP_LISTS-multiple-iter-vars-test) +run_cmake(foreach-ZIP_LISTS-iter-vars-mismatch-test-1) +run_cmake(foreach-ZIP_LISTS-iter-vars-mismatch-test-2) diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-result.txt b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-stderr.txt b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-stderr.txt new file mode 100644 index 0000000..d174bb1 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ITEMS-multiple-iter-vars-test.cmake:1 \(foreach\): + ITEMS or LISTS require exactly one iteration variable +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test.cmake b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test.cmake new file mode 100644 index 0000000..55d33a8 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ITEMS-multiple-iter-vars-test.cmake @@ -0,0 +1,2 @@ +foreach(one two IN ITEMS one two) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-result.txt b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-stderr.txt b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-stderr.txt new file mode 100644 index 0000000..f2f83c2 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-LISTS-multiple-iter-vars-test.cmake:1 \(foreach\): + ITEMS or LISTS require exactly one iteration variable +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test.cmake b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test.cmake new file mode 100644 index 0000000..78f3847 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-LISTS-multiple-iter-vars-test.cmake @@ -0,0 +1,2 @@ +foreach(one two IN LISTS one two) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-result.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-stderr.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-stderr.txt new file mode 100644 index 0000000..fa51e46 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake:1 \(foreach\): + Expected 3 list variables, but given 4 +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake new file mode 100644 index 0000000..458b6ca --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake @@ -0,0 +1,2 @@ +foreach(less than lists IN ZIP_LISTS list_1 list_2 list_3 list_4) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-result.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-stderr.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-stderr.txt new file mode 100644 index 0000000..7b6b484 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2-stderr.txt @@ -0,0 +1,4 @@ +^CMake Error at foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake:1 \(foreach\): + Expected 3 list variables, but given 2 +Call Stack \(most recent call first\): + CMakeLists.txt:3 \(include\) diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake new file mode 100644 index 0000000..d24d99c --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake @@ -0,0 +1,2 @@ +foreach(greater than lists IN ZIP_LISTS list_1 list_2) +endforeach() diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test-stdout.txt b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test-stdout.txt new file mode 100644 index 0000000..e009d15 --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test-stdout.txt @@ -0,0 +1,6 @@ +-- foreach\(\.\.\. IN ZIP_LISTS\): +-- Begin output +-- | one, satu, raz +-- | two, dua, dva +-- | three, tiga, tri +-- End output diff --git a/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test.cmake b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test.cmake new file mode 100644 index 0000000..9647dea --- /dev/null +++ b/Tests/RunCMake/foreach/foreach-ZIP_LISTS-multiple-iter-vars-test.cmake @@ -0,0 +1,42 @@ +function(foreachTest result list_var_1 list_var_2 list_var_3) + set(_options MUTE) + set(_one_value_args) + set(_multi_value_args) + cmake_parse_arguments(PARSE_ARGV 3 _arg "${_options}" "${_one_value_args}" "${_multi_value_args}") + + set(_has_any_output FALSE) + list(APPEND CMAKE_MESSAGE_INDENT "| ") + foreach(first second third IN ZIP_LISTS ${list_var_1} ${list_var_2} ${list_var_3}) + if(NOT first) + set(first "[undefiend]") + endif() + if(NOT second) + set(second "[undefiend]") + endif() + if(NOT third) + set(third "[undefiend]") + endif() + if(NOT _arg_MUTE) + message(STATUS "${first}, ${second}, ${third}") + endif() + set(_has_any_output TRUE) + endforeach() + set(${result} ${_has_any_output} PARENT_SCOPE) +endfunction() + +function(foreachTestDecorated list_var_1 list_var_2 list_var_3) + list(APPEND CMAKE_MESSAGE_INDENT " ") + message(STATUS "Begin output") + foreachTest(_has_any_output ${list_var_1} ${list_var_2} ${list_var_3}) + if(NOT _has_any_output) + message(STATUS "--> empty-output <--") + endif() + message(STATUS "End output") +endfunction() + +list(APPEND english one two three) +list(APPEND bahasa satu dua tiga) +list(APPEND russian raz dva tri) + +message(STATUS "foreach(... IN ZIP_LISTS):") +foreachTestDecorated(english bahasa russian) -- cgit v0.12