1 /* 2 * BATCH.C - batch file processor for CMD.EXE. 3 * 4 * 5 * History: 6 * 7 * ??/??/?? (Evan Jeffrey) 8 * started. 9 * 10 * 15 Jul 1995 (Tim Norman) 11 * modes and bugfixes. 12 * 13 * 08 Aug 1995 (Matt Rains) 14 * i have cleaned up the source code. changes now bring this 15 * source into guidelines for recommended programming practice. 16 * 17 * i have added some constants to help making changes easier. 18 * 19 * 29 Jan 1996 (Steffan Kaiser) 20 * made a few cosmetic changes 21 * 22 * 05 Feb 1996 (Tim Norman) 23 * changed to comply with new first/rest calling scheme 24 * 25 * 14 Jun 1997 (Steffen Kaiser) 26 * bug fixes. added error level expansion %?. ctrl-break handling 27 * 28 * 16 Jul 1998 (Hans B Pufal) 29 * Totally reorganised in conjunction with COMMAND.C (cf) to 30 * implement proper BATCH file nesting and other improvements. 31 * 32 * 16 Jul 1998 (John P Price <linux-guru@gcfl.net>) 33 * Separated commands into individual files. 34 * 35 * 19 Jul 1998 (Hans B Pufal) [HBP_001] 36 * Preserve state of echo flag across batch calls. 37 * 38 * 19 Jul 1998 (Hans B Pufal) [HBP_002] 39 * Implementation of FOR command 40 * 41 * 20-Jul-1998 (John P Price <linux-guru@gcfl.net>) 42 * added error checking after cmd_alloc calls 43 * 44 * 27-Jul-1998 (John P Price <linux-guru@gcfl.net>) 45 * added config.h include 46 * 47 * 02-Aug-1998 (Hans B Pufal) [HBP_003] 48 * Fixed bug in ECHO flag restoration at exit from batch file 49 * 50 * 26-Jan-1999 Eric Kohl 51 * Replaced CRT io functions by Win32 io functions. 52 * Unicode safe! 53 * 54 * 23-Feb-2001 (Carl Nettelblad <cnettel@hem.passagen.es>) 55 * Fixes made to get "for" working. 56 * 57 * 02-Apr-2005 (Magnus Olsen <magnus@greatlord.com>) 58 * Remove all hardcoded strings in En.rc 59 */ 60 61 #include "precomp.h" 62 63 /* The stack of current batch contexts. 64 * NULL when no batch is active. 65 */ 66 BATCH_TYPE BatType = NONE; 67 PBATCH_CONTEXT bc = NULL; 68 69 #ifdef MSCMD_BATCH_ECHO 70 BOOL bBcEcho = TRUE; 71 #endif 72 73 BOOL bEcho = TRUE; /* The echo flag */ 74 75 /* Buffer for reading Batch file lines */ 76 TCHAR textline[BATCH_BUFFSIZE]; 77 78 /* 79 * Returns a pointer to the n'th parameter of the current batch file. 80 * If no such parameter exists returns pointer to empty string. 81 * If no batch file is current, returns NULL. 82 */ 83 BOOL 84 FindArg( 85 IN TCHAR Char, 86 OUT PCTSTR* ArgPtr, 87 OUT BOOL* IsParam0) 88 { 89 PCTSTR pp; 90 INT n = Char - _T('0'); 91 92 TRACE("FindArg: (%d)\n", n); 93 94 *ArgPtr = NULL; 95 96 if (n < 0 || n > 9) 97 return FALSE; 98 99 n = bc->shiftlevel[n]; 100 *IsParam0 = (n == 0); 101 pp = bc->params; 102 103 /* Step up the strings till we reach 104 * the end or the one we want. */ 105 while (*pp && n--) 106 pp += _tcslen(pp) + 1; 107 108 *ArgPtr = pp; 109 return TRUE; 110 } 111 112 113 /* 114 * Builds the batch parameter list in newly allocated memory. 115 * The parameters consist of NULL terminated strings with a 116 * final NULL character signalling the end of the parameters. 117 */ 118 static BOOL 119 BatchParams( 120 IN PCTSTR Arg0, 121 IN PCTSTR Args, 122 OUT PTSTR* RawParams, 123 OUT PTSTR* ParamList) 124 { 125 PTSTR dp; 126 SIZE_T len; 127 128 *RawParams = NULL; 129 *ParamList = NULL; 130 131 /* Make a raw copy of the parameters, but trim any leading and trailing whitespace */ 132 // Args += _tcsspn(Args, _T(" \t")); 133 while (_istspace(*Args)) 134 ++Args; 135 dp = (PTSTR)Args + _tcslen(Args); 136 while ((dp > Args) && _istspace(*(dp - 1))) 137 --dp; 138 len = dp - Args; 139 *RawParams = (PTSTR)cmd_alloc((len + 1)* sizeof(TCHAR)); 140 if (!*RawParams) 141 { 142 WARN("Cannot allocate memory for RawParams!\n"); 143 error_out_of_memory(); 144 return FALSE; 145 } 146 _tcsncpy(*RawParams, Args, len); 147 (*RawParams)[len] = _T('\0'); 148 149 /* Parse the parameters as well */ 150 Args = *RawParams; 151 152 *ParamList = (PTSTR)cmd_alloc((_tcslen(Arg0) + _tcslen(Args) + 3) * sizeof(TCHAR)); 153 if (!*ParamList) 154 { 155 WARN("Cannot allocate memory for ParamList!\n"); 156 error_out_of_memory(); 157 cmd_free(*RawParams); 158 *RawParams = NULL; 159 return FALSE; 160 } 161 162 dp = *ParamList; 163 164 if (Arg0 && *Arg0) 165 { 166 dp = _stpcpy(dp, Arg0); 167 *dp++ = _T('\0'); 168 } 169 170 while (*Args) 171 { 172 BOOL inquotes = FALSE; 173 174 /* Find next parameter */ 175 while (_istspace(*Args) || (*Args && _tcschr(STANDARD_SEPS, *Args))) 176 ++Args; 177 if (!*Args) 178 break; 179 180 /* Copy it */ 181 do 182 { 183 if (!inquotes && (_istspace(*Args) || _tcschr(STANDARD_SEPS, *Args))) 184 break; 185 inquotes ^= (*Args == _T('"')); 186 *dp++ = *Args++; 187 } while (*Args); 188 *dp++ = _T('\0'); 189 } 190 *dp = _T('\0'); 191 192 return TRUE; 193 } 194 195 /* 196 * Free the allocated memory of a batch file. 197 */ 198 static VOID ClearBatch(VOID) 199 { 200 TRACE("ClearBatch mem = %08x ; free = %d\n", bc->mem, bc->memfree); 201 202 if (bc->mem && bc->memfree) 203 cmd_free(bc->mem); 204 205 if (bc->raw_params) 206 cmd_free(bc->raw_params); 207 208 if (bc->params) 209 cmd_free(bc->params); 210 } 211 212 /* 213 * If a batch file is current, exits it, freeing the context block and 214 * chaining back to the previous one. 215 * 216 * If no new batch context is found, sets ECHO back ON. 217 * 218 * If the parameter is non-null or not empty, it is printed as an exit 219 * message 220 */ 221 222 VOID ExitBatch(VOID) 223 { 224 ClearBatch(); 225 226 TRACE("ExitBatch\n"); 227 228 UndoRedirection(bc->RedirList, NULL); 229 FreeRedirection(bc->RedirList); 230 231 #ifndef MSCMD_BATCH_ECHO 232 /* Preserve echo state across batch calls */ 233 bEcho = bc->bEcho; 234 #endif 235 236 while (bc->setlocal) 237 cmd_endlocal(_T("")); 238 239 bc = bc->prev; 240 241 #if 0 242 /* Do not process any more parts of a compound command */ 243 bc->current = NULL; 244 #endif 245 246 /* If there is no more batch contexts, notify the signal handler */ 247 if (!bc) 248 { 249 CheckCtrlBreak(BREAK_OUTOFBATCH); 250 BatType = NONE; 251 252 #ifdef MSCMD_BATCH_ECHO 253 bEcho = bBcEcho; 254 #endif 255 } 256 } 257 258 /* 259 * Exit all the nested batch calls. 260 */ 261 VOID ExitAllBatches(VOID) 262 { 263 while (bc) 264 ExitBatch(); 265 } 266 267 /* 268 * Load batch file into memory. 269 */ 270 static void BatchFile2Mem(HANDLE hBatchFile) 271 { 272 TRACE("BatchFile2Mem()\n"); 273 274 bc->memsize = GetFileSize(hBatchFile, NULL); 275 bc->mem = (char *)cmd_alloc(bc->memsize+1); /* 1 extra for '\0' */ 276 277 /* if memory is available, read it in and close the file */ 278 if (bc->mem != NULL) 279 { 280 TRACE ("BatchFile2Mem memory %08x - %08x\n",bc->mem,bc->memsize); 281 SetFilePointer (hBatchFile, 0, NULL, FILE_BEGIN); 282 ReadFile(hBatchFile, (LPVOID)bc->mem, bc->memsize, &bc->memsize, NULL); 283 bc->mem[bc->memsize]='\0'; /* end this, so you can dump it as a string */ 284 bc->memfree=TRUE; /* this one needs to be freed */ 285 } 286 else 287 { 288 bc->memsize=0; /* this will prevent mem being accessed */ 289 bc->memfree=FALSE; 290 } 291 bc->mempos = 0; /* set position to the start */ 292 } 293 294 /* 295 * Start batch file execution. 296 * 297 * The firstword parameter is the full filename of the batch file. 298 */ 299 INT Batch(LPTSTR fullname, LPTSTR firstword, LPTSTR param, PARSED_COMMAND *Cmd) 300 { 301 INT ret = 0; 302 INT i; 303 HANDLE hFile = NULL; 304 BOOLEAN bSameFn = FALSE; 305 BOOLEAN bTopLevel; 306 BATCH_CONTEXT new; 307 PFOR_CONTEXT saved_fc; 308 309 SetLastError(0); 310 if (bc && bc->mem) 311 { 312 TCHAR fpname[MAX_PATH]; 313 GetFullPathName(fullname, ARRAYSIZE(fpname), fpname, NULL); 314 if (_tcsicmp(bc->BatchFilePath, fpname) == 0) 315 bSameFn = TRUE; 316 } 317 TRACE("Batch(\'%s\', \'%s\', \'%s\') bSameFn = %d\n", 318 debugstr_aw(fullname), debugstr_aw(firstword), debugstr_aw(param), bSameFn); 319 320 if (!bSameFn) 321 { 322 hFile = CreateFile(fullname, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, 323 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | 324 FILE_FLAG_SEQUENTIAL_SCAN, NULL); 325 326 if (hFile == INVALID_HANDLE_VALUE) 327 { 328 ConErrResPuts(STRING_BATCH_ERROR); 329 return 1; 330 } 331 } 332 333 /* 334 * Remember whether this is a top-level batch context, i.e. if there is 335 * no batch context existing prior (bc == NULL originally), and we are 336 * going to create one below. 337 */ 338 bTopLevel = !bc; 339 340 if (bc != NULL && Cmd == bc->current) 341 { 342 /* Then we are transferring to another batch */ 343 ClearBatch(); 344 AddBatchRedirection(&Cmd->Redirections); 345 } 346 else 347 { 348 struct _SETLOCAL *setlocal = NULL; 349 350 if (Cmd == NULL) 351 { 352 /* This is a CALL. CALL will set errorlevel to our return value, so 353 * in order to keep the value of errorlevel unchanged in the case 354 * of calling an empty batch file, we must return that same value. */ 355 ret = nErrorLevel; 356 } 357 else if (bc) 358 { 359 /* If a batch file runs another batch file as part of a compound command 360 * (e.g. "x.bat & somethingelse") then the first file gets terminated. */ 361 362 /* Get its SETLOCAL stack so it can be migrated to the new context */ 363 setlocal = bc->setlocal; 364 bc->setlocal = NULL; 365 ExitBatch(); 366 } 367 368 /* Create a new context. This function will not 369 * return until this context has been exited */ 370 new.prev = bc; 371 /* copy some fields in the new structure if it is the same file */ 372 if (bSameFn) 373 { 374 new.mem = bc->mem; 375 new.memsize = bc->memsize; 376 new.mempos = 0; 377 new.memfree = FALSE; /* don't free this, being used before this */ 378 } 379 bc = &new; 380 bc->RedirList = NULL; 381 bc->setlocal = setlocal; 382 } 383 384 GetFullPathName(fullname, ARRAYSIZE(bc->BatchFilePath), bc->BatchFilePath, NULL); 385 386 /* If a new batch file, load it into memory and close the file */ 387 if (!bSameFn) 388 { 389 BatchFile2Mem(hFile); 390 CloseHandle(hFile); 391 } 392 393 bc->mempos = 0; /* Go to the beginning of the batch file */ 394 #ifndef MSCMD_BATCH_ECHO 395 bc->bEcho = bEcho; /* Preserve echo across batch calls */ 396 #endif 397 for (i = 0; i < 10; i++) 398 bc->shiftlevel[i] = i; 399 400 /* Parse the batch parameters */ 401 if (!BatchParams(firstword, param, &bc->raw_params, &bc->params)) 402 return 1; 403 404 /* If we are calling from inside a FOR, hide the FOR variables */ 405 saved_fc = fc; 406 fc = NULL; 407 408 /* Perform top-level batch initialization */ 409 if (bTopLevel) 410 { 411 TCHAR *dot; 412 413 /* Default the top-level batch context type to .BAT */ 414 BatType = BAT_TYPE; 415 416 /* If this is a .CMD file, adjust the type */ 417 dot = _tcsrchr(bc->BatchFilePath, _T('.')); 418 if (dot && (!_tcsicmp(dot, _T(".cmd")))) 419 { 420 BatType = CMD_TYPE; 421 } 422 423 #ifdef MSCMD_BATCH_ECHO 424 bBcEcho = bEcho; 425 #endif 426 } 427 428 /* If this is a "CALL :label args ...", call a subroutine of 429 * the current batch file, only if extensions are enabled. */ 430 if (bEnableExtensions && (*firstword == _T(':'))) 431 { 432 LPTSTR expLabel; 433 434 /* Position at the place of the parent file (which is the same as the caller) */ 435 bc->mempos = (bc->prev ? bc->prev->mempos : 0); 436 437 /* 438 * Jump to the label. Strip the label's colon; as a side-effect 439 * this will forbid "CALL :EOF"; however "CALL ::EOF" will work! 440 */ 441 bc->current = Cmd; 442 ++firstword; 443 444 /* Expand the label only! (simulate a GOTO command as in Windows' CMD) */ 445 expLabel = DoDelayedExpansion(firstword); 446 ret = cmd_goto(expLabel ? expLabel : firstword); 447 if (expLabel) 448 cmd_free(expLabel); 449 } 450 451 /* If we have created a new context, don't return 452 * until this batch file has completed. */ 453 while (bc == &new && !bExit) 454 { 455 Cmd = ParseCommand(NULL); 456 if (!Cmd) 457 { 458 if (!bParseError) 459 continue; 460 461 /* Echo the pre-parsed batch file line on error */ 462 if (bEcho && !bDisableBatchEcho) 463 { 464 if (!bIgnoreEcho) 465 ConOutChar(_T('\n')); 466 PrintPrompt(); 467 ConOutPuts(ParseLine); 468 ConOutChar(_T('\n')); 469 } 470 /* Stop all execution */ 471 ExitAllBatches(); 472 ret = 1; 473 break; 474 } 475 476 /* JPP 19980807 */ 477 /* Echo the command and execute it */ 478 bc->current = Cmd; 479 ret = ExecuteCommandWithEcho(Cmd); 480 FreeCommand(Cmd); 481 } 482 if (bExit) 483 { 484 /* Stop all execution */ 485 ExitAllBatches(); 486 } 487 488 /* Perform top-level batch cleanup */ 489 if (!bc || bTopLevel) 490 { 491 /* Reset the top-level batch context type */ 492 BatType = NONE; 493 494 #ifdef MSCMD_BATCH_ECHO 495 bEcho = bBcEcho; 496 #endif 497 } 498 499 /* Restore the FOR variables */ 500 fc = saved_fc; 501 502 /* Always return the last command's return code */ 503 TRACE("Batch: returns %d\n", ret); 504 return ret; 505 } 506 507 VOID AddBatchRedirection(REDIRECTION **RedirList) 508 { 509 REDIRECTION **ListEnd; 510 511 /* Prepend the list to the batch context's list */ 512 ListEnd = RedirList; 513 while (*ListEnd) 514 ListEnd = &(*ListEnd)->Next; 515 *ListEnd = bc->RedirList; 516 bc->RedirList = *RedirList; 517 518 /* Null out the pointer so that the list will not be cleared prematurely. 519 * These redirections should persist until the batch file exits. */ 520 *RedirList = NULL; 521 } 522 523 /* 524 * Read a single line from the batch file from the current batch/memory position. 525 * Almost a copy of FileGetString with same UNICODE handling 526 */ 527 BOOL BatchGetString(LPTSTR lpBuffer, INT nBufferLength) 528 { 529 INT len = 0; 530 531 /* read all chars from memory until a '\n' is encountered */ 532 if (bc->mem) 533 { 534 for (; ((bc->mempos + len) < bc->memsize && len < (nBufferLength-1)); len++) 535 { 536 #ifndef _UNICODE 537 lpBuffer[len] = bc->mem[bc->mempos + len]; 538 #endif 539 if (bc->mem[bc->mempos + len] == '\n') 540 { 541 len++; 542 break; 543 } 544 } 545 #ifdef _UNICODE 546 nBufferLength = MultiByteToWideChar(OutputCodePage, 0, &bc->mem[bc->mempos], len, lpBuffer, nBufferLength); 547 lpBuffer[nBufferLength] = L'\0'; 548 lpBuffer[len] = '\0'; 549 #endif 550 bc->mempos += len; 551 } 552 553 return len != 0; 554 } 555 556 /* 557 * Read and return the next executable line form the current batch file 558 * 559 * If no batch file is current or no further executable lines are found 560 * return NULL. 561 * 562 * Set eflag to 0 if line is not to be echoed else 1 563 */ 564 LPTSTR ReadBatchLine(VOID) 565 { 566 TRACE("ReadBatchLine()\n"); 567 568 /* User halt */ 569 if (CheckCtrlBreak(BREAK_BATCHFILE)) 570 { 571 ExitAllBatches(); 572 return NULL; 573 } 574 575 if (!BatchGetString(textline, ARRAYSIZE(textline) - 1)) 576 { 577 TRACE("ReadBatchLine(): Reached EOF!\n"); 578 /* End of file */ 579 ExitBatch(); 580 return NULL; 581 } 582 583 TRACE("ReadBatchLine(): textline: \'%s\'\n", debugstr_aw(textline)); 584 585 #if 1 586 // 587 // FIXME: This is redundant, but keep it for the moment until we correctly 588 // hande the end-of-file situation here, in ReadLine() and in the parser. 589 // (In an EOF, the previous BatchGetString() call will return FALSE but 590 // we want not to run the ExitBatch() at first, but wait later to do it.) 591 // 592 if (textline[_tcslen(textline) - 1] != _T('\n')) 593 _tcscat(textline, _T("\n")); 594 #endif 595 596 return textline; 597 } 598 599 /* EOF */ 600