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