1 /*
2 * FOR.C - for internal batch command.
3 *
4 *
5 * History:
6 *
7 * 16-Jul-1998 (Hans B Pufal)
8 * Started.
9 *
10 * 16-Jul-1998 (John P Price)
11 * Separated commands into individual files.
12 *
13 * 19-Jul-1998 (Hans B Pufal)
14 * Implementation of FOR.
15 *
16 * 27-Jul-1998 (John P Price <linux-guru@gcfl.net>)
17 * Added config.h include.
18 *
19 * 20-Jan-1999 (Eric Kohl)
20 * Unicode and redirection safe!
21 *
22 * 01-Sep-1999 (Eric Kohl)
23 * Added help text.
24 *
25 * 23-Feb-2001 (Carl Nettelblad <cnettel@hem.passagen.se>)
26 * Implemented preservation of echo flag. Some other for related
27 * code in other files fixed, too.
28 *
29 * 28-Apr-2005 (Magnus Olsen <magnus@greatlord.com>)
30 * Remove all hardcoded strings in En.rc
31 */
32
33 #include "precomp.h"
34
35 /* Enable this define for "buggy" Windows' CMD FOR-command compatibility.
36 * Currently, this enables the buggy behaviour of FOR /F token parsing. */
37 #define MSCMD_FOR_QUIRKS
38
39
40 /* FOR is a special command, so this function is only used for showing help now */
cmd_for(LPTSTR param)41 INT cmd_for(LPTSTR param)
42 {
43 TRACE("cmd_for(\'%s\')\n", debugstr_aw(param));
44
45 if (!_tcsncmp(param, _T("/?"), 2))
46 {
47 ConOutResPaging(TRUE, STRING_FOR_HELP1);
48 return 0;
49 }
50
51 ParseErrorEx(param);
52 return 1;
53 }
54
55 /* The stack of current FOR contexts.
56 * NULL when no FOR command is active */
57 PFOR_CONTEXT fc = NULL;
58
59 /* Get the next element of the FOR's list */
GetNextElement(TCHAR ** pStart,TCHAR ** pEnd)60 static BOOL GetNextElement(TCHAR **pStart, TCHAR **pEnd)
61 {
62 TCHAR *p = *pEnd;
63 BOOL InQuotes = FALSE;
64 while (_istspace(*p))
65 p++;
66 if (!*p)
67 return FALSE;
68 *pStart = p;
69 while (*p && (InQuotes || !_istspace(*p)))
70 InQuotes ^= (*p++ == _T('"'));
71 *pEnd = p;
72 return TRUE;
73 }
74
75 /* Execute a single instance of a FOR command */
76 /* Just run the command (variable expansion is done in DoDelayedExpansion) */
77 #define RunInstance(Cmd) \
78 ExecuteCommandWithEcho((Cmd)->Subcommands)
79
80 /* Check if this FOR should be terminated early */
81 #define Exiting(Cmd) \
82 /* Someone might have removed our context */ \
83 (bCtrlBreak || (fc != (Cmd)->For.Context))
84 /* Take also GOTO jumps into account */
85 #define ExitingOrGoto(Cmd) \
86 (Exiting(Cmd) || (bc && bc->current == NULL))
87
88 /* Read the contents of a text file into memory,
89 * dynamically allocating enough space to hold it all */
ReadFileContents(FILE * InputFile,TCHAR * Buffer)90 static LPTSTR ReadFileContents(FILE *InputFile, TCHAR *Buffer)
91 {
92 SIZE_T Len = 0;
93 SIZE_T AllocLen = 1000;
94
95 LPTSTR Contents = cmd_alloc(AllocLen * sizeof(TCHAR));
96 if (!Contents)
97 {
98 WARN("Cannot allocate memory for Contents!\n");
99 return NULL;
100 }
101
102 while (_fgetts(Buffer, CMDLINE_LENGTH, InputFile))
103 {
104 ULONG_PTR CharsRead = _tcslen(Buffer);
105 while (Len + CharsRead >= AllocLen)
106 {
107 LPTSTR OldContents = Contents;
108 Contents = cmd_realloc(Contents, (AllocLen *= 2) * sizeof(TCHAR));
109 if (!Contents)
110 {
111 WARN("Cannot reallocate memory for Contents!\n");
112 cmd_free(OldContents);
113 return NULL;
114 }
115 }
116 _tcscpy(&Contents[Len], Buffer);
117 Len += CharsRead;
118 }
119
120 Contents[Len] = _T('\0');
121 return Contents;
122 }
123
124 /* FOR /F: Parse the contents of each file */
ForF(PARSED_COMMAND * Cmd,LPTSTR List,TCHAR * Buffer)125 static INT ForF(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer)
126 {
127 LPTSTR Delims = _T(" \t");
128 PTCHAR DelimsEndPtr = NULL;
129 TCHAR DelimsEndChr = _T('\0');
130 TCHAR Eol = _T(';');
131 INT SkipLines = 0;
132 DWORD TokensMask = (1 << 1);
133 #ifdef MSCMD_FOR_QUIRKS
134 DWORD NumTokens = 1;
135 DWORD RemainderVar = 0;
136 #else
137 DWORD NumTokens = 0;
138 #endif
139 TCHAR StringQuote = _T('"');
140 TCHAR CommandQuote = _T('\'');
141 LPTSTR Variables[32];
142 PTCHAR Start, End;
143 INT Ret = 0;
144
145 if (Cmd->For.Params)
146 {
147 TCHAR Quote = 0;
148 PTCHAR Param = Cmd->For.Params;
149 if (*Param == _T('"') || *Param == _T('\''))
150 Quote = *Param++;
151
152 while (*Param && *Param != Quote)
153 {
154 if (*Param <= _T(' '))
155 {
156 Param++;
157 }
158 else if (_tcsnicmp(Param, _T("delims="), 7) == 0)
159 {
160 Param += 7;
161 /*
162 * delims=xxx: Specifies the list of characters that separate tokens.
163 * This option does not cumulate: only the latest 'delims=' specification
164 * is taken into account.
165 */
166 Delims = Param;
167 DelimsEndPtr = NULL;
168 while (*Param && *Param != Quote)
169 {
170 if (*Param == _T(' '))
171 {
172 PTCHAR FirstSpace = Param;
173 Param += _tcsspn(Param, _T(" "));
174 /* Exclude trailing spaces if this is not the last parameter */
175 if (*Param && *Param != Quote)
176 {
177 /* Save where the delimiters specification string ends */
178 DelimsEndPtr = FirstSpace;
179 }
180 break;
181 }
182 Param++;
183 }
184 if (*Param == Quote)
185 {
186 /* Save where the delimiters specification string ends */
187 DelimsEndPtr = Param++;
188 }
189 }
190 else if (_tcsnicmp(Param, _T("eol="), 4) == 0)
191 {
192 Param += 4;
193 /* eol=c: Lines starting with this character (may be
194 * preceded by delimiters) are skipped. */
195 Eol = *Param;
196 if (Eol != _T('\0'))
197 Param++;
198 }
199 else if (_tcsnicmp(Param, _T("skip="), 5) == 0)
200 {
201 /* skip=n: Number of lines to skip at the beginning of each file */
202 SkipLines = _tcstol(Param + 5, &Param, 0);
203 if (SkipLines <= 0)
204 goto error;
205 }
206 else if (_tcsnicmp(Param, _T("tokens="), 7) == 0)
207 {
208 #ifdef MSCMD_FOR_QUIRKS
209 DWORD NumToksInSpec = 0; // Number of tokens in this specification.
210 #endif
211 Param += 7;
212 /*
213 * tokens=x,y,m-n: List of token numbers (must be between 1 and 31)
214 * that will be assigned into variables. This option does not cumulate:
215 * only the latest 'tokens=' specification is taken into account.
216 *
217 * NOTE: In MSCMD_FOR_QUIRKS mode, for Windows' CMD compatibility,
218 * not all the tokens-state is reset. This leads to subtle bugs.
219 */
220 TokensMask = 0;
221 #ifdef MSCMD_FOR_QUIRKS
222 NumToksInSpec = 0;
223 // Windows' CMD compatibility: bug: the asterisk-token's position is not reset!
224 // RemainderVar = 0;
225 #else
226 NumTokens = 0;
227 #endif
228
229 while (*Param && *Param != Quote && *Param != _T('*'))
230 {
231 INT First = _tcstol(Param, &Param, 0);
232 INT Last = First;
233 #ifdef MSCMD_FOR_QUIRKS
234 if (First < 1)
235 #else
236 if ((First < 1) || (First > 31))
237 #endif
238 goto error;
239 if (*Param == _T('-'))
240 {
241 /* It's a range of tokens */
242 Last = _tcstol(Param + 1, &Param, 0);
243 #ifdef MSCMD_FOR_QUIRKS
244 /* Ignore the range if the endpoints are not in correct order */
245 if (Last < 1)
246 #else
247 if ((Last < First) || (Last > 31))
248 #endif
249 goto error;
250 }
251 #ifdef MSCMD_FOR_QUIRKS
252 /* Ignore the range if the endpoints are not in correct order */
253 if ((First <= Last) && (Last <= 31))
254 {
255 #endif
256 TokensMask |= (2 << Last) - (1 << First);
257 #ifdef MSCMD_FOR_QUIRKS
258 NumToksInSpec += (Last - First + 1);
259 }
260 #endif
261
262 if (*Param != _T(','))
263 break;
264 Param++;
265 }
266 /* With an asterisk at the end, an additional variable
267 * will be created to hold the remainder of the line
268 * (after the last specified token). */
269 if (*Param == _T('*'))
270 {
271 #ifdef MSCMD_FOR_QUIRKS
272 RemainderVar = ++NumToksInSpec;
273 #else
274 ++NumTokens;
275 #endif
276 Param++;
277 }
278 #ifdef MSCMD_FOR_QUIRKS
279 NumTokens = max(NumTokens, NumToksInSpec);
280 #endif
281 }
282 else if (_tcsnicmp(Param, _T("useback"), 7) == 0)
283 {
284 Param += 7;
285 /* usebackq: Use alternate quote characters */
286 StringQuote = _T('\'');
287 CommandQuote = _T('`');
288 /* Can be written as either "useback" or "usebackq" */
289 if (_totlower(*Param) == _T('q'))
290 Param++;
291 }
292 else
293 {
294 error:
295 error_syntax(Param);
296 return 1;
297 }
298 }
299 }
300
301 #ifdef MSCMD_FOR_QUIRKS
302 /* Windows' CMD compatibility: use the wrongly evaluated number of tokens */
303 fc->varcount = NumTokens;
304 /* Allocate a large enough variables array if needed */
305 if (NumTokens <= ARRAYSIZE(Variables))
306 {
307 fc->values = Variables;
308 }
309 else
310 {
311 fc->values = cmd_alloc(fc->varcount * sizeof(*fc->values));
312 if (!fc->values)
313 {
314 error_out_of_memory();
315 return 1;
316 }
317 }
318 #else
319 /* Count how many variables will be set: one for each token,
320 * plus maybe one for the remainder. */
321 fc->varcount = NumTokens;
322 for (NumTokens = 1; NumTokens < 32; ++NumTokens)
323 fc->varcount += (TokensMask >> NumTokens) & 1;
324 fc->values = Variables;
325 #endif
326
327 if (*List == StringQuote || *List == CommandQuote)
328 {
329 /* Treat the entire "list" as one single element */
330 Start = List;
331 End = &List[_tcslen(List)];
332 goto single_element;
333 }
334
335 /* Loop over each file */
336 End = List;
337 while (!ExitingOrGoto(Cmd) && GetNextElement(&Start, &End))
338 {
339 FILE* InputFile;
340 LPTSTR FullInput, In, NextLine;
341 INT Skip;
342 single_element:
343
344 if (*Start == StringQuote && End[-1] == StringQuote)
345 {
346 /* Input given directly as a string */
347 End[-1] = _T('\0');
348 FullInput = cmd_dup(Start + 1);
349 }
350 else if (*Start == CommandQuote && End[-1] == CommandQuote)
351 {
352 /*
353 * Read input from a command. We let the CRT do the ANSI/UNICODE conversion.
354 * NOTE: Should we do that, or instead read in binary mode and
355 * do the conversion by ourselves, using *OUR* current codepage??
356 */
357 End[-1] = _T('\0');
358 InputFile = _tpopen(Start + 1, _T("r"));
359 if (!InputFile)
360 {
361 error_bad_command(Start + 1);
362 Ret = 1;
363 goto Quit;
364 }
365 FullInput = ReadFileContents(InputFile, Buffer);
366 _pclose(InputFile);
367 }
368 else
369 {
370 /* Read input from a file */
371 TCHAR Temp = *End;
372 *End = _T('\0');
373 StripQuotes(Start);
374 InputFile = _tfopen(Start, _T("r"));
375 *End = Temp;
376 if (!InputFile)
377 {
378 error_sfile_not_found(Start);
379 Ret = 1;
380 goto Quit;
381 }
382 FullInput = ReadFileContents(InputFile, Buffer);
383 fclose(InputFile);
384 }
385
386 if (!FullInput)
387 {
388 error_out_of_memory();
389 Ret = 1;
390 goto Quit;
391 }
392
393 /* Patch the delimiters string */
394 if (DelimsEndPtr)
395 {
396 DelimsEndChr = *DelimsEndPtr;
397 *DelimsEndPtr = _T('\0');
398 }
399
400 /* Loop over the input line by line */
401 for (In = FullInput, Skip = SkipLines;
402 !ExitingOrGoto(Cmd) && (In != NULL);
403 In = NextLine)
404 {
405 DWORD RemainingTokens = TokensMask;
406 LPTSTR* CurVar = fc->values;
407
408 ZeroMemory(fc->values, fc->varcount * sizeof(*fc->values));
409 #ifdef MSCMD_FOR_QUIRKS
410 NumTokens = fc->varcount;
411 #endif
412
413 NextLine = _tcschr(In, _T('\n'));
414 if (NextLine)
415 *NextLine++ = _T('\0');
416
417 if (--Skip >= 0)
418 continue;
419
420 /* Ignore lines where the first token starts with the eol character */
421 In += _tcsspn(In, Delims);
422 if (*In == Eol)
423 continue;
424
425 /* Loop as long as we have not reached the end of
426 * the line, and that we have tokens available.
427 * A maximum of 31 tokens will be enumerated. */
428 while (*In && ((RemainingTokens >>= 1) != 0))
429 {
430 /* Save pointer to this token in a variable if requested */
431 if (RemainingTokens & 1)
432 {
433 #ifdef MSCMD_FOR_QUIRKS
434 --NumTokens;
435 #endif
436 *CurVar++ = In;
437 }
438 /* Find end of token */
439 In += _tcscspn(In, Delims);
440 /* NULL-terminate it and advance to next token */
441 if (*In)
442 {
443 *In++ = _T('\0');
444 In += _tcsspn(In, Delims);
445 }
446 }
447
448 /* Save pointer to remainder of the line if we need to do so */
449 if (*In)
450 #ifdef MSCMD_FOR_QUIRKS
451 if (RemainderVar && (fc->varcount - NumTokens + 1 == RemainderVar))
452 #endif
453 {
454 /* NOTE: This sets fc->values[0] at least, if no tokens
455 * were initialized so far, since CurVar is initialized
456 * originally to point to fc->values. */
457 *CurVar = In;
458 }
459
460 /* Don't run unless we have at least one variable filled */
461 if (fc->values[0])
462 Ret = RunInstance(Cmd);
463 }
464
465 /* Restore the delimiters string */
466 if (DelimsEndPtr)
467 *DelimsEndPtr = DelimsEndChr;
468
469 cmd_free(FullInput);
470 }
471
472 Quit:
473 #ifdef MSCMD_FOR_QUIRKS
474 if (fc->values && (fc->values != Variables))
475 cmd_free(fc->values);
476 #endif
477
478 return Ret;
479 }
480
481 /* FOR /L: Do a numeric loop */
ForLoop(PARSED_COMMAND * Cmd,LPTSTR List,TCHAR * Buffer)482 static INT ForLoop(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer)
483 {
484 enum { START, STEP, END };
485 INT params[3] = { 0, 0, 0 };
486 INT i;
487 INT Ret = 0;
488 TCHAR *Start, *End = List;
489
490 for (i = 0; i < 3 && GetNextElement(&Start, &End); ++i)
491 params[i] = _tcstol(Start, NULL, 0);
492
493 i = params[START];
494 /*
495 * Windows' CMD compatibility:
496 * Contrary to the other FOR-loops, FOR /L does not check
497 * whether a GOTO has been done, and will continue to loop.
498 */
499 while (!Exiting(Cmd) &&
500 (params[STEP] >= 0 ? (i <= params[END]) : (i >= params[END])))
501 {
502 _itot(i, Buffer, 10);
503 Ret = RunInstance(Cmd);
504 i += params[STEP];
505 }
506
507 return Ret;
508 }
509
510 /* Process a FOR in one directory. Stored in Buffer (up to BufPos) is a
511 * string which is prefixed to each element of the list. In a normal FOR
512 * it will be empty, but in FOR /R it will be the directory name. */
ForDir(PARSED_COMMAND * Cmd,LPTSTR List,TCHAR * Buffer,TCHAR * BufPos)513 static INT ForDir(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer, TCHAR *BufPos)
514 {
515 INT Ret = 0;
516 TCHAR *Start, *End = List;
517
518 while (!ExitingOrGoto(Cmd) && GetNextElement(&Start, &End))
519 {
520 if (BufPos + (End - Start) > &Buffer[CMDLINE_LENGTH])
521 continue;
522 memcpy(BufPos, Start, (End - Start) * sizeof(TCHAR));
523 BufPos[End - Start] = _T('\0');
524
525 if (_tcschr(BufPos, _T('?')) || _tcschr(BufPos, _T('*')))
526 {
527 WIN32_FIND_DATA w32fd;
528 HANDLE hFind;
529 TCHAR *FilePart;
530
531 StripQuotes(BufPos);
532 hFind = FindFirstFile(Buffer, &w32fd);
533 if (hFind == INVALID_HANDLE_VALUE)
534 continue;
535 FilePart = _tcsrchr(BufPos, _T('\\'));
536 FilePart = FilePart ? FilePart + 1 : BufPos;
537 do
538 {
539 if (w32fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
540 continue;
541 if (!(w32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
542 != !(Cmd->For.Switches & FOR_DIRS))
543 continue;
544 if (_tcscmp(w32fd.cFileName, _T(".")) == 0 ||
545 _tcscmp(w32fd.cFileName, _T("..")) == 0)
546 continue;
547 _tcscpy(FilePart, w32fd.cFileName);
548 Ret = RunInstance(Cmd);
549 } while (!ExitingOrGoto(Cmd) && FindNextFile(hFind, &w32fd));
550 FindClose(hFind);
551 }
552 else
553 {
554 Ret = RunInstance(Cmd);
555 }
556 }
557 return Ret;
558 }
559
560 /* FOR /R: Process a FOR in each directory of a tree, recursively */
ForRecursive(PARSED_COMMAND * Cmd,LPTSTR List,TCHAR * Buffer,TCHAR * BufPos)561 static INT ForRecursive(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer, TCHAR *BufPos)
562 {
563 INT Ret = 0;
564 HANDLE hFind;
565 WIN32_FIND_DATA w32fd;
566
567 if (BufPos[-1] != _T('\\'))
568 {
569 *BufPos++ = _T('\\');
570 *BufPos = _T('\0');
571 }
572
573 Ret = ForDir(Cmd, List, Buffer, BufPos);
574
575 /* NOTE (We don't apply Windows' CMD compatibility here):
576 * Windows' CMD does not check whether a GOTO has been done,
577 * and will continue to loop. */
578 if (ExitingOrGoto(Cmd))
579 return Ret;
580
581 _tcscpy(BufPos, _T("*"));
582 hFind = FindFirstFile(Buffer, &w32fd);
583 if (hFind == INVALID_HANDLE_VALUE)
584 return Ret;
585 do
586 {
587 if (!(w32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
588 continue;
589 if (_tcscmp(w32fd.cFileName, _T(".")) == 0 ||
590 _tcscmp(w32fd.cFileName, _T("..")) == 0)
591 continue;
592 Ret = ForRecursive(Cmd, List, Buffer, _stpcpy(BufPos, w32fd.cFileName));
593
594 /* NOTE (We don't apply Windows' CMD compatibility here):
595 * Windows' CMD does not check whether a GOTO has been done,
596 * and will continue to loop. */
597 } while (!ExitingOrGoto(Cmd) && FindNextFile(hFind, &w32fd));
598 FindClose(hFind);
599
600 return Ret;
601 }
602
603 INT
ExecuteFor(PARSED_COMMAND * Cmd)604 ExecuteFor(PARSED_COMMAND *Cmd)
605 {
606 INT Ret;
607 LPTSTR List;
608 PFOR_CONTEXT lpNew;
609 TCHAR Buffer[CMDLINE_LENGTH]; /* Buffer to hold the variable value */
610 LPTSTR BufferPtr = Buffer;
611
612 List = DoDelayedExpansion(Cmd->For.List);
613 if (!List)
614 return 1;
615
616 /* Create our FOR context */
617 lpNew = cmd_alloc(sizeof(FOR_CONTEXT));
618 if (!lpNew)
619 {
620 WARN("Cannot allocate memory for lpNew!\n");
621 cmd_free(List);
622 return 1;
623 }
624 lpNew->prev = fc;
625 lpNew->firstvar = Cmd->For.Variable;
626 lpNew->varcount = 1;
627 lpNew->values = &BufferPtr;
628
629 Cmd->For.Context = lpNew;
630 fc = lpNew;
631
632 /* Run the extended FOR syntax only if extensions are enabled */
633 if (bEnableExtensions)
634 {
635 if (Cmd->For.Switches & FOR_F)
636 {
637 Ret = ForF(Cmd, List, Buffer);
638 }
639 else if (Cmd->For.Switches & FOR_LOOP)
640 {
641 Ret = ForLoop(Cmd, List, Buffer);
642 }
643 else if (Cmd->For.Switches & FOR_RECURSIVE)
644 {
645 DWORD Len = GetFullPathName(Cmd->For.Params ? Cmd->For.Params : _T("."),
646 MAX_PATH, Buffer, NULL);
647 Ret = ForRecursive(Cmd, List, Buffer, &Buffer[Len]);
648 }
649 else
650 {
651 Ret = ForDir(Cmd, List, Buffer, Buffer);
652 }
653 }
654 else
655 {
656 Ret = ForDir(Cmd, List, Buffer, Buffer);
657 }
658
659 /* Remove our context, unless someone already did that */
660 if (fc == lpNew)
661 fc = lpNew->prev;
662
663 cmd_free(lpNew);
664 cmd_free(List);
665 return Ret;
666 }
667
668 /* EOF */
669