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