| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file Copyright.txt or https://cmake.org/licensing for details. */ | |
| #include "cmForEachCommand.h" | |
| #include <algorithm> | |
| #include <cassert> | |
| #include <cstddef> // IWYU pragma: keep | |
| // NOTE The declaration of `std::abs` has moved to `cmath` since C++17 | |
| // See https://en.cppreference.com/w/cpp/numeric/math/abs | |
| // ALERT But IWYU used to lint `#include`s do not "understand" | |
| // conditional compilation (i.e. `#if __cplusplus >= 201703L`) | |
| #include <cstdlib> | |
| #include <iterator> | |
| #include <map> | |
| #include <sstream> | |
| #include <stdexcept> | |
| #include <utility> | |
| #include <cm/memory> | |
| #include <cm/optional> | |
| #include <cm/string_view> | |
| #include <cmext/string_view> | |
| #include "cmExecutionStatus.h" | |
| #include "cmFunctionBlocker.h" | |
| #include "cmList.h" | |
| #include "cmListFileCache.h" | |
| #include "cmMakefile.h" | |
| #include "cmMessageType.h" | |
| #include "cmPolicies.h" | |
| #include "cmRange.h" | |
| #include "cmStringAlgorithms.h" | |
| #include "cmSystemTools.h" | |
| #include "cmValue.h" | |
| namespace { | |
| class cmForEachFunctionBlocker : public cmFunctionBlocker | |
| { | |
| public: | |
| explicit cmForEachFunctionBlocker(cmMakefile* mf); | |
| ~cmForEachFunctionBlocker() override; | |
| cm::string_view StartCommandName() const override { return "foreach"_s; } | |
| cm::string_view EndCommandName() const override { return "endforeach"_s; } | |
| bool ArgumentsMatch(cmListFileFunction const& lff, | |
| cmMakefile& mf) const override; | |
| bool Replay(std::vector<cmListFileFunction> functions, | |
| cmExecutionStatus& inStatus) override; | |
| void SetIterationVarsCount(const std::size_t varsCount) | |
| { | |
| this->IterationVarsCount = varsCount; | |
| } | |
| void SetZipLists() { this->ZipLists = true; } | |
| std::vector<std::string> Args; | |
| private: | |
| struct InvokeResult | |
| { | |
| bool Restore; | |
| bool Break; | |
| }; | |
| bool ReplayItems(std::vector<cmListFileFunction> const& functions, | |
| cmExecutionStatus& inStatus); | |
| bool ReplayZipLists(std::vector<cmListFileFunction> const& functions, | |
| cmExecutionStatus& inStatus); | |
| InvokeResult invoke(std::vector<cmListFileFunction> const& functions, | |
| cmExecutionStatus& inStatus, cmMakefile& mf); | |
| cmMakefile* Makefile; | |
| std::size_t IterationVarsCount = 0u; | |
| bool ZipLists = false; | |
| }; | |
| cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf) | |
| : Makefile(mf) | |
| { | |
| this->Makefile->PushLoopBlock(); | |
| } | |
| cmForEachFunctionBlocker::~cmForEachFunctionBlocker() | |
| { | |
| this->Makefile->PopLoopBlock(); | |
| } | |
| bool cmForEachFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff, | |
| cmMakefile& mf) const | |
| { | |
| std::vector<std::string> expandedArguments; | |
| mf.ExpandArguments(lff.Arguments(), expandedArguments); | |
| return expandedArguments.empty() || | |
| expandedArguments.front() == this->Args.front(); | |
| } | |
| bool cmForEachFunctionBlocker::Replay( | |
| std::vector<cmListFileFunction> functions, cmExecutionStatus& inStatus) | |
| { | |
| return this->ZipLists ? this->ReplayZipLists(functions, inStatus) | |
| : this->ReplayItems(functions, inStatus); | |
| } | |
| bool cmForEachFunctionBlocker::ReplayItems( | |
| std::vector<cmListFileFunction> 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 | |
| // store the old value | |
| cm::optional<std::string> oldDef; | |
| if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) { | |
| oldDef = mf.GetSafeDefinition(this->Args.front()); | |
| } else if (mf.IsNormalDefinitionSet(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 | |
| mf.AddDefinition(this->Args.front(), arg); | |
| // Invoke all the functions that were collected in the block. | |
| auto r = this->invoke(functions, inStatus, mf); | |
| restore = r.Restore; | |
| if (r.Break) { | |
| break; | |
| } | |
| } | |
| if (restore) { | |
| if (oldDef) { | |
| // restore the variable to its prior value | |
| mf.AddDefinition(this->Args.front(), *oldDef); | |
| } else { | |
| mf.RemoveDefinition(this->Args.front()); | |
| } | |
| } | |
| return true; | |
| } | |
| bool cmForEachFunctionBlocker::ReplayZipLists( | |
| std::vector<cmListFileFunction> 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<cmList> values; | |
| values.reserve(this->Args.size() - this->IterationVarsCount); | |
| // Also track the longest list size | |
| std::size_t maxItems = 0u; | |
| for (auto const& var : | |
| cmMakeRange(this->Args).advance(this->IterationVarsCount)) { | |
| cmList items; | |
| auto const& value = mf.GetSafeDefinition(var); | |
| if (!value.empty()) { | |
| items.assign(value, cmList::EmptyElements::Yes); | |
| } | |
| maxItems = std::max(maxItems, items.size()); | |
| values.emplace_back(std::move(items)); | |
| } | |
| // Form the list of iteration variables | |
| std::vector<std::string> 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<std::string, cm::optional<std::string>> oldDefs; | |
| for (auto i = 0u; i < values.size(); ++i) { | |
| const auto& varName = iterationVars[i]; | |
| if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) { | |
| oldDefs.emplace(varName, mf.GetSafeDefinition(varName)); | |
| } else if (mf.IsNormalDefinitionSet(varName)) { | |
| oldDefs.emplace(varName, *mf.GetDefinition(varName)); | |
| } else { | |
| oldDefs.emplace(varName, cm::nullopt); | |
| } | |
| } | |
| // Form a vector of current positions in all lists (Ok, vectors) of values | |
| std::vector<decltype(values)::value_type::iterator> 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(); }); | |
| assert("Sanity check" && positions.size() == values.size()); | |
| auto restore = false; | |
| // Iterate over all the lists simulateneously | |
| 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(iterationVars[j], *positions[j]); | |
| ++positions[j]; | |
| } else { | |
| mf.RemoveDefinition(iterationVars[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 variables to its prior value | |
| if (restore) { | |
| for (auto const& p : oldDefs) { | |
| if (p.second) { | |
| mf.AddDefinition(p.first, *p.second); | |
| } else { | |
| mf.RemoveDefinition(p.first); | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| auto cmForEachFunctionBlocker::invoke( | |
| std::vector<cmListFileFunction> 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(status.GetReturnVariables()); | |
| result.Break = true; | |
| break; | |
| } | |
| if (status.GetBreakInvoked()) { | |
| result.Break = true; | |
| break; | |
| } | |
| if (status.GetContinueInvoked()) { | |
| break; | |
| } | |
| if (cmSystemTools::GetFatalErrorOccurred()) { | |
| result.Restore = false; | |
| result.Break = true; | |
| break; | |
| } | |
| } | |
| return result; | |
| } | |
| bool HandleInMode(std::vector<std::string> const& args, | |
| std::vector<std::string>::const_iterator kwInIter, | |
| cmMakefile& makefile) | |
| { | |
| assert("A valid iterator expected" && kwInIter != args.end()); | |
| auto fb = cm::make_unique<cmForEachFunctionBlocker>(&makefile); | |
| // 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 | |
| { | |
| DoingNone, | |
| DoingLists, | |
| DoingItems, | |
| DoingZipLists | |
| }; | |
| Doing doing = DoingNone; | |
| // 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") { | |
| 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 = 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, cmList::EmptyElements::Yes); | |
| } | |
| } else if (doing == DoingItems || doing == DoingZipLists) { | |
| fb->Args.push_back(arg); | |
| } else { | |
| makefile.IssueMessage(MessageType::FATAL_ERROR, | |
| cmStrCat("Unknown argument:\n", " ", arg, "\n")); | |
| return true; | |
| } | |
| } | |
| // 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; | |
| } | |
| bool TryParseInteger(cmExecutionStatus& status, const std::string& str, int& i) | |
| { | |
| try { | |
| i = std::stoi(str); | |
| } catch (std::invalid_argument&) { | |
| std::ostringstream e; | |
| e << "Invalid integer: '" << str << "'"; | |
| status.SetError(e.str()); | |
| cmSystemTools::SetFatalErrorOccurred(); | |
| return false; | |
| } catch (std::out_of_range&) { | |
| std::ostringstream e; | |
| e << "Integer out of range: '" << str << "'"; | |
| status.SetError(e.str()); | |
| cmSystemTools::SetFatalErrorOccurred(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| } // anonymous namespace | |
| bool cmForEachCommand(std::vector<std::string> const& args, | |
| cmExecutionStatus& status) | |
| { | |
| if (args.empty()) { | |
| status.SetError("called with incorrect number of arguments"); | |
| return false; | |
| } | |
| auto kwInIter = std::find(args.begin(), args.end(), "IN"); | |
| if (kwInIter != args.end()) { | |
| return HandleInMode(args, kwInIter, status.GetMakefile()); | |
| } | |
| // create a function blocker | |
| auto fb = cm::make_unique<cmForEachFunctionBlocker>(&status.GetMakefile()); | |
| if (args.size() > 1) { | |
| if (args[1] == "RANGE") { | |
| int start = 0; | |
| int stop = 0; | |
| int step = 0; | |
| if (args.size() == 3) { | |
| if (!TryParseInteger(status, args[2], stop)) { | |
| return false; | |
| } | |
| } | |
| if (args.size() == 4) { | |
| if (!TryParseInteger(status, args[2], start)) { | |
| return false; | |
| } | |
| if (!TryParseInteger(status, args[3], stop)) { | |
| return false; | |
| } | |
| } | |
| if (args.size() == 5) { | |
| if (!TryParseInteger(status, args[2], start)) { | |
| return false; | |
| } | |
| if (!TryParseInteger(status, args[3], stop)) { | |
| return false; | |
| } | |
| if (!TryParseInteger(status, args[4], step)) { | |
| return false; | |
| } | |
| } | |
| if (step == 0) { | |
| if (start > stop) { | |
| step = -1; | |
| } else { | |
| step = 1; | |
| } | |
| } | |
| if ((start > stop && step > 0) || (start < stop && step < 0) || | |
| step == 0) { | |
| status.SetError( | |
| cmStrCat("called with incorrect range specification: start ", start, | |
| ", stop ", stop, ", step ", step)); | |
| cmSystemTools::SetFatalErrorOccurred(); | |
| return false; | |
| } | |
| // Calculate expected iterations count and reserve enough space | |
| // in the `fb->Args` vector. The first item is the iteration variable | |
| // name... | |
| const std::size_t iter_cnt = 2u + | |
| static_cast<int>(start < stop) * (stop - start) / std::abs(step) + | |
| static_cast<int>(start > stop) * (start - stop) / std::abs(step); | |
| fb->Args.resize(iter_cnt); | |
| fb->Args.front() = args.front(); | |
| auto cc = start; | |
| auto generator = [&cc, step]() -> std::string { | |
| auto result = std::to_string(cc); | |
| cc += step; | |
| return result; | |
| }; | |
| // Fill the `range` vector w/ generated string values | |
| // (starting from 2nd position) | |
| std::generate(++fb->Args.begin(), fb->Args.end(), generator); | |
| } else { | |
| fb->Args = args; | |
| } | |
| } else { | |
| fb->Args = args; | |
| } | |
| fb->SetIterationVarsCount(1u); | |
| status.GetMakefile().AddFunctionBlocker(std::move(fb)); | |
| return true; | |
| } |