jscript: Removed SysFreeString calls missed in previous patch.
[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       WCMD_process_commands(toExecute, FALSE, NULL, NULL);
93       WCMD_free_commands(toExecute);
94       toExecute = NULL;
95   }
96   CloseHandle (h);
97
98 /*
99  *  If there are outstanding setlocal's to the current context, unwind them.
100  */
101   while (saved_environment && saved_environment->batchhandle == context->h) {
102       WCMD_endlocal();
103   }
104
105 /*
106  *      If invoked by a CALL, we return to the context of our caller. Otherwise return
107  *      to the caller's caller.
108  */
109
110   HeapFree(GetProcessHeap(), 0, context->batchfileW);
111   LocalFree (context);
112   if ((prev_context != NULL) && (!called)) {
113     prev_context -> skip_rest = TRUE;
114     context = prev_context;
115   }
116   context = prev_context;
117 }
118
119 /*******************************************************************
120  * WCMD_parameter
121  *
122  * Extracts a delimited parameter from an input string
123  *
124  * PARAMS
125  *  s     [I] input string, non NULL
126  *  n     [I] # of the parameter to return, counted from 0
127  *  start [O] Optional. Pointer to the first char of param n in s
128  *  end   [O] Optional. Pointer to the last char of param n in s
129  *  raw   [I] True to return the parameter in raw format (quotes maintained)
130  *            False returns the parameter with quotes stripped
131  *
132  * RETURNS
133  *  Success: The nth delimited parameter found in s
134  *           if start != NULL, *start points to the start of the param
135  *           if end != NULL, *end points to the end of the param
136  *  Failure: An empty string if the param is not found.
137  *           *start == *end == NULL
138  *
139  * NOTES
140  *  Return value is stored in static storage (i.e. overwritten after each call).
141  *  Specify 'start' and/or 'end' to include delimiting double quotes as well, if any.
142  *  By default, the parameter is returned with quotes removed, ready for use with
143  *  other API calls, e.g. c:\"a b"\c is returned as c:\a b\c. However, some commands
144  *  need to preserve the exact syntax (echo, for, etc) hence the raw option.
145  */
146 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, WCHAR **end, BOOL raw)
147 {
148     static const WCHAR defaultDelims[] = { ' ', '\t', ',', '=', ';', '\0' };
149     int curParamNb = 0;
150     static WCHAR param[MAX_PATH];
151     WCHAR *p = s, *begin;
152
153     if (start != NULL) *start = NULL;
154     if (end != NULL) *end = NULL;
155     param[0] = '\0';
156
157     while (TRUE) {
158
159         /* Absorb repeated word delimiters until we get to the next token (or the end!) */
160         while (*p && (strchrW(defaultDelims, *p) != NULL))
161             p++;
162         if (*p == '\0') return param;
163
164         /* If we have reached the token number we want, remember the beginning of it */
165         if (start != NULL && curParamNb == n) *start = p;
166
167         /* Return the whole word up to the next delimiter, handling quotes in the middle
168            of it, e.g. a"\b c\"d is a single parameter.                                  */
169         begin = p;
170
171         /* Loop character by character, but just need to special case quotes */
172         while (*p) {
173             /* Once we have found a delimiter, break */
174             if (strchrW(defaultDelims, *p) != NULL) break;
175
176             /* If we find a quote, copy until we get the end quote */
177             if (*p == '"') {
178                 p++;
179                 while (*p && *p != '"') p++;
180             }
181
182             /* Now skip the character / quote */
183             if (*p) p++;
184         }
185
186         if (curParamNb == n) {
187             /* Return the parameter in static storage either as-is (raw) or
188                suitable for use with other win32 api calls (quotes stripped) */
189             if (raw) {
190                 memcpy(param, begin, (p - begin) * sizeof(WCHAR));
191                 param[p-begin] = '\0';
192             } else {
193                 int i=0;
194                 while (begin < p) {
195                   if (*begin != '"') param[i++] = *begin;
196                   begin++;
197                 }
198                 param[i] = '\0';
199             }
200             if (end) *end = p - 1;
201             return param;
202         }
203         curParamNb++;
204     }
205 }
206
207 /****************************************************************************
208  * WCMD_fgets
209  *
210  * Gets one line from a file/console and puts it into buffer buf
211  * Pre:  buf has size noChars
212  *       1 <= noChars <= MAXSTRING
213  * Post: buf is filled with at most noChars-1 characters, and gets nul-terminated
214          buf does not include EOL terminator
215  * Returns:
216  *       buf on success
217  *       NULL on error or EOF
218  */
219
220 WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
221 {
222   DWORD charsRead;
223   BOOL status;
224   LARGE_INTEGER filepos;
225   DWORD i;
226
227   /* We can't use the native f* functions because of the filename syntax differences
228      between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
229
230   if (!WCMD_is_console_handle(h)) {
231     /* Save current file position */
232     filepos.QuadPart = 0;
233     SetFilePointerEx(h, filepos, &filepos, FILE_CURRENT);
234   }
235
236   status = WCMD_ReadFile(h, buf, noChars, &charsRead);
237   if (!status || charsRead == 0) return NULL;
238
239   /* Find first EOL */
240   for (i = 0; i < charsRead; i++) {
241     if (buf[i] == '\n' || buf[i] == '\r')
242       break;
243   }
244
245   if (!WCMD_is_console_handle(h) && i != charsRead) {
246     /* Sets file pointer to the start of the next line, if any */
247     filepos.QuadPart += i + 1 + (buf[i] == '\r' ? 1 : 0);
248     SetFilePointerEx(h, filepos, NULL, FILE_BEGIN);
249   }
250
251   /* Truncate at EOL (or end of buffer) */
252   if (i == noChars)
253     i--;
254
255   buf[i] = '\0';
256
257   return buf;
258 }
259
260 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
261 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
262 {
263         const WCHAR* end; /* end of processed string */
264         const WCHAR* p;  /* search pointer */
265         const WCHAR* s;  /* copy pointer */
266
267         /* extract drive name */
268         if (path[0] && path[1]==':') {
269                 if (drv) {
270                         *drv++ = *path++;
271                         *drv++ = *path++;
272                         *drv = '\0';
273                 }
274         } else if (drv)
275                 *drv = '\0';
276
277         end = path + strlenW(path);
278
279         /* search for begin of file extension */
280         for(p=end; p>path && *--p!='\\' && *p!='/'; )
281                 if (*p == '.') {
282                         end = p;
283                         break;
284                 }
285
286         if (ext)
287                 for(s=end; (*ext=*s++); )
288                         ext++;
289
290         /* search for end of directory name */
291         for(p=end; p>path; )
292                 if (*--p=='\\' || *p=='/') {
293                         p++;
294                         break;
295                 }
296
297         if (name) {
298                 for(s=p; s<end; )
299                         *name++ = *s++;
300
301                 *name = '\0';
302         }
303
304         if (dir) {
305                 for(s=path; s<p; )
306                         *dir++ = *s++;
307
308                 *dir = '\0';
309         }
310 }
311
312 /****************************************************************************
313  * WCMD_HandleTildaModifiers
314  *
315  * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
316  *    %~xxxxxV  (V=0-9 or A-Z)
317  * Where xxxx is any combination of:
318  *    ~ - Removes quotes
319  *    f - Fully qualified path (assumes current dir if not drive\dir)
320  *    d - drive letter
321  *    p - path
322  *    n - filename
323  *    x - file extension
324  *    s - path with shortnames
325  *    a - attributes
326  *    t - date/time
327  *    z - size
328  *    $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
329  *                   qualified path
330  *
331  *  To work out the length of the modifier:
332  *
333  *  Note: In the case of %0-9 knowing the end of the modifier is easy,
334  *    but in a for loop, the for end WCHARacter may also be a modifier
335  *    eg. for %a in (c:\a.a) do echo XXX
336  *             where XXX = %~a    (just ~)
337  *                         %~aa   (~ and attributes)
338  *                         %~aaxa (~, attributes and extension)
339  *                   BUT   %~aax  (~ and attributes followed by 'x')
340  *
341  *  Hence search forwards until find an invalid modifier, and then
342  *  backwards until find for variable or 0-9
343  */
344 void WCMD_HandleTildaModifiers(WCHAR **start, const WCHAR *forVariable,
345                                const WCHAR *forValue, BOOL justFors) {
346
347 #define NUMMODIFIERS 11
348   static const WCHAR validmodifiers[NUMMODIFIERS] = {
349         '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
350   };
351
352   WIN32_FILE_ATTRIBUTE_DATA fileInfo;
353   WCHAR  outputparam[MAX_PATH];
354   WCHAR  finaloutput[MAX_PATH];
355   WCHAR  fullfilename[MAX_PATH];
356   WCHAR  thisoutput[MAX_PATH];
357   WCHAR  *pos            = *start+1;
358   WCHAR  *firstModifier  = pos;
359   WCHAR  *lastModifier   = NULL;
360   int   modifierLen     = 0;
361   BOOL  finished        = FALSE;
362   int   i               = 0;
363   BOOL  exists          = TRUE;
364   BOOL  skipFileParsing = FALSE;
365   BOOL  doneModifier    = FALSE;
366
367   /* Search forwards until find invalid character modifier */
368   while (!finished) {
369
370     /* Work on the previous character */
371     if (lastModifier != NULL) {
372
373       for (i=0; i<NUMMODIFIERS; i++) {
374         if (validmodifiers[i] == *lastModifier) {
375
376           /* Special case '$' to skip until : found */
377           if (*lastModifier == '$') {
378             while (*pos != ':' && *pos) pos++;
379             if (*pos == 0x00) return; /* Invalid syntax */
380             pos++;                    /* Skip ':'       */
381           }
382           break;
383         }
384       }
385
386       if (i==NUMMODIFIERS) {
387         finished = TRUE;
388       }
389     }
390
391     /* Save this one away */
392     if (!finished) {
393       lastModifier = pos;
394       pos++;
395     }
396   }
397
398   while (lastModifier > firstModifier) {
399     WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
400                wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
401
402     if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
403       /* Its a valid parameter identifier - OK */
404       break;
405
406     } else if (forVariable && *lastModifier == *(forVariable+1)) {
407       /* Its a valid parameter identifier - OK */
408       break;
409
410     } else {
411       lastModifier--;
412     }
413   }
414   if (lastModifier == firstModifier) return; /* Invalid syntax */
415
416   /* Extract the parameter to play with */
417   if (*lastModifier == '0') {
418     strcpyW(outputparam, context->batchfileW);
419   } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
420     strcpyW(outputparam,
421             WCMD_parameter (context -> command, *lastModifier-'0' + context -> shift_count[*lastModifier-'0'],
422                             NULL, NULL, FALSE));
423   } else {
424     strcpyW(outputparam, forValue);
425   }
426
427   /* So now, firstModifier points to beginning of modifiers, lastModifier
428      points to the variable just after the modifiers. Process modifiers
429      in a specific order, remembering there could be duplicates           */
430   modifierLen = lastModifier - firstModifier;
431   finaloutput[0] = 0x00;
432
433   /* Useful for debugging purposes: */
434   /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
435              (modifierLen), (modifierLen), firstModifier, *lastModifier,
436              outputparam);*/
437
438   /* 1. Handle '~' : Strip surrounding quotes */
439   if (outputparam[0]=='"' &&
440       memchrW(firstModifier, '~', modifierLen) != NULL) {
441     int len = strlenW(outputparam);
442     if (outputparam[len-1] == '"') {
443         outputparam[len-1]=0x00;
444         len = len - 1;
445     }
446     memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
447   }
448
449   /* 2. Handle the special case of a $ */
450   if (memchrW(firstModifier, '$', modifierLen) != NULL) {
451     /* Special Case: Search envar specified in $[envvar] for outputparam
452        Note both $ and : are guaranteed otherwise check above would fail */
453     WCHAR *begin = strchrW(firstModifier, '$') + 1;
454     WCHAR *end   = strchrW(firstModifier, ':');
455     WCHAR env[MAX_PATH];
456     WCHAR fullpath[MAX_PATH];
457
458     /* Extract the env var */
459     memcpy(env, begin, (end-begin) * sizeof(WCHAR));
460     env[(end-begin)] = 0x00;
461
462     /* If env var not found, return empty string */
463     if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
464         (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
465       finaloutput[0] = 0x00;
466       outputparam[0] = 0x00;
467       skipFileParsing = TRUE;
468     }
469   }
470
471   /* After this, we need full information on the file,
472     which is valid not to exist.  */
473   if (!skipFileParsing) {
474     if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
475       return;
476
477     exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
478                                   &fileInfo);
479
480     /* 2. Handle 'a' : Output attributes */
481     if (exists &&
482         memchrW(firstModifier, 'a', modifierLen) != NULL) {
483
484       WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
485       doneModifier = TRUE;
486       strcpyW(thisoutput, defaults);
487       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
488         thisoutput[0]='d';
489       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
490         thisoutput[1]='r';
491       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
492         thisoutput[2]='a';
493       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
494         thisoutput[3]='h';
495       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
496         thisoutput[4]='s';
497       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
498         thisoutput[5]='c';
499       /* FIXME: What are 6 and 7? */
500       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
501         thisoutput[8]='l';
502       strcatW(finaloutput, thisoutput);
503     }
504
505     /* 3. Handle 't' : Date+time */
506     if (exists &&
507         memchrW(firstModifier, 't', modifierLen) != NULL) {
508
509       SYSTEMTIME systime;
510       int datelen;
511
512       doneModifier = TRUE;
513       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
514
515       /* Format the time */
516       FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
517       GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
518                         NULL, thisoutput, MAX_PATH);
519       strcatW(thisoutput, spaceW);
520       datelen = strlenW(thisoutput);
521       GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
522                         NULL, (thisoutput+datelen), MAX_PATH-datelen);
523       strcatW(finaloutput, thisoutput);
524     }
525
526     /* 4. Handle 'z' : File length */
527     if (exists &&
528         memchrW(firstModifier, 'z', modifierLen) != NULL) {
529       /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
530       ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
531                                   fileInfo.nFileSizeLow;
532       static const WCHAR fmt[] = {'%','u','\0'};
533
534       doneModifier = TRUE;
535       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
536       wsprintfW(thisoutput, fmt, fullsize);
537       strcatW(finaloutput, thisoutput);
538     }
539
540     /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
541     if (memchrW(firstModifier, 's', modifierLen) != NULL) {
542       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
543       /* Don't flag as doneModifier - %~s on its own is processed later */
544       GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
545     }
546
547     /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
548     /*      Note this overrides d,p,n,x                                 */
549     if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
550       doneModifier = TRUE;
551       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
552       strcatW(finaloutput, fullfilename);
553     } else {
554
555       WCHAR drive[10];
556       WCHAR dir[MAX_PATH];
557       WCHAR fname[MAX_PATH];
558       WCHAR ext[MAX_PATH];
559       BOOL doneFileModifier = FALSE;
560
561       if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
562
563       /* Split into components */
564       WCMD_splitpath(fullfilename, drive, dir, fname, ext);
565
566       /* 5. Handle 'd' : Drive Letter */
567       if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
568         strcatW(finaloutput, drive);
569         doneModifier = TRUE;
570         doneFileModifier = TRUE;
571       }
572
573       /* 6. Handle 'p' : Path */
574       if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
575         strcatW(finaloutput, dir);
576         doneModifier = TRUE;
577         doneFileModifier = TRUE;
578       }
579
580       /* 7. Handle 'n' : Name */
581       if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
582         strcatW(finaloutput, fname);
583         doneModifier = TRUE;
584         doneFileModifier = TRUE;
585       }
586
587       /* 8. Handle 'x' : Ext */
588       if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
589         strcatW(finaloutput, ext);
590         doneModifier = TRUE;
591         doneFileModifier = TRUE;
592       }
593
594       /* If 's' but no other parameter, dump the whole thing */
595       if (!doneFileModifier &&
596           memchrW(firstModifier, 's', modifierLen) != NULL) {
597         doneModifier = TRUE;
598         if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
599         strcatW(finaloutput, outputparam);
600       }
601     }
602   }
603
604   /* If No other modifier processed,  just add in parameter */
605   if (!doneModifier) strcpyW(finaloutput, outputparam);
606
607   /* Finish by inserting the replacement into the string */
608   WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
609 }
610
611 /*******************************************************************
612  * WCMD_call - processes a batch call statement
613  *
614  *      If there is a leading ':', calls within this batch program
615  *      otherwise launches another program.
616  */
617 void WCMD_call (WCHAR *command) {
618
619   /* Run other program if no leading ':' */
620   if (*command != ':') {
621     WCMD_run_program(command, TRUE);
622   } else {
623
624     WCHAR gotoLabel[MAX_PATH];
625
626     strcpyW(gotoLabel, param1);
627
628     if (context) {
629
630       LARGE_INTEGER li;
631
632       /* Save the current file position, call the same file,
633          restore position                                    */
634       li.QuadPart = 0;
635       li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
636                      &li.u.HighPart, FILE_CURRENT);
637
638       WCMD_batch (param1, command, TRUE, gotoLabel, context->h);
639
640       SetFilePointer(context -> h, li.u.LowPart,
641                      &li.u.HighPart, FILE_BEGIN);
642     } else {
643       WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT));
644     }
645   }
646 }