cmd: Added HeapFree wrapper.
[wine] / programs / cmd / batch.c
1 /*
2  * CMD - Wine-compatible command line interface - batch interface.
3  *
4  * Copyright (C) 1999 D A Pickles
5  * Copyright (C) 2007 J Edmeades
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20  */
21
22 #include "wcmd.h"
23 #include "wine/debug.h"
24
25 extern struct env_stack *saved_environment;
26
27 WINE_DEFAULT_DEBUG_CHANNEL(cmd);
28
29 /****************************************************************************
30  * WCMD_batch
31  *
32  * Open and execute a batch file.
33  * On entry *command includes the complete command line beginning with the name
34  * of the batch file (if a CALL command was entered the CALL has been removed).
35  * *file is the name of the file, which might not exist and may not have the
36  * .BAT suffix on. Called is 1 for a CALL, 0 otherwise.
37  *
38  * We need to handle recursion correctly, since one batch program might call another.
39  * So parameters for this batch file are held in a BATCH_CONTEXT structure.
40  *
41  * To support call within the same batch program, another input parameter is
42  * a label to goto once opened.
43  */
44
45 void WCMD_batch (WCHAR *file, WCHAR *command, BOOL called, WCHAR *startLabel, HANDLE pgmHandle)
46 {
47   HANDLE h = INVALID_HANDLE_VALUE;
48   BATCH_CONTEXT *prev_context;
49
50   if (startLabel == NULL) {
51     h = CreateFileW (file, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
52                      NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
53     if (h == INVALID_HANDLE_VALUE) {
54       SetLastError (ERROR_FILE_NOT_FOUND);
55       WCMD_print_error ();
56       return;
57     }
58   } else {
59     DuplicateHandle(GetCurrentProcess(), pgmHandle,
60                     GetCurrentProcess(), &h,
61                     0, FALSE, DUPLICATE_SAME_ACCESS);
62   }
63
64 /*
65  *      Create a context structure for this batch file.
66  */
67
68   prev_context = context;
69   context = LocalAlloc (LMEM_FIXED, sizeof (BATCH_CONTEXT));
70   context -> h = h;
71   context->batchfileW = heap_strdupW(file);
72   context -> command = command;
73   memset(context -> shift_count, 0x00, sizeof(context -> shift_count));
74   context -> prev_context = prev_context;
75   context -> skip_rest = FALSE;
76
77   /* If processing a call :label, 'goto' the label in question */
78   if (startLabel) {
79     strcpyW(param1, startLabel);
80     WCMD_goto(NULL);
81   }
82
83 /*
84  *      Work through the file line by line. Specific batch commands are processed here,
85  *      the rest are handled by the main command processor.
86  */
87
88   while (context -> skip_rest == FALSE) {
89       CMD_LIST *toExecute = NULL;         /* Commands left to be executed */
90       if (!WCMD_ReadAndParseLine(NULL, &toExecute, h))
91         break;
92       /* Note: although this batch program itself may be called, we are not retrying
93          the command as a result of a call failing to find a program, hence the
94          retryCall parameter below is FALSE                                           */
95       WCMD_process_commands(toExecute, FALSE, FALSE);
96       WCMD_free_commands(toExecute);
97       toExecute = NULL;
98   }
99   CloseHandle (h);
100
101 /*
102  *  If there are outstanding setlocal's to the current context, unwind them.
103  */
104   while (saved_environment && saved_environment->batchhandle == context->h) {
105       WCMD_endlocal();
106   }
107
108 /*
109  *      If invoked by a CALL, we return to the context of our caller. Otherwise return
110  *      to the caller's caller.
111  */
112
113   heap_free(context->batchfileW);
114   LocalFree (context);
115   if ((prev_context != NULL) && (!called)) {
116     WINE_TRACE("Batch completed, but was not 'called' so skipping outer batch too\n");
117     prev_context -> skip_rest = TRUE;
118     context = prev_context;
119   }
120   context = prev_context;
121 }
122
123 /*******************************************************************
124  * WCMD_parameter_with_delims
125  *
126  * Extracts a delimited parameter from an input string, providing
127  * the delimiters characters to use
128  *
129  * PARAMS
130  *  s     [I] input string, non NULL
131  *  n     [I] # of the parameter to return, counted from 0
132  *  start [O] Optional. Pointer to the first char of param n in s
133  *  raw   [I] TRUE to return the parameter in raw format (quotes maintained)
134  *            FALSE to return the parameter with quotes stripped (including internal ones)
135  *  wholecmdline [I] TRUE to indicate this routine is being used to parse the
136  *                   command line, and special logic for arg0->1 transition
137  *                   needs to be applied.
138  *  delims[I] The delimiter characters to use
139  *
140  * RETURNS
141  *  Success: The nth delimited parameter found in s
142  *           if start != NULL, *start points to the start of the param (quotes maintained)
143  *  Failure: An empty string if the param is not found.
144  *           *start == NULL
145  *
146  * NOTES
147  *  Return value is stored in static storage (i.e. overwritten after each call).
148  *  By default, the parameter is returned with quotes removed, ready for use with
149  *  other API calls, e.g. c:\"a b"\c is returned as c:\a b\c. However, some commands
150  *  need to preserve the exact syntax (echo, for, etc) hence the raw option.
151  */
152 WCHAR *WCMD_parameter_with_delims (WCHAR *s, int n, WCHAR **start,
153                                    BOOL raw, BOOL wholecmdline, const WCHAR *delims)
154 {
155     int curParamNb = 0;
156     static WCHAR param[MAX_PATH];
157     WCHAR *p = s, *begin;
158
159     if (start != NULL) *start = NULL;
160     param[0] = '\0';
161
162     while (TRUE) {
163
164         /* Absorb repeated word delimiters until we get to the next token (or the end!) */
165         while (*p && (strchrW(delims, *p) != NULL))
166             p++;
167         if (*p == '\0') return param;
168
169         /* If we have reached the token number we want, remember the beginning of it */
170         if (start != NULL && curParamNb == n) *start = p;
171
172         /* Return the whole word up to the next delimiter, handling quotes in the middle
173            of it, e.g. a"\b c\"d is a single parameter.                                  */
174         begin = p;
175
176         /* Loop character by character, but just need to special case quotes */
177         while (*p) {
178             /* Once we have found a delimiter, break */
179             if (strchrW(delims, *p) != NULL) break;
180
181             /* Very odd special case - Seems as if a ( acts as a delimiter which is
182                not swallowed but is effective only when it comes between the program
183                name and the parameters. Need to avoid this triggering when used
184                to walk parameters generally.                                         */
185             if (wholecmdline && curParamNb == 0 && *p=='(') break;
186
187             /* If we find a quote, copy until we get the end quote */
188             if (*p == '"') {
189                 p++;
190                 while (*p && *p != '"') p++;
191             }
192
193             /* Now skip the character / quote */
194             if (*p) p++;
195         }
196
197         if (curParamNb == n) {
198             /* Return the parameter in static storage either as-is (raw) or
199                suitable for use with other win32 api calls (quotes stripped) */
200             if (raw) {
201                 memcpy(param, begin, (p - begin) * sizeof(WCHAR));
202                 param[p-begin] = '\0';
203             } else {
204                 int i=0;
205                 while (begin < p) {
206                   if (*begin != '"') param[i++] = *begin;
207                   begin++;
208                 }
209                 param[i] = '\0';
210             }
211             return param;
212         }
213         curParamNb++;
214     }
215 }
216
217 /*******************************************************************
218  * WCMD_parameter
219  *
220  * Extracts a delimited parameter from an input string, using a
221  * default set of delimiter characters. For parameters, see the main
222  * function above.
223  */
224 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw,
225                        BOOL wholecmdline)
226 {
227   static const WCHAR defaultDelims[] = { ' ', '\t', ',', '=', ';', '\0' };
228   return WCMD_parameter_with_delims (s, n, start, raw, wholecmdline, defaultDelims);
229 }
230
231 /****************************************************************************
232  * WCMD_fgets
233  *
234  * Gets one line from a file/console and puts it into buffer buf
235  * Pre:  buf has size noChars
236  *       1 <= noChars <= MAXSTRING
237  * Post: buf is filled with at most noChars-1 characters, and gets nul-terminated
238          buf does not include EOL terminator
239  * Returns:
240  *       buf on success
241  *       NULL on error or EOF
242  */
243
244 WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
245 {
246   DWORD charsRead;
247   BOOL status;
248   DWORD i;
249
250   /* We can't use the native f* functions because of the filename syntax differences
251      between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
252
253   if (!WCMD_is_console_handle(h)) {
254       LARGE_INTEGER filepos;
255       char *bufA;
256       UINT cp;
257       const char *p;
258
259       cp = GetConsoleCP();
260       bufA = heap_alloc(noChars);
261
262       /* Save current file position */
263       filepos.QuadPart = 0;
264       SetFilePointerEx(h, filepos, &filepos, FILE_CURRENT);
265
266       status = ReadFile(h, bufA, noChars, &charsRead, NULL);
267       if (!status || charsRead == 0) {
268           heap_free(bufA);
269           return NULL;
270       }
271
272       /* Find first EOL */
273       for (p = bufA; p < (bufA + charsRead); p = CharNextExA(cp, p, 0)) {
274           if (*p == '\n' || *p == '\r')
275               break;
276       }
277
278       /* Sets file pointer to the start of the next line, if any */
279       filepos.QuadPart += p - bufA + 1 + (*p == '\r' ? 1 : 0);
280       SetFilePointerEx(h, filepos, NULL, FILE_BEGIN);
281
282       i = MultiByteToWideChar(cp, 0, bufA, p - bufA, buf, noChars);
283       heap_free(bufA);
284   }
285   else {
286       status = WCMD_ReadFile(h, buf, noChars, &charsRead);
287       if (!status || charsRead == 0) return NULL;
288
289       /* Find first EOL */
290       for (i = 0; i < charsRead; i++) {
291           if (buf[i] == '\n' || buf[i] == '\r')
292               break;
293       }
294   }
295
296   /* Truncate at EOL (or end of buffer) */
297   if (i == noChars)
298     i--;
299
300   buf[i] = '\0';
301
302   return buf;
303 }
304
305 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
306 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
307 {
308         const WCHAR* end; /* end of processed string */
309         const WCHAR* p;  /* search pointer */
310         const WCHAR* s;  /* copy pointer */
311
312         /* extract drive name */
313         if (path[0] && path[1]==':') {
314                 if (drv) {
315                         *drv++ = *path++;
316                         *drv++ = *path++;
317                         *drv = '\0';
318                 }
319         } else if (drv)
320                 *drv = '\0';
321
322         end = path + strlenW(path);
323
324         /* search for begin of file extension */
325         for(p=end; p>path && *--p!='\\' && *p!='/'; )
326                 if (*p == '.') {
327                         end = p;
328                         break;
329                 }
330
331         if (ext)
332                 for(s=end; (*ext=*s++); )
333                         ext++;
334
335         /* search for end of directory name */
336         for(p=end; p>path; )
337                 if (*--p=='\\' || *p=='/') {
338                         p++;
339                         break;
340                 }
341
342         if (name) {
343                 for(s=p; s<end; )
344                         *name++ = *s++;
345
346                 *name = '\0';
347         }
348
349         if (dir) {
350                 for(s=path; s<p; )
351                         *dir++ = *s++;
352
353                 *dir = '\0';
354         }
355 }
356
357 /****************************************************************************
358  * WCMD_HandleTildaModifiers
359  *
360  * Handle the ~ modifiers when expanding %0-9 or (%a-z/A-Z in for command)
361  *    %~xxxxxV  (V=0-9 or A-Z, a-z)
362  * Where xxxx is any combination of:
363  *    ~ - Removes quotes
364  *    f - Fully qualified path (assumes current dir if not drive\dir)
365  *    d - drive letter
366  *    p - path
367  *    n - filename
368  *    x - file extension
369  *    s - path with shortnames
370  *    a - attributes
371  *    t - date/time
372  *    z - size
373  *    $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
374  *                   qualified path
375  *
376  *  To work out the length of the modifier:
377  *
378  *  Note: In the case of %0-9 knowing the end of the modifier is easy,
379  *    but in a for loop, the for end WCHARacter may also be a modifier
380  *    eg. for %a in (c:\a.a) do echo XXX
381  *             where XXX = %~a    (just ~)
382  *                         %~aa   (~ and attributes)
383  *                         %~aaxa (~, attributes and extension)
384  *                   BUT   %~aax  (~ and attributes followed by 'x')
385  *
386  *  Hence search forwards until find an invalid modifier, and then
387  *  backwards until find for variable or 0-9
388  */
389 void WCMD_HandleTildaModifiers(WCHAR **start, BOOL justFors)
390 {
391
392 #define NUMMODIFIERS 11
393   static const WCHAR validmodifiers[NUMMODIFIERS] = {
394         '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
395   };
396
397   WIN32_FILE_ATTRIBUTE_DATA fileInfo;
398   WCHAR  outputparam[MAX_PATH];
399   WCHAR  finaloutput[MAX_PATH];
400   WCHAR  fullfilename[MAX_PATH];
401   WCHAR  thisoutput[MAX_PATH];
402   WCHAR  *pos            = *start+1;
403   WCHAR  *firstModifier  = pos;
404   WCHAR  *lastModifier   = NULL;
405   int   modifierLen     = 0;
406   BOOL  finished        = FALSE;
407   int   i               = 0;
408   BOOL  exists          = TRUE;
409   BOOL  skipFileParsing = FALSE;
410   BOOL  doneModifier    = FALSE;
411
412   /* Search forwards until find invalid character modifier */
413   while (!finished) {
414
415     /* Work on the previous character */
416     if (lastModifier != NULL) {
417
418       for (i=0; i<NUMMODIFIERS; i++) {
419         if (validmodifiers[i] == *lastModifier) {
420
421           /* Special case '$' to skip until : found */
422           if (*lastModifier == '$') {
423             while (*pos != ':' && *pos) pos++;
424             if (*pos == 0x00) return; /* Invalid syntax */
425             pos++;                    /* Skip ':'       */
426           }
427           break;
428         }
429       }
430
431       if (i==NUMMODIFIERS) {
432         finished = TRUE;
433       }
434     }
435
436     /* Save this one away */
437     if (!finished) {
438       lastModifier = pos;
439       pos++;
440     }
441   }
442
443   while (lastModifier > firstModifier) {
444     WINE_TRACE("Looking backwards for parameter id: %s\n",
445                wine_dbgstr_w(lastModifier));
446
447     if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
448       /* Its a valid parameter identifier - OK */
449       break;
450
451     } else {
452       int foridx = FOR_VAR_IDX(*lastModifier);
453       /* Its a valid parameter identifier - OK */
454       if ((foridx >= 0) && (forloopcontext.variable[foridx] != NULL)) break;
455
456       /* Its not a valid parameter identifier - step backwards */
457       lastModifier--;
458     }
459   }
460   if (lastModifier == firstModifier) return; /* Invalid syntax */
461
462   /* Extract the parameter to play with */
463   if (*lastModifier == '0') {
464     strcpyW(outputparam, context->batchfileW);
465   } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
466     strcpyW(outputparam,
467             WCMD_parameter (context -> command,
468                             *lastModifier-'0' + context -> shift_count[*lastModifier-'0'],
469                             NULL, FALSE, TRUE));
470   } else {
471     int foridx = FOR_VAR_IDX(*lastModifier);
472     strcpyW(outputparam, forloopcontext.variable[foridx]);
473   }
474
475   /* So now, firstModifier points to beginning of modifiers, lastModifier
476      points to the variable just after the modifiers. Process modifiers
477      in a specific order, remembering there could be duplicates           */
478   modifierLen = lastModifier - firstModifier;
479   finaloutput[0] = 0x00;
480
481   /* 1. Handle '~' : Strip surrounding quotes */
482   if (outputparam[0]=='"' &&
483       memchrW(firstModifier, '~', modifierLen) != NULL) {
484     int len = strlenW(outputparam);
485     if (outputparam[len-1] == '"') {
486         outputparam[len-1]=0x00;
487         len = len - 1;
488     }
489     memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
490   }
491
492   /* 2. Handle the special case of a $ */
493   if (memchrW(firstModifier, '$', modifierLen) != NULL) {
494     /* Special Case: Search envar specified in $[envvar] for outputparam
495        Note both $ and : are guaranteed otherwise check above would fail */
496     WCHAR *begin = strchrW(firstModifier, '$') + 1;
497     WCHAR *end   = strchrW(firstModifier, ':');
498     WCHAR env[MAX_PATH];
499     WCHAR fullpath[MAX_PATH];
500
501     /* Extract the env var */
502     memcpy(env, begin, (end-begin) * sizeof(WCHAR));
503     env[(end-begin)] = 0x00;
504
505     /* If env var not found, return empty string */
506     if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
507         (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
508       finaloutput[0] = 0x00;
509       outputparam[0] = 0x00;
510       skipFileParsing = TRUE;
511     }
512   }
513
514   /* After this, we need full information on the file,
515     which is valid not to exist.  */
516   if (!skipFileParsing) {
517     if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
518       return;
519
520     exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
521                                   &fileInfo);
522
523     /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
524     if (memchrW(firstModifier, 'a', modifierLen) != NULL) {
525
526       WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
527       doneModifier = TRUE;
528
529       if (exists) {
530         strcpyW(thisoutput, defaults);
531         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
532           thisoutput[0]='d';
533         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
534           thisoutput[1]='r';
535         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
536           thisoutput[2]='a';
537         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
538           thisoutput[3]='h';
539         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
540           thisoutput[4]='s';
541         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
542           thisoutput[5]='c';
543         /* FIXME: What are 6 and 7? */
544         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
545           thisoutput[8]='l';
546         strcatW(finaloutput, thisoutput);
547       }
548     }
549
550     /* 3. Handle 't' : Date+time (File doesn't have to exist) */
551     if (memchrW(firstModifier, 't', modifierLen) != NULL) {
552
553       SYSTEMTIME systime;
554       int datelen;
555
556       doneModifier = TRUE;
557
558       if (exists) {
559         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
560
561         /* Format the time */
562         FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
563         GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
564                           NULL, thisoutput, MAX_PATH);
565         strcatW(thisoutput, spaceW);
566         datelen = strlenW(thisoutput);
567         GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
568                           NULL, (thisoutput+datelen), MAX_PATH-datelen);
569         strcatW(finaloutput, thisoutput);
570       }
571     }
572
573     /* 4. Handle 'z' : File length (File doesn't have to exist) */
574     if (memchrW(firstModifier, 'z', modifierLen) != NULL) {
575       /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
576       ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
577                                   fileInfo.nFileSizeLow;
578       static const WCHAR fmt[] = {'%','u','\0'};
579
580       doneModifier = TRUE;
581       if (exists) {
582         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
583         wsprintfW(thisoutput, fmt, fullsize);
584         strcatW(finaloutput, thisoutput);
585       }
586     }
587
588     /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
589     if (memchrW(firstModifier, 's', modifierLen) != NULL) {
590       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
591       /* Don't flag as doneModifier - %~s on its own is processed later */
592       GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
593     }
594
595     /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
596     /*      Note this overrides d,p,n,x                                 */
597     if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
598       doneModifier = TRUE;
599       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
600       strcatW(finaloutput, fullfilename);
601     } else {
602
603       WCHAR drive[10];
604       WCHAR dir[MAX_PATH];
605       WCHAR fname[MAX_PATH];
606       WCHAR ext[MAX_PATH];
607       BOOL doneFileModifier = FALSE;
608       BOOL addSpace = (finaloutput[0] != 0x00);
609
610       /* Split into components */
611       WCMD_splitpath(fullfilename, drive, dir, fname, ext);
612
613       /* 5. Handle 'd' : Drive Letter */
614       if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
615         if (addSpace) {
616           strcatW(finaloutput, spaceW);
617           addSpace = FALSE;
618         }
619
620         strcatW(finaloutput, drive);
621         doneModifier = TRUE;
622         doneFileModifier = TRUE;
623       }
624
625       /* 6. Handle 'p' : Path */
626       if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
627         if (addSpace) {
628           strcatW(finaloutput, spaceW);
629           addSpace = FALSE;
630         }
631
632         strcatW(finaloutput, dir);
633         doneModifier = TRUE;
634         doneFileModifier = TRUE;
635       }
636
637       /* 7. Handle 'n' : Name */
638       if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
639         if (addSpace) {
640           strcatW(finaloutput, spaceW);
641           addSpace = FALSE;
642         }
643
644         strcatW(finaloutput, fname);
645         doneModifier = TRUE;
646         doneFileModifier = TRUE;
647       }
648
649       /* 8. Handle 'x' : Ext */
650       if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
651         if (addSpace) {
652           strcatW(finaloutput, spaceW);
653           addSpace = FALSE;
654         }
655
656         strcatW(finaloutput, ext);
657         doneModifier = TRUE;
658         doneFileModifier = TRUE;
659       }
660
661       /* If 's' but no other parameter, dump the whole thing */
662       if (!doneFileModifier &&
663           memchrW(firstModifier, 's', modifierLen) != NULL) {
664         doneModifier = TRUE;
665         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
666         strcatW(finaloutput, outputparam);
667       }
668     }
669   }
670
671   /* If No other modifier processed,  just add in parameter */
672   if (!doneModifier) strcpyW(finaloutput, outputparam);
673
674   /* Finish by inserting the replacement into the string */
675   WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
676 }
677
678 /*******************************************************************
679  * WCMD_call - processes a batch call statement
680  *
681  *      If there is a leading ':', calls within this batch program
682  *      otherwise launches another program.
683  */
684 void WCMD_call (WCHAR *command) {
685
686   /* Run other program if no leading ':' */
687   if (*command != ':') {
688     WCMD_run_program(command, TRUE);
689     /* If the thing we try to run does not exist, call returns 1 */
690     if (errorlevel) errorlevel=1;
691   } else {
692
693     WCHAR gotoLabel[MAX_PATH];
694
695     strcpyW(gotoLabel, param1);
696
697     if (context) {
698
699       LARGE_INTEGER li;
700       FOR_CONTEXT oldcontext;
701
702       /* Save the for variable context, then start with an empty context
703          as for loop variables do not survive a call                    */
704       oldcontext = forloopcontext;
705       memset(&forloopcontext, 0, sizeof(forloopcontext));
706
707       /* Save the current file position, call the same file,
708          restore position                                    */
709       li.QuadPart = 0;
710       li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
711                      &li.u.HighPart, FILE_CURRENT);
712       WCMD_batch (param1, command, TRUE, gotoLabel, context->h);
713       SetFilePointer(context -> h, li.u.LowPart,
714                      &li.u.HighPart, FILE_BEGIN);
715
716       /* Restore the for loop context */
717       forloopcontext = oldcontext;
718     } else {
719       WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT));
720     }
721   }
722 }