1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3 #include "cmForEachCommand.h"
4
5 #include <algorithm>
6 #include <cassert>
7 #include <cstddef> // IWYU pragma: keep
8 // NOTE The declaration of `std::abs` has moved to `cmath` since C++17
9 // See https://en.cppreference.com/w/cpp/numeric/math/abs
10 // ALERT But IWYU used to lint `#include`s do not "understand"
11 // conditional compilation (i.e. `#if __cplusplus >= 201703L`)
12 #include <cstdlib>
13 #include <iterator>
14 #include <map>
15 #include <sstream>
16 #include <stdexcept>
17 #include <utility>
18
19 #include <cm/memory>
20 #include <cm/optional>
21 #include <cm/string_view>
22 #include <cmext/string_view>
23
24 #include "cmExecutionStatus.h"
25 #include "cmFunctionBlocker.h"
26 #include "cmListFileCache.h"
27 #include "cmMakefile.h"
28 #include "cmMessageType.h"
29 #include "cmPolicies.h"
30 #include "cmRange.h"
31 #include "cmStringAlgorithms.h"
32 #include "cmSystemTools.h"
33 #include "cmValue.h"
34
35 namespace {
36 class cmForEachFunctionBlocker : public cmFunctionBlocker
37 {
38 public:
39 explicit cmForEachFunctionBlocker(cmMakefile* mf);
40 ~cmForEachFunctionBlocker() override;
41
StartCommandName() const42 cm::string_view StartCommandName() const override { return "foreach"_s; }
EndCommandName() const43 cm::string_view EndCommandName() const override { return "endforeach"_s; }
44
45 bool ArgumentsMatch(cmListFileFunction const& lff,
46 cmMakefile& mf) const override;
47
48 bool Replay(std::vector<cmListFileFunction> functions,
49 cmExecutionStatus& inStatus) override;
50
SetIterationVarsCount(const std::size_t varsCount)51 void SetIterationVarsCount(const std::size_t varsCount)
52 {
53 this->IterationVarsCount = varsCount;
54 }
SetZipLists()55 void SetZipLists() { this->ZipLists = true; }
56
57 std::vector<std::string> Args;
58
59 private:
60 struct InvokeResult
61 {
62 bool Restore;
63 bool Break;
64 };
65
66 bool ReplayItems(std::vector<cmListFileFunction> const& functions,
67 cmExecutionStatus& inStatus);
68
69 bool ReplayZipLists(std::vector<cmListFileFunction> const& functions,
70 cmExecutionStatus& inStatus);
71
72 InvokeResult invoke(std::vector<cmListFileFunction> const& functions,
73 cmExecutionStatus& inStatus, cmMakefile& mf);
74
75 cmMakefile* Makefile;
76 std::size_t IterationVarsCount = 0u;
77 bool ZipLists = false;
78 };
79
cmForEachFunctionBlocker(cmMakefile * mf)80 cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf)
81 : Makefile(mf)
82 {
83 this->Makefile->PushLoopBlock();
84 }
85
~cmForEachFunctionBlocker()86 cmForEachFunctionBlocker::~cmForEachFunctionBlocker()
87 {
88 this->Makefile->PopLoopBlock();
89 }
90
ArgumentsMatch(cmListFileFunction const & lff,cmMakefile & mf) const91 bool cmForEachFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff,
92 cmMakefile& mf) const
93 {
94 std::vector<std::string> expandedArguments;
95 mf.ExpandArguments(lff.Arguments(), expandedArguments);
96 return expandedArguments.empty() ||
97 expandedArguments.front() == this->Args.front();
98 }
99
Replay(std::vector<cmListFileFunction> functions,cmExecutionStatus & inStatus)100 bool cmForEachFunctionBlocker::Replay(
101 std::vector<cmListFileFunction> functions, cmExecutionStatus& inStatus)
102 {
103 return this->ZipLists ? this->ReplayZipLists(functions, inStatus)
104 : this->ReplayItems(functions, inStatus);
105 }
106
ReplayItems(std::vector<cmListFileFunction> const & functions,cmExecutionStatus & inStatus)107 bool cmForEachFunctionBlocker::ReplayItems(
108 std::vector<cmListFileFunction> const& functions,
109 cmExecutionStatus& inStatus)
110 {
111 assert("Unexpected number of iteration variables" &&
112 this->IterationVarsCount == 1);
113
114 auto& mf = inStatus.GetMakefile();
115
116 // At end of for each execute recorded commands
117 // store the old value
118 cm::optional<std::string> oldDef;
119 if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) {
120 oldDef = mf.GetSafeDefinition(this->Args.front());
121 } else if (mf.IsNormalDefinitionSet(this->Args.front())) {
122 oldDef = *mf.GetDefinition(this->Args.front());
123 }
124
125 auto restore = false;
126 for (std::string const& arg : cmMakeRange(this->Args).advance(1)) {
127 // Set the variable to the loop value
128 mf.AddDefinition(this->Args.front(), arg);
129 // Invoke all the functions that were collected in the block.
130 auto r = this->invoke(functions, inStatus, mf);
131 restore = r.Restore;
132 if (r.Break) {
133 break;
134 }
135 }
136
137 if (restore) {
138 if (oldDef) {
139 // restore the variable to its prior value
140 mf.AddDefinition(this->Args.front(), *oldDef);
141 } else {
142 mf.RemoveDefinition(this->Args.front());
143 }
144 }
145
146 return true;
147 }
148
ReplayZipLists(std::vector<cmListFileFunction> const & functions,cmExecutionStatus & inStatus)149 bool cmForEachFunctionBlocker::ReplayZipLists(
150 std::vector<cmListFileFunction> const& functions,
151 cmExecutionStatus& inStatus)
152 {
153 assert("Unexpected number of iteration variables" &&
154 this->IterationVarsCount >= 1);
155
156 auto& mf = inStatus.GetMakefile();
157
158 // Expand the list of list-variables into a list of lists of strings
159 std::vector<std::vector<std::string>> values;
160 values.reserve(this->Args.size() - this->IterationVarsCount);
161 // Also track the longest list size
162 std::size_t maxItems = 0u;
163 for (auto const& var :
164 cmMakeRange(this->Args).advance(this->IterationVarsCount)) {
165 std::vector<std::string> items;
166 auto const& value = mf.GetSafeDefinition(var);
167 if (!value.empty()) {
168 cmExpandList(value, items, true);
169 }
170 maxItems = std::max(maxItems, items.size());
171 values.emplace_back(std::move(items));
172 }
173
174 // Form the list of iteration variables
175 std::vector<std::string> iterationVars;
176 if (this->IterationVarsCount > 1) {
177 // If multiple iteration variables has given,
178 // just copy them to the `iterationVars` list.
179 iterationVars.reserve(values.size());
180 std::copy(this->Args.begin(),
181 this->Args.begin() + this->IterationVarsCount,
182 std::back_inserter(iterationVars));
183 } else {
184 // In case of the only iteration variable,
185 // generate names as `var_name_N`,
186 // where `N` is the count of lists to zip
187 iterationVars.resize(values.size());
188 const auto iter_var_prefix = this->Args.front() + "_";
189 auto i = 0u;
190 std::generate(
191 iterationVars.begin(), iterationVars.end(),
192 [&]() -> std::string { return iter_var_prefix + std::to_string(i++); });
193 }
194 assert("Sanity check" && iterationVars.size() == values.size());
195
196 // Store old values for iteration variables
197 std::map<std::string, cm::optional<std::string>> oldDefs;
198 for (auto i = 0u; i < values.size(); ++i) {
199 const auto& varName = iterationVars[i];
200 if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) {
201 oldDefs.emplace(varName, mf.GetSafeDefinition(varName));
202 } else if (mf.IsNormalDefinitionSet(varName)) {
203 oldDefs.emplace(varName, *mf.GetDefinition(varName));
204 } else {
205 oldDefs.emplace(varName, cm::nullopt);
206 }
207 }
208
209 // Form a vector of current positions in all lists (Ok, vectors) of values
210 std::vector<decltype(values)::value_type::iterator> positions;
211 positions.reserve(values.size());
212 std::transform(
213 values.begin(), values.end(), std::back_inserter(positions),
214 // Set the initial position to the beginning of every list
215 [](decltype(values)::value_type& list) { return list.begin(); });
216 assert("Sanity check" && positions.size() == values.size());
217
218 auto restore = false;
219 // Iterate over all the lists simulateneously
220 for (auto i = 0u; i < maxItems; ++i) {
221 // Declare iteration variables
222 for (auto j = 0u; j < values.size(); ++j) {
223 // Define (or not) the iteration variable if the current position
224 // still not at the end...
225 if (positions[j] != values[j].end()) {
226 mf.AddDefinition(iterationVars[j], *positions[j]);
227 ++positions[j];
228 } else {
229 mf.RemoveDefinition(iterationVars[j]);
230 }
231 }
232 // Invoke all the functions that were collected in the block.
233 auto r = this->invoke(functions, inStatus, mf);
234 restore = r.Restore;
235 if (r.Break) {
236 break;
237 }
238 }
239
240 // Restore the variables to its prior value
241 if (restore) {
242 for (auto const& p : oldDefs) {
243 if (p.second) {
244 mf.AddDefinition(p.first, *p.second);
245 } else {
246 mf.RemoveDefinition(p.first);
247 }
248 }
249 }
250 return true;
251 }
252
invoke(std::vector<cmListFileFunction> const & functions,cmExecutionStatus & inStatus,cmMakefile & mf)253 auto cmForEachFunctionBlocker::invoke(
254 std::vector<cmListFileFunction> const& functions,
255 cmExecutionStatus& inStatus, cmMakefile& mf) -> InvokeResult
256 {
257 InvokeResult result = { true, false };
258 // Invoke all the functions that were collected in the block.
259 for (cmListFileFunction const& func : functions) {
260 cmExecutionStatus status(mf);
261 mf.ExecuteCommand(func, status);
262 if (status.GetReturnInvoked()) {
263 inStatus.SetReturnInvoked();
264 result.Break = true;
265 break;
266 }
267 if (status.GetBreakInvoked()) {
268 result.Break = true;
269 break;
270 }
271 if (status.GetContinueInvoked()) {
272 break;
273 }
274 if (cmSystemTools::GetFatalErrorOccured()) {
275 result.Restore = false;
276 result.Break = true;
277 break;
278 }
279 }
280 return result;
281 }
282
HandleInMode(std::vector<std::string> const & args,std::vector<std::string>::const_iterator kwInIter,cmMakefile & makefile)283 bool HandleInMode(std::vector<std::string> const& args,
284 std::vector<std::string>::const_iterator kwInIter,
285 cmMakefile& makefile)
286 {
287 assert("A valid iterator expected" && kwInIter != args.end());
288
289 auto fb = cm::make_unique<cmForEachFunctionBlocker>(&makefile);
290
291 // Copy iteration variable names first
292 std::copy(args.begin(), kwInIter, std::back_inserter(fb->Args));
293 // Remember the count of given iteration variable names
294 const auto varsCount = fb->Args.size();
295 fb->SetIterationVarsCount(varsCount);
296
297 enum Doing
298 {
299 DoingNone,
300 DoingLists,
301 DoingItems,
302 DoingZipLists
303 };
304 Doing doing = DoingNone;
305 // Iterate over arguments past the "IN" keyword
306 for (std::string const& arg : cmMakeRange(++kwInIter, args.end())) {
307 if (arg == "LISTS") {
308 if (doing == DoingZipLists) {
309 makefile.IssueMessage(MessageType::FATAL_ERROR,
310 "ZIP_LISTS can not be used with LISTS or ITEMS");
311 return true;
312 }
313 if (varsCount != 1u) {
314 makefile.IssueMessage(
315 MessageType::FATAL_ERROR,
316 "ITEMS or LISTS require exactly one iteration variable");
317 return true;
318 }
319 doing = DoingLists;
320
321 } else if (arg == "ITEMS") {
322 if (doing == DoingZipLists) {
323 makefile.IssueMessage(MessageType::FATAL_ERROR,
324 "ZIP_LISTS can not be used with LISTS or ITEMS");
325 return true;
326 }
327 if (varsCount != 1u) {
328 makefile.IssueMessage(
329 MessageType::FATAL_ERROR,
330 "ITEMS or LISTS require exactly one iteration variable");
331 return true;
332 }
333 doing = DoingItems;
334
335 } else if (arg == "ZIP_LISTS") {
336 if (doing != DoingNone) {
337 makefile.IssueMessage(MessageType::FATAL_ERROR,
338 "ZIP_LISTS can not be used with LISTS or ITEMS");
339 return true;
340 }
341 doing = DoingZipLists;
342 fb->SetZipLists();
343
344 } else if (doing == DoingLists) {
345 auto const& value = makefile.GetSafeDefinition(arg);
346 if (!value.empty()) {
347 cmExpandList(value, fb->Args, true);
348 }
349
350 } else if (doing == DoingItems || doing == DoingZipLists) {
351 fb->Args.push_back(arg);
352
353 } else {
354 makefile.IssueMessage(MessageType::FATAL_ERROR,
355 cmStrCat("Unknown argument:\n", " ", arg, "\n"));
356 return true;
357 }
358 }
359
360 // If `ZIP_LISTS` given and variables count more than 1,
361 // make sure the given lists count matches variables...
362 if (doing == DoingZipLists && varsCount > 1u &&
363 (2u * varsCount) != fb->Args.size()) {
364 makefile.IssueMessage(
365 MessageType::FATAL_ERROR,
366 cmStrCat("Expected ", std::to_string(varsCount),
367 " list variables, but given ",
368 std::to_string(fb->Args.size() - varsCount)));
369 return true;
370 }
371
372 makefile.AddFunctionBlocker(std::move(fb));
373
374 return true;
375 }
376
TryParseInteger(cmExecutionStatus & status,const std::string & str,int & i)377 bool TryParseInteger(cmExecutionStatus& status, const std::string& str, int& i)
378 {
379 try {
380 i = std::stoi(str);
381 } catch (std::invalid_argument&) {
382 std::ostringstream e;
383 e << "Invalid integer: '" << str << "'";
384 status.SetError(e.str());
385 cmSystemTools::SetFatalErrorOccured();
386 return false;
387 } catch (std::out_of_range&) {
388 std::ostringstream e;
389 e << "Integer out of range: '" << str << "'";
390 status.SetError(e.str());
391 cmSystemTools::SetFatalErrorOccured();
392 return false;
393 }
394
395 return true;
396 }
397
398 } // anonymous namespace
399
cmForEachCommand(std::vector<std::string> const & args,cmExecutionStatus & status)400 bool cmForEachCommand(std::vector<std::string> const& args,
401 cmExecutionStatus& status)
402 {
403 if (args.empty()) {
404 status.SetError("called with incorrect number of arguments");
405 return false;
406 }
407 auto kwInIter = std::find(args.begin(), args.end(), "IN");
408 if (kwInIter != args.end()) {
409 return HandleInMode(args, kwInIter, status.GetMakefile());
410 }
411
412 // create a function blocker
413 auto fb = cm::make_unique<cmForEachFunctionBlocker>(&status.GetMakefile());
414 if (args.size() > 1) {
415 if (args[1] == "RANGE") {
416 int start = 0;
417 int stop = 0;
418 int step = 0;
419 if (args.size() == 3) {
420 if (!TryParseInteger(status, args[2], stop)) {
421 return false;
422 }
423 }
424 if (args.size() == 4) {
425 if (!TryParseInteger(status, args[2], start)) {
426 return false;
427 }
428 if (!TryParseInteger(status, args[3], stop)) {
429 return false;
430 }
431 }
432 if (args.size() == 5) {
433 if (!TryParseInteger(status, args[2], start)) {
434 return false;
435 }
436 if (!TryParseInteger(status, args[3], stop)) {
437 return false;
438 }
439 if (!TryParseInteger(status, args[4], step)) {
440 return false;
441 }
442 }
443 if (step == 0) {
444 if (start > stop) {
445 step = -1;
446 } else {
447 step = 1;
448 }
449 }
450 if ((start > stop && step > 0) || (start < stop && step < 0) ||
451 step == 0) {
452 status.SetError(
453 cmStrCat("called with incorrect range specification: start ", start,
454 ", stop ", stop, ", step ", step));
455 cmSystemTools::SetFatalErrorOccured();
456 return false;
457 }
458
459 // Calculate expected iterations count and reserve enough space
460 // in the `fb->Args` vector. The first item is the iteration variable
461 // name...
462 const std::size_t iter_cnt = 2u +
463 int(start < stop) * (stop - start) / std::abs(step) +
464 int(start > stop) * (start - stop) / std::abs(step);
465 fb->Args.resize(iter_cnt);
466 fb->Args.front() = args.front();
467 auto cc = start;
468 auto generator = [&cc, step]() -> std::string {
469 auto result = std::to_string(cc);
470 cc += step;
471 return result;
472 };
473 // Fill the `range` vector w/ generated string values
474 // (starting from 2nd position)
475 std::generate(++fb->Args.begin(), fb->Args.end(), generator);
476 } else {
477 fb->Args = args;
478 }
479 } else {
480 fb->Args = args;
481 }
482
483 fb->SetIterationVarsCount(1u);
484 status.GetMakefile().AddFunctionBlocker(std::move(fb));
485
486 return true;
487 }
488