cmd/tests: Add basic FOR /F tests.
[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, const WCHAR *forVariable,
295                                const WCHAR *forValue, BOOL justFors) {
296
297 #define NUMMODIFIERS 11
298   static const WCHAR validmodifiers[NUMMODIFIERS] = {
299         '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
300   };
301   static const WCHAR space[] = {' ', '\0'};
302
303   WIN32_FILE_ATTRIBUTE_DATA fileInfo;
304   WCHAR  outputparam[MAX_PATH];
305   WCHAR  finaloutput[MAX_PATH];
306   WCHAR  fullfilename[MAX_PATH];
307   WCHAR  thisoutput[MAX_PATH];
308   WCHAR  *pos            = *start+1;
309   WCHAR  *firstModifier  = pos;
310   WCHAR  *lastModifier   = NULL;
311   int   modifierLen     = 0;
312   BOOL  finished        = FALSE;
313   int   i               = 0;
314   BOOL  exists          = TRUE;
315   BOOL  skipFileParsing = FALSE;
316   BOOL  doneModifier    = FALSE;
317
318   /* Search forwards until find invalid character modifier */
319   while (!finished) {
320
321     /* Work on the previous character */
322     if (lastModifier != NULL) {
323
324       for (i=0; i<NUMMODIFIERS; i++) {
325         if (validmodifiers[i] == *lastModifier) {
326
327           /* Special case '$' to skip until : found */
328           if (*lastModifier == '$') {
329             while (*pos != ':' && *pos) pos++;
330             if (*pos == 0x00) return; /* Invalid syntax */
331             pos++;                    /* Skip ':'       */
332           }
333           break;
334         }
335       }
336
337       if (i==NUMMODIFIERS) {
338         finished = TRUE;
339       }
340     }
341
342     /* Save this one away */
343     if (!finished) {
344       lastModifier = pos;
345       pos++;
346     }
347   }
348
349   while (lastModifier > firstModifier) {
350     WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
351                wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
352
353     if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
354       /* Its a valid parameter identifier - OK */
355       break;
356
357     } else if (forVariable && *lastModifier == *(forVariable+1)) {
358       /* Its a valid parameter identifier - OK */
359       break;
360
361     } else {
362       lastModifier--;
363     }
364   }
365   if (lastModifier == firstModifier) return; /* Invalid syntax */
366
367   /* Extract the parameter to play with */
368   if (*lastModifier == '0') {
369     strcpyW(outputparam, context->batchfileW);
370   } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
371     strcpyW(outputparam, WCMD_parameter (context -> command,
372                  *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
373   } else {
374     strcpyW(outputparam, forValue);
375   }
376
377   /* So now, firstModifier points to beginning of modifiers, lastModifier
378      points to the variable just after the modifiers. Process modifiers
379      in a specific order, remembering there could be duplicates           */
380   modifierLen = lastModifier - firstModifier;
381   finaloutput[0] = 0x00;
382
383   /* Useful for debugging purposes: */
384   /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
385              (modifierLen), (modifierLen), firstModifier, *lastModifier,
386              outputparam);*/
387
388   /* 1. Handle '~' : Strip surrounding quotes */
389   if (outputparam[0]=='"' &&
390       memchrW(firstModifier, '~', modifierLen) != NULL) {
391     int len = strlenW(outputparam);
392     if (outputparam[len-1] == '"') {
393         outputparam[len-1]=0x00;
394         len = len - 1;
395     }
396     memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
397   }
398
399   /* 2. Handle the special case of a $ */
400   if (memchrW(firstModifier, '$', modifierLen) != NULL) {
401     /* Special Case: Search envar specified in $[envvar] for outputparam
402        Note both $ and : are guaranteed otherwise check above would fail */
403     WCHAR *begin = strchrW(firstModifier, '$') + 1;
404     WCHAR *end   = strchrW(firstModifier, ':');
405     WCHAR env[MAX_PATH];
406     WCHAR fullpath[MAX_PATH];
407
408     /* Extract the env var */
409     memcpy(env, begin, (end-begin) * sizeof(WCHAR));
410     env[(end-begin)] = 0x00;
411
412     /* If env var not found, return empty string */
413     if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
414         (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
415       finaloutput[0] = 0x00;
416       outputparam[0] = 0x00;
417       skipFileParsing = TRUE;
418     }
419   }
420
421   /* After this, we need full information on the file,
422     which is valid not to exist.  */
423   if (!skipFileParsing) {
424     if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
425       return;
426
427     exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
428                                   &fileInfo);
429
430     /* 2. Handle 'a' : Output attributes */
431     if (exists &&
432         memchrW(firstModifier, 'a', modifierLen) != NULL) {
433
434       WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
435       doneModifier = TRUE;
436       strcpyW(thisoutput, defaults);
437       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
438         thisoutput[0]='d';
439       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
440         thisoutput[1]='r';
441       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
442         thisoutput[2]='a';
443       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
444         thisoutput[3]='h';
445       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
446         thisoutput[4]='s';
447       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
448         thisoutput[5]='c';
449       /* FIXME: What are 6 and 7? */
450       if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
451         thisoutput[8]='l';
452       strcatW(finaloutput, thisoutput);
453     }
454
455     /* 3. Handle 't' : Date+time */
456     if (exists &&
457         memchrW(firstModifier, 't', modifierLen) != NULL) {
458
459       SYSTEMTIME systime;
460       int datelen;
461
462       doneModifier = TRUE;
463       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
464
465       /* Format the time */
466       FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
467       GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
468                         NULL, thisoutput, MAX_PATH);
469       strcatW(thisoutput, space);
470       datelen = strlenW(thisoutput);
471       GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
472                         NULL, (thisoutput+datelen), MAX_PATH-datelen);
473       strcatW(finaloutput, thisoutput);
474     }
475
476     /* 4. Handle 'z' : File length */
477     if (exists &&
478         memchrW(firstModifier, 'z', modifierLen) != NULL) {
479       /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
480       ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
481                                   fileInfo.nFileSizeLow;
482       static const WCHAR fmt[] = {'%','u','\0'};
483
484       doneModifier = TRUE;
485       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
486       wsprintfW(thisoutput, fmt, fullsize);
487       strcatW(finaloutput, thisoutput);
488     }
489
490     /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
491     if (memchrW(firstModifier, 's', modifierLen) != NULL) {
492       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
493       /* Don't flag as doneModifier - %~s on its own is processed later */
494       GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
495     }
496
497     /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
498     /*      Note this overrides d,p,n,x                                 */
499     if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
500       doneModifier = TRUE;
501       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
502       strcatW(finaloutput, fullfilename);
503     } else {
504
505       WCHAR drive[10];
506       WCHAR dir[MAX_PATH];
507       WCHAR fname[MAX_PATH];
508       WCHAR ext[MAX_PATH];
509       BOOL doneFileModifier = FALSE;
510
511       if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
512
513       /* Split into components */
514       WCMD_splitpath(fullfilename, drive, dir, fname, ext);
515
516       /* 5. Handle 'd' : Drive Letter */
517       if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
518         strcatW(finaloutput, drive);
519         doneModifier = TRUE;
520         doneFileModifier = TRUE;
521       }
522
523       /* 6. Handle 'p' : Path */
524       if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
525         strcatW(finaloutput, dir);
526         doneModifier = TRUE;
527         doneFileModifier = TRUE;
528       }
529
530       /* 7. Handle 'n' : Name */
531       if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
532         strcatW(finaloutput, fname);
533         doneModifier = TRUE;
534         doneFileModifier = TRUE;
535       }
536
537       /* 8. Handle 'x' : Ext */
538       if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
539         strcatW(finaloutput, ext);
540         doneModifier = TRUE;
541         doneFileModifier = TRUE;
542       }
543
544       /* If 's' but no other parameter, dump the whole thing */
545       if (!doneFileModifier &&
546           memchrW(firstModifier, 's', modifierLen) != NULL) {
547         doneModifier = TRUE;
548         if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
549         strcatW(finaloutput, outputparam);
550       }
551     }
552   }
553
554   /* If No other modifier processed,  just add in parameter */
555   if (!doneModifier) strcpyW(finaloutput, outputparam);
556
557   /* Finish by inserting the replacement into the string */
558   WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
559 }
560
561 /*******************************************************************
562  * WCMD_call - processes a batch call statement
563  *
564  *      If there is a leading ':', calls within this batch program
565  *      otherwise launches another program.
566  */
567 void WCMD_call (WCHAR *command) {
568
569   /* Run other program if no leading ':' */
570   if (*command != ':') {
571     WCMD_run_program(command, 1);
572   } else {
573
574     WCHAR gotoLabel[MAX_PATH];
575
576     strcpyW(gotoLabel, param1);
577
578     if (context) {
579
580       LARGE_INTEGER li;
581
582       /* Save the current file position, call the same file,
583          restore position                                    */
584       li.QuadPart = 0;
585       li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
586                      &li.u.HighPart, FILE_CURRENT);
587
588       WCMD_batch (param1, command, 1, gotoLabel, context->h);
589
590       SetFilePointer(context -> h, li.u.LowPart,
591                      &li.u.HighPart, FILE_BEGIN);
592     } else {
593       WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT));
594     }
595   }
596 }