xref: /reactos/base/shell/cmd/batch.c (revision 94a413ae)
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 PBATCH_CONTEXT bc = NULL;
67 
68 BOOL bEcho = TRUE;  /* The echo flag */
69 
70 /* Buffer for reading Batch file lines */
71 TCHAR textline[BATCH_BUFFSIZE];
72 
73 
74 /*
75  * Returns a pointer to the n'th parameter of the current batch file.
76  * If no such parameter exists returns pointer to empty string.
77  * If no batch file is current, returns NULL
78  *
79  */
80 LPTSTR FindArg(TCHAR Char, BOOL *IsParam0)
81 {
82     LPTSTR pp;
83     INT n = Char - _T('0');
84 
85     TRACE ("FindArg: (%d)\n", n);
86 
87     if (n < 0 || n > 9)
88         return NULL;
89 
90     n = bc->shiftlevel[n];
91     *IsParam0 = (n == 0);
92     pp = bc->params;
93 
94     /* Step up the strings till we reach the end */
95     /* or the one we want */
96     while (*pp && n--)
97         pp += _tcslen (pp) + 1;
98 
99     return pp;
100 }
101 
102 
103 /*
104  * Batch_params builds a parameter list in newly allocated memory.
105  * The parameters consist of null terminated strings with a final
106  * NULL character signalling the end of the parameters.
107  *
108  */
109 static LPTSTR BatchParams(LPTSTR s1, LPTSTR s2)
110 {
111     LPTSTR dp = (LPTSTR)cmd_alloc((_tcslen(s1) + _tcslen(s2) + 3) * sizeof (TCHAR));
112 
113     /* JPP 20-Jul-1998 added error checking */
114     if (dp == NULL)
115     {
116         WARN("Cannot allocate memory for dp!\n");
117         error_out_of_memory();
118         return NULL;
119     }
120 
121     if (s1 && *s1)
122     {
123         s1 = _stpcpy (dp, s1);
124         *s1++ = _T('\0');
125     }
126     else
127         s1 = dp;
128 
129     while (*s2)
130     {
131         BOOL inquotes = FALSE;
132 
133         /* Find next parameter */
134         while (_istspace(*s2) || (*s2 && _tcschr(STANDARD_SEPS, *s2)))
135             s2++;
136         if (!*s2)
137             break;
138 
139         /* Copy it */
140         do
141         {
142             if (!inquotes && (_istspace(*s2) || _tcschr(STANDARD_SEPS, *s2)))
143                 break;
144             inquotes ^= (*s2 == _T('"'));
145             *s1++ = *s2++;
146         } while (*s2);
147         *s1++ = _T('\0');
148     }
149 
150     *s1 = _T('\0');
151 
152     return dp;
153 }
154 
155 /*
156  * Free the allocated memory of a batch file.
157  */
158 static VOID ClearBatch(VOID)
159 {
160     TRACE("ClearBatch  mem = %08x ; free = %d\n", bc->mem, bc->memfree);
161 
162     if (bc->mem && bc->memfree)
163         cmd_free(bc->mem);
164 
165     if (bc->raw_params)
166         cmd_free(bc->raw_params);
167 
168     if (bc->params)
169         cmd_free(bc->params);
170 }
171 
172 /*
173  * If a batch file is current, exits it, freeing the context block and
174  * chaining back to the previous one.
175  *
176  * If no new batch context is found, sets ECHO back ON.
177  *
178  * If the parameter is non-null or not empty, it is printed as an exit
179  * message
180  */
181 
182 VOID ExitBatch(VOID)
183 {
184     ClearBatch();
185 
186     TRACE("ExitBatch\n");
187 
188     UndoRedirection(bc->RedirList, NULL);
189     FreeRedirection(bc->RedirList);
190 
191     /* Preserve echo state across batch calls */
192     bEcho = bc->bEcho;
193 
194     while (bc->setlocal)
195         cmd_endlocal(_T(""));
196 
197     bc = bc->prev;
198 
199 #if 0
200     /* Do not process any more parts of a compound command */
201     bc->current = NULL;
202 #endif
203 
204     /* If there is no more batch contexts, notify the signal handler */
205     if (!bc)
206         CheckCtrlBreak(BREAK_OUTOFBATCH);
207 }
208 
209 /*
210  * Exit all the nested batch calls.
211  */
212 VOID ExitAllBatches(VOID)
213 {
214     while (bc)
215         ExitBatch();
216 }
217 
218 /*
219  * Load batch file into memory.
220  */
221 static void BatchFile2Mem(HANDLE hBatchFile)
222 {
223     TRACE("BatchFile2Mem()\n");
224 
225     bc->memsize = GetFileSize(hBatchFile, NULL);
226     bc->mem     = (char *)cmd_alloc(bc->memsize+1);     /* 1 extra for '\0' */
227 
228     /* if memory is available, read it in and close the file */
229     if (bc->mem != NULL)
230     {
231         TRACE ("BatchFile2Mem memory %08x - %08x\n",bc->mem,bc->memsize);
232         SetFilePointer (hBatchFile, 0, NULL, FILE_BEGIN);
233         ReadFile(hBatchFile, (LPVOID)bc->mem, bc->memsize,  &bc->memsize, NULL);
234         bc->mem[bc->memsize]='\0';  /* end this, so you can dump it as a string */
235         bc->memfree=TRUE;           /* this one needs to be freed */
236     }
237     else
238     {
239         bc->memsize=0;              /* this will prevent mem being accessed */
240         bc->memfree=FALSE;
241     }
242     bc->mempos = 0;                 /* set position to the start */
243 }
244 
245 /*
246  * Start batch file execution.
247  *
248  * The firstword parameter is the full filename of the batch file.
249  */
250 INT Batch(LPTSTR fullname, LPTSTR firstword, LPTSTR param, PARSED_COMMAND *Cmd)
251 {
252     INT ret = 0;
253     INT i;
254     HANDLE hFile = NULL;
255     BOOL bSameFn = FALSE;
256     BATCH_CONTEXT new;
257     PFOR_CONTEXT saved_fc;
258 
259     SetLastError(0);
260     if (bc && bc->mem)
261     {
262         TCHAR fpname[MAX_PATH];
263         GetFullPathName(fullname, ARRAYSIZE(fpname), fpname, NULL);
264         if (_tcsicmp(bc->BatchFilePath, fpname) == 0)
265             bSameFn = TRUE;
266     }
267     TRACE("Batch(\'%s\', \'%s\', \'%s\')  bSameFn = %d\n",
268         debugstr_aw(fullname), debugstr_aw(firstword), debugstr_aw(param), bSameFn);
269 
270     if (!bSameFn)
271     {
272         hFile = CreateFile(fullname, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, NULL,
273                            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL |
274                            FILE_FLAG_SEQUENTIAL_SCAN, NULL);
275 
276         if (hFile == INVALID_HANDLE_VALUE)
277         {
278             ConErrResPuts(STRING_BATCH_ERROR);
279             return 1;
280         }
281     }
282 
283     if (bc != NULL && Cmd == bc->current)
284     {
285         /* Then we are transferring to another batch */
286         ClearBatch();
287         AddBatchRedirection(&Cmd->Redirections);
288     }
289     else
290     {
291         struct _SETLOCAL *setlocal = NULL;
292 
293         if (Cmd == NULL)
294         {
295             /* This is a CALL. CALL will set errorlevel to our return value, so
296              * in order to keep the value of errorlevel unchanged in the case
297              * of calling an empty batch file, we must return that same value. */
298             ret = nErrorLevel;
299         }
300         else if (bc)
301         {
302             /* If a batch file runs another batch file as part of a compound command
303              * (e.g. "x.bat & somethingelse") then the first file gets terminated. */
304 
305             /* Get its SETLOCAL stack so it can be migrated to the new context */
306             setlocal = bc->setlocal;
307             bc->setlocal = NULL;
308             ExitBatch();
309         }
310 
311         /* Create a new context. This function will not
312          * return until this context has been exited */
313         new.prev = bc;
314         /* copy some fields in the new structure if it is the same file */
315         if (bSameFn)
316         {
317             new.mem     = bc->mem;
318             new.memsize = bc->memsize;
319             new.mempos  = 0;
320             new.memfree = FALSE;    /* don't free this, being used before this */
321         }
322         bc = &new;
323         bc->RedirList = NULL;
324         bc->setlocal = setlocal;
325     }
326 
327     GetFullPathName(fullname, ARRAYSIZE(bc->BatchFilePath), bc->BatchFilePath, NULL);
328 
329     /* If a new batch file, load it into memory and close the file */
330     if (!bSameFn)
331     {
332         BatchFile2Mem(hFile);
333         CloseHandle(hFile);
334     }
335 
336     bc->mempos = 0;    /* Go to the beginning of the batch file */
337     bc->bEcho = bEcho; /* Preserve echo across batch calls */
338     for (i = 0; i < 10; i++)
339         bc->shiftlevel[i] = i;
340 
341     /* Parse the parameters and make a raw copy of them without modifications */
342     bc->params = BatchParams(firstword, param);
343     bc->raw_params = cmd_dup(param);
344     if (bc->raw_params == NULL)
345     {
346         error_out_of_memory();
347         return 1;
348     }
349 
350     /* Check if this is a "CALL :label" */
351     if (*firstword == _T(':'))
352         ret = cmd_goto(firstword);
353 
354     /* If we are calling from inside a FOR, hide the FOR variables */
355     saved_fc = fc;
356     fc = NULL;
357 
358     /* If we have created a new context, don't return
359      * until this batch file has completed. */
360     while (bc == &new && !bExit)
361     {
362         Cmd = ParseCommand(NULL);
363         if (!Cmd)
364         {
365             if (!bParseError)
366                 continue;
367 
368             /* Echo the pre-parsed batch file line on error */
369             if (bEcho && !bDisableBatchEcho)
370             {
371                 if (!bIgnoreEcho)
372                     ConOutChar(_T('\n'));
373                 PrintPrompt();
374                 ConOutPuts(ParseLine);
375                 ConOutChar(_T('\n'));
376             }
377             /* Stop all execution */
378             ExitAllBatches();
379             ret = 1;
380             break;
381         }
382 
383         /* JPP 19980807 */
384         /* Echo the command and execute it */
385         bc->current = Cmd;
386         ret = ExecuteCommandWithEcho(Cmd);
387         FreeCommand(Cmd);
388     }
389 
390     /* Restore the FOR variables */
391     fc = saved_fc;
392 
393     /* Always return the last command's return code */
394     TRACE("Batch: returns %d\n", ret);
395     return ret;
396 }
397 
398 VOID AddBatchRedirection(REDIRECTION **RedirList)
399 {
400     REDIRECTION **ListEnd;
401 
402     /* Prepend the list to the batch context's list */
403     ListEnd = RedirList;
404     while (*ListEnd)
405         ListEnd = &(*ListEnd)->Next;
406     *ListEnd = bc->RedirList;
407     bc->RedirList = *RedirList;
408 
409     /* Null out the pointer so that the list will not be cleared prematurely.
410      * These redirections should persist until the batch file exits. */
411     *RedirList = NULL;
412 }
413 
414 /*
415  *   Read a single line from the batch file from the current batch/memory position.
416  *   Almost a copy of FileGetString with same UNICODE handling
417  */
418 BOOL BatchGetString(LPTSTR lpBuffer, INT nBufferLength)
419 {
420     LPSTR lpString;
421     INT len = 0;
422 #ifdef _UNICODE
423     lpString = cmd_alloc(nBufferLength);
424     if (!lpString)
425     {
426         WARN("Cannot allocate memory for lpString\n");
427         error_out_of_memory();
428         return FALSE;
429     }
430 #else
431     lpString = lpBuffer;
432 #endif
433     /* read all chars from memory until a '\n' is encountered */
434     if (bc->mem)
435     {
436         for (; (bc->mempos < bc->memsize  &&  len < (nBufferLength-1)); len++)
437         {
438             lpString[len] = bc->mem[bc->mempos++];
439             if (lpString[len] == '\n' )
440             {
441                 len++;
442                 break;
443             }
444         }
445     }
446 
447     if (!len)
448     {
449 #ifdef _UNICODE
450         cmd_free(lpString);
451 #endif
452         return FALSE;
453     }
454 
455     lpString[len++] = '\0';
456 #ifdef _UNICODE
457     MultiByteToWideChar(OutputCodePage, 0, lpString, -1, lpBuffer, len);
458     cmd_free(lpString);
459 #endif
460     return TRUE;
461 }
462 
463 /*
464  * Read and return the next executable line form the current batch file
465  *
466  * If no batch file is current or no further executable lines are found
467  * return NULL.
468  *
469  * Set eflag to 0 if line is not to be echoed else 1
470  */
471 LPTSTR ReadBatchLine(VOID)
472 {
473     TRACE("ReadBatchLine()\n");
474 
475     /* User halt */
476     if (CheckCtrlBreak(BREAK_BATCHFILE))
477     {
478         ExitAllBatches();
479         return NULL;
480     }
481 
482     if (!BatchGetString(textline, ARRAYSIZE(textline) - 1))
483     {
484         TRACE("ReadBatchLine(): Reached EOF!\n");
485         /* End of file */
486         ExitBatch();
487         return NULL;
488     }
489 
490     TRACE("ReadBatchLine(): textline: \'%s\'\n", debugstr_aw(textline));
491 
492     if (textline[_tcslen(textline) - 1] != _T('\n'))
493         _tcscat(textline, _T("\n"));
494 
495     return textline;
496 }
497 
498 /* EOF */
499