msvcrt: Added _strtoi64 implementation.
[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 WINE_DEFAULT_DEBUG_CHANNEL(cmd);
26
27 extern int echo_mode;
28 extern WCHAR quals[MAX_PATH], param1[MAX_PATH], param2[MAX_PATH];
29 extern BATCH_CONTEXT *context;
30 extern DWORD errorlevel;
31
32 /****************************************************************************
33  * WCMD_batch
34  *
35  * Open and execute a batch file.
36  * On entry *command includes the complete command line beginning with the name
37  * of the batch file (if a CALL command was entered the CALL has been removed).
38  * *file is the name of the file, which might not exist and may not have the
39  * .BAT suffix on. Called is 1 for a CALL, 0 otherwise.
40  *
41  * We need to handle recursion correctly, since one batch program might call another.
42  * So parameters for this batch file are held in a BATCH_CONTEXT structure.
43  *
44  * To support call within the same batch program, another input parameter is
45  * a label to goto once opened.
46  */
47
48 void WCMD_batch (WCHAR *file, WCHAR *command, int called, WCHAR *startLabel, HANDLE pgmHandle) {
49
50   HANDLE h = INVALID_HANDLE_VALUE;
51   BATCH_CONTEXT *prev_context;
52
53   if (startLabel == NULL) {
54     h = CreateFileW (file, GENERIC_READ, FILE_SHARE_READ,
55                      NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
56     if (h == INVALID_HANDLE_VALUE) {
57       SetLastError (ERROR_FILE_NOT_FOUND);
58       WCMD_print_error ();
59       return;
60     }
61   } else {
62     DuplicateHandle(GetCurrentProcess(), pgmHandle,
63                     GetCurrentProcess(), &h,
64                     0, FALSE, DUPLICATE_SAME_ACCESS);
65   }
66
67 /*
68  *      Create a context structure for this batch file.
69  */
70
71   prev_context = context;
72   context = LocalAlloc (LMEM_FIXED, sizeof (BATCH_CONTEXT));
73   context -> h = h;
74   context->batchfileW = WCMD_strdupW(file);
75   context -> command = command;
76   memset(context -> shift_count, 0x00, sizeof(context -> shift_count));
77   context -> prev_context = prev_context;
78   context -> skip_rest = FALSE;
79
80   /* If processing a call :label, 'goto' the label in question */
81   if (startLabel) {
82     strcpyW(param1, startLabel);
83     WCMD_goto(NULL);
84   }
85
86 /*
87  *      Work through the file line by line. Specific batch commands are processed here,
88  *      the rest are handled by the main command processor.
89  */
90
91   while (context -> skip_rest == FALSE) {
92       CMD_LIST *toExecute = NULL;         /* Commands left to be executed */
93       if (WCMD_ReadAndParseLine(NULL, &toExecute, h) == NULL)
94         break;
95       WCMD_process_commands(toExecute, FALSE, NULL, NULL);
96       WCMD_free_commands(toExecute);
97       toExecute = NULL;
98   }
99   CloseHandle (h);
100
101 /*
102  *      If invoked by a CALL, we return to the context of our caller. Otherwise return
103  *      to the caller's caller.
104  */
105
106   HeapFree(GetProcessHeap(), 0, context->batchfileW);
107   LocalFree (context);
108   if ((prev_context != NULL) && (!called)) {
109     prev_context -> skip_rest = TRUE;
110     context = prev_context;
111   }
112   context = prev_context;
113 }
114
115 /*******************************************************************
116  * WCMD_parameter - extract a parameter from a command line.
117  *
118  *      Returns the 'n'th delimited parameter on the command line (zero-based).
119  *      Parameter is in static storage overwritten on the next call.
120  *      Parameters in quotes (and brackets) are handled.
121  *      Also returns a pointer to the location of the parameter in the command line.
122  */
123
124 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **where) {
125
126   int i = 0;
127   static WCHAR param[MAX_PATH];
128   WCHAR *p;
129
130   if (where != NULL) *where = NULL;
131   p = param;
132   while (TRUE) {
133     switch (*s) {
134       case ' ': /* Skip leading spaces */
135       case '\t': /* Treat tabs as spaces */
136         s++;
137         break;
138       case '"':
139         if (where != NULL && i==n) *where = s;
140         s++;
141         while ((*s != '\0') && (*s != '"')) {
142           *p++ = *s++;
143         }
144         if (i == n) {
145           *p = '\0';
146           return param;
147         }
148         if (*s == '"') s++;
149           param[0] = '\0';
150           i++;
151         p = param;
152         break;
153       /* The code to handle bracketed parms is removed because it should no longer
154          be necessary after the multiline support has been added and the for loop
155          set of data is now parseable individually. */
156       case '\0':
157         return param;
158       default:
159         /* Only return where if it is for the right parameter */
160         if (where != NULL && i==n) *where = s;
161         while ((*s != '\0') && (*s != ' ') && (*s != ',') && (*s != '=') && (*s != '\t')) {
162           *p++ = *s++;
163         }
164         if (i == n && (p!=param)) {
165           *p = '\0';
166           return param;
167         }
168         /* Skip double delimiters, eg. dir a.a,,,,,b.b */
169         if (p != param) {
170           param[0] = '\0';
171           i++;
172         } else {
173           s++; /* Skip delimiter */
174         }
175         p = param;
176     }
177   }
178 }
179
180 /****************************************************************************
181  * WCMD_fgets
182  *
183  * Get one line from a batch file. We can't use the native f* functions because
184  * of the filename syntax differences between DOS and Unix. Also need to lose
185  * the LF (or CRLF) from the line.
186  */
187
188 WCHAR *WCMD_fgets (WCHAR *s, int noChars, HANDLE h) {
189
190   DWORD bytes;
191   BOOL status;
192   WCHAR *p;
193
194   p = s;
195   do {
196     status = WCMD_ReadFile (h, s, 1, &bytes, NULL);
197     if ((status == 0) || ((bytes == 0) && (s == p))) return NULL;
198     if (*s == '\n') bytes = 0;
199     else if (*s != '\r') {
200       s++;
201       noChars--;
202     }
203     *s = '\0';
204   } while ((bytes == 1) && (noChars > 1));
205   return p;
206 }
207
208 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
209 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
210 {
211         const WCHAR* end; /* end of processed string */
212         const WCHAR* p;  /* search pointer */
213         const WCHAR* s;  /* copy pointer */
214
215         /* extract drive name */
216         if (path[0] && path[1]==':') {
217                 if (drv) {
218                         *drv++ = *path++;
219                         *drv++ = *path++;
220                         *drv = '\0';
221                 }
222         } else if (drv)
223                 *drv = '\0';
224
225         /* search for end of string or stream separator */
226         for(end=path; *end && *end!=':'; )
227                 end++;
228
229         /* search for begin of file extension */
230         for(p=end; p>path && *--p!='\\' && *p!='/'; )
231                 if (*p == '.') {
232                         end = p;
233                         break;
234                 }
235
236         if (ext)
237                 for(s=end; (*ext=*s++); )
238                         ext++;
239
240         /* search for end of directory name */
241         for(p=end; p>path; )
242                 if (*--p=='\\' || *p=='/') {
243                         p++;
244                         break;
245                 }
246
247         if (name) {
248                 for(s=p; s<end; )
249                         *name++ = *s++;
250
251                 *name = '\0';
252         }
253
254         if (dir) {
255                 for(s=path; s<p; )
256                         *dir++ = *s++;
257
258                 *dir = '\0';
259         }
260 }
261
262 /****************************************************************************
263  * WCMD_HandleTildaModifiers
264  *
265  * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
266  *    %~xxxxxV  (V=0-9 or A-Z)
267  * Where xxxx is any combination of:
268  *    ~ - Removes quotes
269  *    f - Fully qualified path (assumes current dir if not drive\dir)
270  *    d - drive letter
271  *    p - path
272  *    n - filename
273  *    x - file extension
274  *    s - path with shortnames
275  *    a - attributes
276  *    t - date/time
277  *    z - size
278  *    $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
279  *                   qualified path
280  *
281  *  To work out the length of the modifier:
282  *
283  *  Note: In the case of %0-9 knowing the end of the modifier is easy,
284  *    but in a for loop, the for end WCHARacter may also be a modifier
285  *    eg. for %a in (c:\a.a) do echo XXX
286  *             where XXX = %~a    (just ~)
287  *                         %~aa   (~ and attributes)
288  *                         %~aaxa (~, attributes and extension)
289  *                   BUT   %~aax  (~ and attributes followed by 'x')
290  *
291  *  Hence search forwards until find an invalid modifier, and then
292  *  backwards until find for variable or 0-9
293  */
294 void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable, WCHAR *forValue, BOOL justFors) {
295
296 #define NUMMODIFIERS 11
297   static const WCHAR validmodifiers[NUMMODIFIERS] = {
298         '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
299   };
300   static const WCHAR space[] = {' ', '\0'};
301
302   WIN32_FILE_ATTRIBUTE_DATA fileInfo;
303   WCHAR  outputparam[MAX_PATH];
304   WCHAR  finaloutput[MAX_PATH];
305   WCHAR  fullfilename[MAX_PATH];
306   WCHAR  thisoutput[MAX_PATH];
307   WCHAR  *pos            = *start+1;
308   WCHAR  *firstModifier  = pos;
309   WCHAR  *lastModifier   = NULL;
310   int   modifierLen     = 0;
311   BOOL  finished        = FALSE;
312   int   i               = 0;
313   BOOL  exists          = TRUE;
314   BOOL  skipFileParsing = FALSE;
315   BOOL  doneModifier    = FALSE;
316
317   /* Search forwards until find invalid character modifier */
318   while (!finished) {
319
320     /* Work on the previous character */
321     if (lastModifier != NULL) {
322
323       for (i=0; i<NUMMODIFIERS; i++) {
324         if (validmodifiers[i] == *lastModifier) {
325
326           /* Special case '$' to skip until : found */
327           if (*lastModifier == '$') {
328             while (*pos != ':' && *pos) pos++;
329             if (*pos == 0x00) return; /* Invalid syntax */
330             pos++;                    /* Skip ':'       */
331           }
332           break;
333         }
334       }
335
336       if (i==NUMMODIFIERS) {
337         finished = TRUE;
338       }
339     }
340
341     /* Save this one away */
342     if (!finished) {
343       lastModifier = pos;
344       pos++;
345     }
346   }
347
348   while (lastModifier > firstModifier) {
349     WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
350                wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
351
352     if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
353       /* Its a valid parameter identifier - OK */
354       break;
355
356     } else if (forVariable && *lastModifier == *(forVariable+1)) {
357       /* Its a valid parameter identifier - OK */
358       break;
359
360     } else {
361       lastModifier--;
362     }
363   }
364   if (lastModifier == firstModifier) return; /* Invalid syntax */
365
366   /* Extract the parameter to play with */
367   if (*lastModifier == '0') {
368     strcpyW(outputparam, context->batchfileW);
369   } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
370     strcpyW(outputparam, WCMD_parameter (context -> command,
371                  *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
372   } else {
373     strcpyW(outputparam, forValue);
374   }
375
376   /* So now, firstModifier points to beginning of modifiers, lastModifier
377      points to the variable just after the modifiers. Process modifiers
378      in a specific order, remembering there could be duplicates           */
379   modifierLen = lastModifier - firstModifier;
380   finaloutput[0] = 0x00;
381
382   /* Useful for debugging purposes: */
383   /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
384              (modifierLen), (modifierLen), firstModifier, *lastModifier,
385              outputparam);*/
386
387   /* 1. Handle '~' : Strip surrounding quotes */
388   if (outputparam[0]=='"' &&
389       memchrW(firstModifier, '~', modifierLen) != NULL) {
390     int len = strlenW(outputparam);
391     if (outputparam[len-1] == '"') {
392         outputparam[len-1]=0x00;
393         len = len - 1;
394     }
395     memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
396   }
397
398   /* 2. Handle the special case of a $ */
399   if (memchrW(firstModifier, '$', modifierLen) != NULL) {
400     /* Special Case: Search envar specified in $[envvar] for outputparam
401        Note both $ and : are guaranteed otherwise check above would fail */
402     WCHAR *start = strchrW(firstModifier, '$') + 1;
403     WCHAR *end   = strchrW(firstModifier, ':');
404     WCHAR env[MAX_PATH];
405     WCHAR fullpath[MAX_PATH];
406
407     /* Extract the env var */
408     memcpy(env, start, (end-start) * sizeof(WCHAR));
409     env[(end-start)] = 0x00;
410
411     /* If env var not found, return empty string */
412     if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
413         (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
414       finaloutput[0] = 0x00;
415       outputparam[0] = 0x00;
416       skipFileParsing = TRUE;
417     }
418   }
419
420   /* After this, we need full information on the file,
421     which is valid not to exist.  */
422   if (!skipFileParsing) {
423     if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
424       return;
425
426     exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
427                                   &fileInfo);
428
429     /* 2. Handle 'a' : Output attributes */
430     if (exists &&
431         memchrW(firstModifier, 'a', modifierLen) != NULL) {
432
433       WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
434       doneModifier = TRUE;
435       strcpyW(thisoutput, defaults);
436       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
437         thisoutput[0]='d';
438       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
439         thisoutput[1]='r';
440       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
441         thisoutput[2]='a';
442       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
443         thisoutput[3]='h';
444       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
445         thisoutput[4]='s';
446       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
447         thisoutput[5]='c';
448       /* FIXME: What are 6 and 7? */
449       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
450         thisoutput[8]='l';
451       strcatW(finaloutput, thisoutput);
452     }
453
454     /* 3. Handle 't' : Date+time */
455     if (exists &&
456         memchrW(firstModifier, 't', modifierLen) != NULL) {
457
458       SYSTEMTIME systime;
459       int datelen;
460
461       doneModifier = TRUE;
462       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
463
464       /* Format the time */
465       FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
466       GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
467                         NULL, thisoutput, MAX_PATH);
468       strcatW(thisoutput, space);
469       datelen = strlenW(thisoutput);
470       GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
471                         NULL, (thisoutput+datelen), MAX_PATH-datelen);
472       strcatW(finaloutput, thisoutput);
473     }
474
475     /* 4. Handle 'z' : File length */
476     if (exists &&
477         memchrW(firstModifier, 'z', modifierLen) != NULL) {
478       /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
479       ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
480                                   fileInfo.nFileSizeLow;
481       static const WCHAR fmt[] = {'%','u','\0'};
482
483       doneModifier = TRUE;
484       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
485       wsprintfW(thisoutput, fmt, fullsize);
486       strcatW(finaloutput, thisoutput);
487     }
488
489     /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
490     if (memchrW(firstModifier, 's', modifierLen) != NULL) {
491       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
492       /* Don't flag as doneModifier - %~s on its own is processed later */
493       GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
494     }
495
496     /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
497     /*      Note this overrides d,p,n,x                                 */
498     if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
499       doneModifier = TRUE;
500       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
501       strcatW(finaloutput, fullfilename);
502     } else {
503
504       WCHAR drive[10];
505       WCHAR dir[MAX_PATH];
506       WCHAR fname[MAX_PATH];
507       WCHAR ext[MAX_PATH];
508       BOOL doneFileModifier = FALSE;
509
510       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
511
512       /* Split into components */
513       WCMD_splitpath(fullfilename, drive, dir, fname, ext);
514
515       /* 5. Handle 'd' : Drive Letter */
516       if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
517         strcatW(finaloutput, drive);
518         doneModifier = TRUE;
519         doneFileModifier = TRUE;
520       }
521
522       /* 6. Handle 'p' : Path */
523       if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
524         strcatW(finaloutput, dir);
525         doneModifier = TRUE;
526         doneFileModifier = TRUE;
527       }
528
529       /* 7. Handle 'n' : Name */
530       if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
531         strcatW(finaloutput, fname);
532         doneModifier = TRUE;
533         doneFileModifier = TRUE;
534       }
535
536       /* 8. Handle 'x' : Ext */
537       if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
538         strcatW(finaloutput, ext);
539         doneModifier = TRUE;
540         doneFileModifier = TRUE;
541       }
542
543       /* If 's' but no other parameter, dump the whole thing */
544       if (!doneFileModifier &&
545           memchrW(firstModifier, 's', modifierLen) != NULL) {
546         doneModifier = TRUE;
547         if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
548         strcatW(finaloutput, outputparam);
549       }
550     }
551   }
552
553   /* If No other modifier processed,  just add in parameter */
554   if (!doneModifier) strcpyW(finaloutput, outputparam);
555
556   /* Finish by inserting the replacement into the string */
557   WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
558 }
559
560 /*******************************************************************
561  * WCMD_call - processes a batch call statement
562  *
563  *      If there is a leading ':', calls within this batch program
564  *      otherwise launches another program.
565  */
566 void WCMD_call (WCHAR *command) {
567
568   /* Run other program if no leading ':' */
569   if (*command != ':') {
570     WCMD_run_program(command, 1);
571   } else {
572
573     WCHAR gotoLabel[MAX_PATH];
574
575     strcpyW(gotoLabel, param1);
576
577     if (context) {
578
579       LARGE_INTEGER li;
580
581       /* Save the current file position, call the same file,
582          restore position                                    */
583       li.QuadPart = 0;
584       li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
585                      &li.u.HighPart, FILE_CURRENT);
586
587       WCMD_batch (param1, command, 1, gotoLabel, context->h);
588
589       SetFilePointer(context -> h, li.u.LowPart,
590                      &li.u.HighPart, FILE_BEGIN);
591     } else {
592       WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT));
593     }
594   }
595 }