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