mshtml: Properly report history update for location.replace call.
[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 = WCMD_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   HeapFree(GetProcessHeap(), 0, 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 = HeapAlloc(GetProcessHeap(), 0, noChars);
261       if (!bufA) return NULL;
262
263       /* Save current file position */
264       filepos.QuadPart = 0;
265       SetFilePointerEx(h, filepos, &filepos, FILE_CURRENT);
266
267       status = ReadFile(h, bufA, noChars, &charsRead, NULL);
268       if (!status || charsRead == 0) {
269           HeapFree(GetProcessHeap(), 0, bufA);
270           return NULL;
271       }
272
273       /* Find first EOL */
274       for (p = bufA; p < (bufA + charsRead); p = CharNextExA(cp, p, 0)) {
275           if (*p == '\n' || *p == '\r')
276               break;
277       }
278
279       /* Sets file pointer to the start of the next line, if any */
280       filepos.QuadPart += p - bufA + 1 + (*p == '\r' ? 1 : 0);
281       SetFilePointerEx(h, filepos, NULL, FILE_BEGIN);
282
283       i = MultiByteToWideChar(cp, 0, bufA, p - bufA, buf, noChars);
284       HeapFree(GetProcessHeap(), 0, bufA);
285   }
286   else {
287       status = WCMD_ReadFile(h, buf, noChars, &charsRead);
288       if (!status || charsRead == 0) return NULL;
289
290       /* Find first EOL */
291       for (i = 0; i < charsRead; i++) {
292           if (buf[i] == '\n' || buf[i] == '\r')
293               break;
294       }
295   }
296
297   /* Truncate at EOL (or end of buffer) */
298   if (i == noChars)
299     i--;
300
301   buf[i] = '\0';
302
303   return buf;
304 }
305
306 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
307 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
308 {
309         const WCHAR* end; /* end of processed string */
310         const WCHAR* p;  /* search pointer */
311         const WCHAR* s;  /* copy pointer */
312
313         /* extract drive name */
314         if (path[0] && path[1]==':') {
315                 if (drv) {
316                         *drv++ = *path++;
317                         *drv++ = *path++;
318                         *drv = '\0';
319                 }
320         } else if (drv)
321                 *drv = '\0';
322
323         end = path + strlenW(path);
324
325         /* search for begin of file extension */
326         for(p=end; p>path && *--p!='\\' && *p!='/'; )
327                 if (*p == '.') {
328                         end = p;
329                         break;
330                 }
331
332         if (ext)
333                 for(s=end; (*ext=*s++); )
334                         ext++;
335
336         /* search for end of directory name */
337         for(p=end; p>path; )
338                 if (*--p=='\\' || *p=='/') {
339                         p++;
340                         break;
341                 }
342
343         if (name) {
344                 for(s=p; s<end; )
345                         *name++ = *s++;
346
347                 *name = '\0';
348         }
349
350         if (dir) {
351                 for(s=path; s<p; )
352                         *dir++ = *s++;
353
354                 *dir = '\0';
355         }
356 }
357
358 /****************************************************************************
359  * WCMD_HandleTildaModifiers
360  *
361  * Handle the ~ modifiers when expanding %0-9 or (%a-z/A-Z in for command)
362  *    %~xxxxxV  (V=0-9 or A-Z, a-z)
363  * Where xxxx is any combination of:
364  *    ~ - Removes quotes
365  *    f - Fully qualified path (assumes current dir if not drive\dir)
366  *    d - drive letter
367  *    p - path
368  *    n - filename
369  *    x - file extension
370  *    s - path with shortnames
371  *    a - attributes
372  *    t - date/time
373  *    z - size
374  *    $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
375  *                   qualified path
376  *
377  *  To work out the length of the modifier:
378  *
379  *  Note: In the case of %0-9 knowing the end of the modifier is easy,
380  *    but in a for loop, the for end WCHARacter may also be a modifier
381  *    eg. for %a in (c:\a.a) do echo XXX
382  *             where XXX = %~a    (just ~)
383  *                         %~aa   (~ and attributes)
384  *                         %~aaxa (~, attributes and extension)
385  *                   BUT   %~aax  (~ and attributes followed by 'x')
386  *
387  *  Hence search forwards until find an invalid modifier, and then
388  *  backwards until find for variable or 0-9
389  */
390 void WCMD_HandleTildaModifiers(WCHAR **start, BOOL justFors)
391 {
392
393 #define NUMMODIFIERS 11
394   static const WCHAR validmodifiers[NUMMODIFIERS] = {
395         '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
396   };
397
398   WIN32_FILE_ATTRIBUTE_DATA fileInfo;
399   WCHAR  outputparam[MAX_PATH];
400   WCHAR  finaloutput[MAX_PATH];
401   WCHAR  fullfilename[MAX_PATH];
402   WCHAR  thisoutput[MAX_PATH];
403   WCHAR  *pos            = *start+1;
404   WCHAR  *firstModifier  = pos;
405   WCHAR  *lastModifier   = NULL;
406   int   modifierLen     = 0;
407   BOOL  finished        = FALSE;
408   int   i               = 0;
409   BOOL  exists          = TRUE;
410   BOOL  skipFileParsing = FALSE;
411   BOOL  doneModifier    = FALSE;
412
413   /* Search forwards until find invalid character modifier */
414   while (!finished) {
415
416     /* Work on the previous character */
417     if (lastModifier != NULL) {
418
419       for (i=0; i<NUMMODIFIERS; i++) {
420         if (validmodifiers[i] == *lastModifier) {
421
422           /* Special case '$' to skip until : found */
423           if (*lastModifier == '$') {
424             while (*pos != ':' && *pos) pos++;
425             if (*pos == 0x00) return; /* Invalid syntax */
426             pos++;                    /* Skip ':'       */
427           }
428           break;
429         }
430       }
431
432       if (i==NUMMODIFIERS) {
433         finished = TRUE;
434       }
435     }
436
437     /* Save this one away */
438     if (!finished) {
439       lastModifier = pos;
440       pos++;
441     }
442   }
443
444   while (lastModifier > firstModifier) {
445     WINE_TRACE("Looking backwards for parameter id: %s\n",
446                wine_dbgstr_w(lastModifier));
447
448     if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
449       /* Its a valid parameter identifier - OK */
450       break;
451
452     } else {
453       int foridx = FOR_VAR_IDX(*lastModifier);
454       /* Its a valid parameter identifier - OK */
455       if ((foridx >= 0) && (forloopcontext.variable[foridx] != NULL)) break;
456
457       /* Its not a valid parameter identifier - step backwards */
458       lastModifier--;
459     }
460   }
461   if (lastModifier == firstModifier) return; /* Invalid syntax */
462
463   /* Extract the parameter to play with */
464   if (*lastModifier == '0') {
465     strcpyW(outputparam, context->batchfileW);
466   } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
467     strcpyW(outputparam,
468             WCMD_parameter (context -> command,
469                             *lastModifier-'0' + context -> shift_count[*lastModifier-'0'],
470                             NULL, FALSE, TRUE));
471   } else {
472     int foridx = FOR_VAR_IDX(*lastModifier);
473     strcpyW(outputparam, forloopcontext.variable[foridx]);
474   }
475
476   /* So now, firstModifier points to beginning of modifiers, lastModifier
477      points to the variable just after the modifiers. Process modifiers
478      in a specific order, remembering there could be duplicates           */
479   modifierLen = lastModifier - firstModifier;
480   finaloutput[0] = 0x00;
481
482   /* 1. Handle '~' : Strip surrounding quotes */
483   if (outputparam[0]=='"' &&
484       memchrW(firstModifier, '~', modifierLen) != NULL) {
485     int len = strlenW(outputparam);
486     if (outputparam[len-1] == '"') {
487         outputparam[len-1]=0x00;
488         len = len - 1;
489     }
490     memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
491   }
492
493   /* 2. Handle the special case of a $ */
494   if (memchrW(firstModifier, '$', modifierLen) != NULL) {
495     /* Special Case: Search envar specified in $[envvar] for outputparam
496        Note both $ and : are guaranteed otherwise check above would fail */
497     WCHAR *begin = strchrW(firstModifier, '$') + 1;
498     WCHAR *end   = strchrW(firstModifier, ':');
499     WCHAR env[MAX_PATH];
500     WCHAR fullpath[MAX_PATH];
501
502     /* Extract the env var */
503     memcpy(env, begin, (end-begin) * sizeof(WCHAR));
504     env[(end-begin)] = 0x00;
505
506     /* If env var not found, return empty string */
507     if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
508         (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
509       finaloutput[0] = 0x00;
510       outputparam[0] = 0x00;
511       skipFileParsing = TRUE;
512     }
513   }
514
515   /* After this, we need full information on the file,
516     which is valid not to exist.  */
517   if (!skipFileParsing) {
518     if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
519       return;
520
521     exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
522                                   &fileInfo);
523
524     /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
525     if (memchrW(firstModifier, 'a', modifierLen) != NULL) {
526
527       WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
528       doneModifier = TRUE;
529
530       if (exists) {
531         strcpyW(thisoutput, defaults);
532         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
533           thisoutput[0]='d';
534         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
535           thisoutput[1]='r';
536         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
537           thisoutput[2]='a';
538         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
539           thisoutput[3]='h';
540         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
541           thisoutput[4]='s';
542         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
543           thisoutput[5]='c';
544         /* FIXME: What are 6 and 7? */
545         if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
546           thisoutput[8]='l';
547         strcatW(finaloutput, thisoutput);
548       }
549     }
550
551     /* 3. Handle 't' : Date+time (File doesn't have to exist) */
552     if (memchrW(firstModifier, 't', modifierLen) != NULL) {
553
554       SYSTEMTIME systime;
555       int datelen;
556
557       doneModifier = TRUE;
558
559       if (exists) {
560         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
561
562         /* Format the time */
563         FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
564         GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
565                           NULL, thisoutput, MAX_PATH);
566         strcatW(thisoutput, spaceW);
567         datelen = strlenW(thisoutput);
568         GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
569                           NULL, (thisoutput+datelen), MAX_PATH-datelen);
570         strcatW(finaloutput, thisoutput);
571       }
572     }
573
574     /* 4. Handle 'z' : File length (File doesn't have to exist) */
575     if (memchrW(firstModifier, 'z', modifierLen) != NULL) {
576       /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
577       ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
578                                   fileInfo.nFileSizeLow;
579       static const WCHAR fmt[] = {'%','u','\0'};
580
581       doneModifier = TRUE;
582       if (exists) {
583         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
584         wsprintfW(thisoutput, fmt, fullsize);
585         strcatW(finaloutput, thisoutput);
586       }
587     }
588
589     /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
590     if (memchrW(firstModifier, 's', modifierLen) != NULL) {
591       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
592       /* Don't flag as doneModifier - %~s on its own is processed later */
593       GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
594     }
595
596     /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
597     /*      Note this overrides d,p,n,x                                 */
598     if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
599       doneModifier = TRUE;
600       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
601       strcatW(finaloutput, fullfilename);
602     } else {
603
604       WCHAR drive[10];
605       WCHAR dir[MAX_PATH];
606       WCHAR fname[MAX_PATH];
607       WCHAR ext[MAX_PATH];
608       BOOL doneFileModifier = FALSE;
609       BOOL addSpace = (finaloutput[0] != 0x00);
610
611       /* Split into components */
612       WCMD_splitpath(fullfilename, drive, dir, fname, ext);
613
614       /* 5. Handle 'd' : Drive Letter */
615       if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
616         if (addSpace) {
617           strcatW(finaloutput, spaceW);
618           addSpace = FALSE;
619         }
620
621         strcatW(finaloutput, drive);
622         doneModifier = TRUE;
623         doneFileModifier = TRUE;
624       }
625
626       /* 6. Handle 'p' : Path */
627       if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
628         if (addSpace) {
629           strcatW(finaloutput, spaceW);
630           addSpace = FALSE;
631         }
632
633         strcatW(finaloutput, dir);
634         doneModifier = TRUE;
635         doneFileModifier = TRUE;
636       }
637
638       /* 7. Handle 'n' : Name */
639       if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
640         if (addSpace) {
641           strcatW(finaloutput, spaceW);
642           addSpace = FALSE;
643         }
644
645         strcatW(finaloutput, fname);
646         doneModifier = TRUE;
647         doneFileModifier = TRUE;
648       }
649
650       /* 8. Handle 'x' : Ext */
651       if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
652         if (addSpace) {
653           strcatW(finaloutput, spaceW);
654           addSpace = FALSE;
655         }
656
657         strcatW(finaloutput, ext);
658         doneModifier = TRUE;
659         doneFileModifier = TRUE;
660       }
661
662       /* If 's' but no other parameter, dump the whole thing */
663       if (!doneFileModifier &&
664           memchrW(firstModifier, 's', modifierLen) != NULL) {
665         doneModifier = TRUE;
666         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
667         strcatW(finaloutput, outputparam);
668       }
669     }
670   }
671
672   /* If No other modifier processed,  just add in parameter */
673   if (!doneModifier) strcpyW(finaloutput, outputparam);
674
675   /* Finish by inserting the replacement into the string */
676   WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
677 }
678
679 /*******************************************************************
680  * WCMD_call - processes a batch call statement
681  *
682  *      If there is a leading ':', calls within this batch program
683  *      otherwise launches another program.
684  */
685 void WCMD_call (WCHAR *command) {
686
687   /* Run other program if no leading ':' */
688   if (*command != ':') {
689     WCMD_run_program(command, TRUE);
690     /* If the thing we try to run does not exist, call returns 1 */
691     if (errorlevel) errorlevel=1;
692   } else {
693
694     WCHAR gotoLabel[MAX_PATH];
695
696     strcpyW(gotoLabel, param1);
697
698     if (context) {
699
700       LARGE_INTEGER li;
701       FOR_CONTEXT oldcontext;
702
703       /* Save the for variable context, then start with an empty context
704          as for loop variables do not survive a call                    */
705       oldcontext = forloopcontext;
706       memset(&forloopcontext, 0, sizeof(forloopcontext));
707
708       /* Save the current file position, call the same file,
709          restore position                                    */
710       li.QuadPart = 0;
711       li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
712                      &li.u.HighPart, FILE_CURRENT);
713       WCMD_batch (param1, command, TRUE, gotoLabel, context->h);
714       SetFilePointer(context -> h, li.u.LowPart,
715                      &li.u.HighPart, FILE_BEGIN);
716
717       /* Restore the for loop context */
718       forloopcontext = oldcontext;
719     } else {
720       WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT));
721     }
722   }
723 }