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 && Cmd == bc->current) 341 { 342 /* Then we are transferring to another batch */ 343 if (!bSameFn) 344 ClearBatch(); 345 AddBatchRedirection(&Cmd->Redirections); 346 } 347 else 348 { 349 struct _SETLOCAL *setlocal = NULL; 350 351 if (Cmd == NULL) 352 { 353 /* This is a CALL. CALL will set errorlevel to our return value, so 354 * in order to keep the value of errorlevel unchanged in the case 355 * of calling an empty batch file, we must return that same value. */ 356 ret = nErrorLevel; 357 } 358 else if (bc) 359 { 360 /* If a batch file runs another batch file as part of a compound command 361 * (e.g. "x.bat & somethingelse") then the first file gets terminated. */ 362 363 /* Get its SETLOCAL stack so it can be migrated to the new context */ 364 setlocal = bc->setlocal; 365 bc->setlocal = NULL; 366 ExitBatch(); 367 } 368 369 /* Create a new context. This function will not 370 * return until this context has been exited */ 371 new.prev = bc; 372 /* copy some fields in the new structure if it is the same file */ 373 if (bSameFn) 374 { 375 new.mem = bc->mem; 376 new.memsize = bc->memsize; 377 new.mempos = 0; 378 new.memfree = FALSE; /* don't free this, being used before this */ 379 } 380 bc = &new; 381 bc->RedirList = NULL; 382 bc->setlocal = setlocal; 383 } 384 385 GetFullPathName(fullname, ARRAYSIZE(bc->BatchFilePath), bc->BatchFilePath, NULL); 386 387 /* If a new batch file, load it into memory and close the file */ 388 if (!bSameFn) 389 { 390 BatchFile2Mem(hFile); 391 CloseHandle(hFile); 392 } 393 394 bc->mempos = 0; /* Go to the beginning of the batch file */ 395 #ifndef MSCMD_BATCH_ECHO 396 bc->bEcho = bEcho; /* Preserve echo across batch calls */ 397 #endif 398 for (i = 0; i < 10; i++) 399 bc->shiftlevel[i] = i; 400 401 /* Parse the batch parameters */ 402 if (!BatchParams(firstword, param, &bc->raw_params, &bc->params)) 403 return 1; 404 405 /* If we are calling from inside a FOR, hide the FOR variables */ 406 saved_fc = fc; 407 fc = NULL; 408 409 /* Perform top-level batch initialization */ 410 if (bTopLevel) 411 { 412 /* Default the top-level batch context type 413 * to .BAT, unless this is a .CMD file */ 414 PTCHAR dotext = _tcsrchr(bc->BatchFilePath, _T('.')); 415 BatType = (dotext && (!_tcsicmp(dotext, _T(".cmd")))) ? CMD_TYPE : BAT_TYPE; 416 417 #ifdef MSCMD_BATCH_ECHO 418 bBcEcho = bEcho; 419 #endif 420 } 421 422 /* If this is a "CALL :label args ...", call a subroutine of 423 * the current batch file, only if extensions are enabled. */ 424 if (bEnableExtensions && (*firstword == _T(':'))) 425 { 426 LPTSTR expLabel; 427 428 /* Position at the place of the parent file (which is the same as the caller) */ 429 bc->mempos = (bc->prev ? bc->prev->mempos : 0); 430 431 /* 432 * Jump to the label. Strip the label's colon; as a side-effect 433 * this will forbid "CALL :EOF"; however "CALL ::EOF" will work! 434 */ 435 bc->current = Cmd; 436 ++firstword; 437 438 /* Expand the label only! (simulate a GOTO command as in Windows' CMD) */ 439 expLabel = DoDelayedExpansion(firstword); 440 ret = cmd_goto(expLabel ? expLabel : firstword); 441 if (expLabel) 442 cmd_free(expLabel); 443 } 444 445 /* If we have created a new context, don't return 446 * until this batch file has completed. */ 447 while (bc == &new && !bExit) 448 { 449 Cmd = ParseCommand(NULL); 450 if (!Cmd) 451 { 452 if (!bParseError) 453 continue; 454 455 /* Echo the pre-parsed batch file line on error */ 456 if (bEcho && !bDisableBatchEcho) 457 { 458 if (!bIgnoreEcho) 459 ConOutChar(_T('\n')); 460 PrintPrompt(); 461 ConOutPuts(ParseLine); 462 ConOutChar(_T('\n')); 463 } 464 /* Stop all execution */ 465 ExitAllBatches(); 466 ret = 1; 467 break; 468 } 469 470 /* JPP 19980807 */ 471 /* Echo the command and execute it */ 472 bc->current = Cmd; 473 ret = ExecuteCommandWithEcho(Cmd); 474 FreeCommand(Cmd); 475 } 476 if (bExit) 477 { 478 /* Stop all execution */ 479 ExitAllBatches(); 480 } 481 482 /* Perform top-level batch cleanup */ 483 if (!bc || bTopLevel) 484 { 485 /* Reset the top-level batch context type */ 486 BatType = NONE; 487 488 #ifdef MSCMD_BATCH_ECHO 489 bEcho = bBcEcho; 490 #endif 491 } 492 493 /* Restore the FOR variables */ 494 fc = saved_fc; 495 496 /* Always return the last command's return code */ 497 TRACE("Batch: returns %d\n", ret); 498 return ret; 499 } 500 501 VOID AddBatchRedirection(REDIRECTION **RedirList) 502 { 503 REDIRECTION **ListEnd; 504 505 /* Prepend the list to the batch context's list */ 506 ListEnd = RedirList; 507 while (*ListEnd) 508 ListEnd = &(*ListEnd)->Next; 509 *ListEnd = bc->RedirList; 510 bc->RedirList = *RedirList; 511 512 /* Null out the pointer so that the list will not be cleared prematurely. 513 * These redirections should persist until the batch file exits. */ 514 *RedirList = NULL; 515 } 516 517 /* 518 * Read a single line from the batch file from the current batch/memory position. 519 * Almost a copy of FileGetString with same UNICODE handling 520 */ 521 BOOL BatchGetString(LPTSTR lpBuffer, INT nBufferLength) 522 { 523 INT len = 0; 524 525 /* read all chars from memory until a '\n' is encountered */ 526 if (bc->mem) 527 { 528 for (; ((bc->mempos + len) < bc->memsize && len < (nBufferLength-1)); len++) 529 { 530 #ifndef _UNICODE 531 lpBuffer[len] = bc->mem[bc->mempos + len]; 532 #endif 533 if (bc->mem[bc->mempos + len] == '\n') 534 { 535 len++; 536 break; 537 } 538 } 539 #ifdef _UNICODE 540 nBufferLength = MultiByteToWideChar(OutputCodePage, 0, &bc->mem[bc->mempos], len, lpBuffer, nBufferLength); 541 lpBuffer[nBufferLength] = L'\0'; 542 lpBuffer[len] = '\0'; 543 #endif 544 bc->mempos += len; 545 } 546 547 return len != 0; 548 } 549 550 /* 551 * Read and return the next executable line form the current batch file 552 * 553 * If no batch file is current or no further executable lines are found 554 * return NULL. 555 * 556 * Set eflag to 0 if line is not to be echoed else 1 557 */ 558 LPTSTR ReadBatchLine(VOID) 559 { 560 TRACE("ReadBatchLine()\n"); 561 562 /* User halt */ 563 if (CheckCtrlBreak(BREAK_BATCHFILE)) 564 { 565 ExitAllBatches(); 566 return NULL; 567 } 568 569 if (!BatchGetString(textline, ARRAYSIZE(textline) - 1)) 570 { 571 TRACE("ReadBatchLine(): Reached EOF!\n"); 572 /* End of file */ 573 ExitBatch(); 574 return NULL; 575 } 576 577 TRACE("ReadBatchLine(): textline: \'%s\'\n", debugstr_aw(textline)); 578 579 #if 1 580 // 581 // FIXME: This is redundant, but keep it for the moment until we correctly 582 // hande the end-of-file situation here, in ReadLine() and in the parser. 583 // (In an EOF, the previous BatchGetString() call will return FALSE but 584 // we want not to run the ExitBatch() at first, but wait later to do it.) 585 // 586 if (textline[_tcslen(textline) - 1] != _T('\n')) 587 _tcscat(textline, _T("\n")); 588 #endif 589 590 return textline; 591 } 592 593 /* EOF */ 594