xref: /reactos/base/shell/cmd/batch.c (revision 171a9206)
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