xref: /reactos/base/shell/cmd/for.c (revision 53221834)
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 */
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 */
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 */
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 */
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 */
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. */
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 */
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
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