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