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