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
FindArg(IN TCHAR Char,OUT PCTSTR * ArgPtr,OUT BOOL * IsParam0)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
BatchParams(IN PCTSTR Arg0,IN PCTSTR Args,OUT PTSTR * RawParams,OUT PTSTR * ParamList)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 */
ClearBatch(VOID)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
ExitBatch(VOID)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 */
ExitAllBatches(VOID)261 VOID ExitAllBatches(VOID)
262 {
263 while (bc)
264 ExitBatch();
265 }
266
267 /*
268 * Load batch file into memory.
269 */
BatchFile2Mem(HANDLE hBatchFile)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 */
Batch(LPTSTR fullname,LPTSTR firstword,LPTSTR param,PARSED_COMMAND * Cmd)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
AddBatchRedirection(REDIRECTION ** RedirList)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 */
BatchGetString(LPTSTR lpBuffer,INT nBufferLength)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 */
ReadBatchLine(VOID)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