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