cmd.exe: Fix FOR so it works as 'well' as before.
[wine.git] / programs / cmd / batch.c
blob8f0187c88561357094dc711c266ef970dce1339f
1 /*
2 * CMD - Wine-compatible command line interface - batch interface.
4 * Copyright (C) 1999 D A Pickles
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.
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.
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
21 #include "wcmd.h"
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;
28 /****************************************************************************
29 * WCMD_batch
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.
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.
40 * To support call within the same batch program, another input parameter is
41 * a label to goto once opened.
44 void WCMD_batch (WCHAR *file, WCHAR *command, int called, WCHAR *startLabel, HANDLE pgmHandle) {
46 #define WCMD_BATCH_EXT_SIZE 5
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;
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);
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 ();
77 return;
79 } else {
80 DuplicateHandle(GetCurrentProcess(), pgmHandle,
81 GetCurrentProcess(), &h,
82 0, FALSE, DUPLICATE_SAME_ACCESS);
86 * Create a context structure for this batch file.
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;
97 /* If processing a call :label, 'goto' the label in question */
98 if (startLabel) {
99 strcpyW(param1, startLabel);
100 WCMD_goto(NULL);
104 * Work through the file line by line. Specific batch commands are processed here,
105 * the rest are handled by the main command processor.
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;
116 CloseHandle (h);
119 * If invoked by a CALL, we return to the context of our caller. Otherwise return
120 * to the caller's caller.
123 LocalFree ((HANDLE)context);
124 if ((prev_context != NULL) && (!called)) {
125 CloseHandle (prev_context -> h);
126 context = prev_context -> prev_context;
127 LocalFree ((HANDLE)prev_context);
129 else {
130 context = prev_context;
134 /*******************************************************************
135 * WCMD_parameter - extract a parameter from a command line.
137 * Returns the 'n'th space-delimited parameter on the command line (zero-based).
138 * Parameter is in static storage overwritten on the next call.
139 * Parameters in quotes (and brackets) are handled.
140 * Also returns a pointer to the location of the parameter in the command line.
143 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **where) {
145 int i = 0;
146 static WCHAR param[MAX_PATH];
147 WCHAR *p;
149 if (where != NULL) *where = NULL;
150 p = param;
151 while (TRUE) {
152 switch (*s) {
153 case ' ':
154 s++;
155 break;
156 case '"':
157 if (where != NULL && i==n) *where = s;
158 s++;
159 while ((*s != '\0') && (*s != '"')) {
160 *p++ = *s++;
162 if (i == n) {
163 *p = '\0';
164 return param;
166 if (*s == '"') s++;
167 param[0] = '\0';
168 i++;
169 p = param;
170 break;
171 /* The code to handle bracketed parms is removed because it should no longer
172 be necessary after the multiline support has been added and the for loop
173 set of data is now parseable individually. */
174 case '\0':
175 return param;
176 default:
177 /* Only return where if it is for the right parameter */
178 if (where != NULL && i==n) *where = s;
179 while ((*s != '\0') && (*s != ' ')) {
180 *p++ = *s++;
182 if (i == n) {
183 *p = '\0';
184 return param;
186 param[0] = '\0';
187 i++;
188 p = param;
193 /****************************************************************************
194 * WCMD_fgets
196 * Get one line from a batch file. We can't use the native f* functions because
197 * of the filename syntax differences between DOS and Unix. Also need to lose
198 * the LF (or CRLF) from the line.
201 WCHAR *WCMD_fgets (WCHAR *s, int noChars, HANDLE h) {
203 DWORD bytes;
204 BOOL status;
205 WCHAR *p;
207 p = s;
208 do {
209 status = WCMD_ReadFile (h, s, 1, &bytes, NULL);
210 if ((status == 0) || ((bytes == 0) && (s == p))) return NULL;
211 if (*s == '\n') bytes = 0;
212 else if (*s != '\r') {
213 s++;
214 noChars--;
216 *s = '\0';
217 } while ((bytes == 1) && (noChars > 1));
218 return p;
221 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
222 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
224 const WCHAR* end; /* end of processed string */
225 const WCHAR* p; /* search pointer */
226 const WCHAR* s; /* copy pointer */
228 /* extract drive name */
229 if (path[0] && path[1]==':') {
230 if (drv) {
231 *drv++ = *path++;
232 *drv++ = *path++;
233 *drv = '\0';
235 } else if (drv)
236 *drv = '\0';
238 /* search for end of string or stream separator */
239 for(end=path; *end && *end!=':'; )
240 end++;
242 /* search for begin of file extension */
243 for(p=end; p>path && *--p!='\\' && *p!='/'; )
244 if (*p == '.') {
245 end = p;
246 break;
249 if (ext)
250 for(s=end; (*ext=*s++); )
251 ext++;
253 /* search for end of directory name */
254 for(p=end; p>path; )
255 if (*--p=='\\' || *p=='/') {
256 p++;
257 break;
260 if (name) {
261 for(s=p; s<end; )
262 *name++ = *s++;
264 *name = '\0';
267 if (dir) {
268 for(s=path; s<p; )
269 *dir++ = *s++;
271 *dir = '\0';
275 /****************************************************************************
276 * WCMD_HandleTildaModifiers
278 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
279 * %~xxxxxV (V=0-9 or A-Z)
280 * Where xxxx is any combination of:
281 * ~ - Removes quotes
282 * f - Fully qualified path (assumes current dir if not drive\dir)
283 * d - drive letter
284 * p - path
285 * n - filename
286 * x - file extension
287 * s - path with shortnames
288 * a - attributes
289 * t - date/time
290 * z - size
291 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
292 * qualified path
294 * To work out the length of the modifier:
296 * Note: In the case of %0-9 knowing the end of the modifier is easy,
297 * but in a for loop, the for end WCHARacter may also be a modifier
298 * eg. for %a in (c:\a.a) do echo XXX
299 * where XXX = %~a (just ~)
300 * %~aa (~ and attributes)
301 * %~aaxa (~, attributes and extension)
302 * BUT %~aax (~ and attributes followed by 'x')
304 * Hence search forwards until find an invalid modifier, and then
305 * backwards until find for variable or 0-9
307 void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable) {
309 #define NUMMODIFIERS 11
310 static const WCHAR validmodifiers[NUMMODIFIERS] = {
311 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
313 static const WCHAR space[] = {' ', '\0'};
315 WIN32_FILE_ATTRIBUTE_DATA fileInfo;
316 WCHAR outputparam[MAX_PATH];
317 WCHAR finaloutput[MAX_PATH];
318 WCHAR fullfilename[MAX_PATH];
319 WCHAR thisoutput[MAX_PATH];
320 WCHAR *pos = *start+1;
321 WCHAR *firstModifier = pos;
322 WCHAR *lastModifier = NULL;
323 int modifierLen = 0;
324 BOOL finished = FALSE;
325 int i = 0;
326 BOOL exists = TRUE;
327 BOOL skipFileParsing = FALSE;
328 BOOL doneModifier = FALSE;
330 /* Search forwards until find invalid WCHARacter modifier */
331 while (!finished) {
333 /* Work on the previous WCHARacter */
334 if (lastModifier != NULL) {
336 for (i=0; i<NUMMODIFIERS; i++) {
337 if (validmodifiers[i] == *lastModifier) {
339 /* Special case '$' to skip until : found */
340 if (*lastModifier == '$') {
341 while (*pos != ':' && *pos) pos++;
342 if (*pos == 0x00) return; /* Invalid syntax */
343 pos++; /* Skip ':' */
345 break;
349 if (i==NUMMODIFIERS) {
350 finished = TRUE;
354 /* Save this one away */
355 if (!finished) {
356 lastModifier = pos;
357 pos++;
361 /* Now make sure the position we stopped at is a valid parameter */
362 if (!(*lastModifier >= '0' || *lastModifier <= '9') &&
363 (forVariable != NULL) &&
364 (toupperW(*lastModifier) != toupperW(*forVariable))) {
366 /* Its not... Step backwards until it matches or we get to the start */
367 while (toupperW(*lastModifier) != toupperW(*forVariable) &&
368 lastModifier > firstModifier) {
369 lastModifier--;
371 if (lastModifier == firstModifier) return; /* Invalid syntax */
374 /* Extract the parameter to play with */
375 if ((*lastModifier >= '0' && *lastModifier <= '9')) {
376 strcpyW(outputparam, WCMD_parameter (context -> command,
377 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
378 } else {
379 /* FIXME: Retrieve 'for' variable %c\n", *lastModifier); */
380 /* Need to get 'for' loop variable into outputparam */
381 return;
384 /* So now, firstModifier points to beginning of modifiers, lastModifier
385 points to the variable just after the modifiers. Process modifiers
386 in a specific order, remembering there could be duplicates */
387 modifierLen = lastModifier - firstModifier;
388 finaloutput[0] = 0x00;
390 /* Useful for debugging purposes: */
391 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
392 (modifierLen), (modifierLen), firstModifier, *lastModifier,
393 outputparam);*/
395 /* 1. Handle '~' : Strip surrounding quotes */
396 if (outputparam[0]=='"' &&
397 memchrW(firstModifier, '~', modifierLen) != NULL) {
398 int len = strlenW(outputparam);
399 if (outputparam[len-1] == '"') {
400 outputparam[len-1]=0x00;
401 len = len - 1;
403 memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
406 /* 2. Handle the special case of a $ */
407 if (memchrW(firstModifier, '$', modifierLen) != NULL) {
408 /* Special Case: Search envar specified in $[envvar] for outputparam
409 Note both $ and : are guaranteed otherwise check above would fail */
410 WCHAR *start = strchrW(firstModifier, '$') + 1;
411 WCHAR *end = strchrW(firstModifier, ':');
412 WCHAR env[MAX_PATH];
413 WCHAR fullpath[MAX_PATH];
415 /* Extract the env var */
416 memcpy(env, start, (end-start) * sizeof(WCHAR));
417 env[(end-start)] = 0x00;
419 /* If env var not found, return emptry string */
420 if ((GetEnvironmentVariable(env, fullpath, MAX_PATH) == 0) ||
421 (SearchPath(fullpath, outputparam, NULL,
422 MAX_PATH, outputparam, NULL) == 0)) {
423 finaloutput[0] = 0x00;
424 outputparam[0] = 0x00;
425 skipFileParsing = TRUE;
429 /* After this, we need full information on the file,
430 which is valid not to exist. */
431 if (!skipFileParsing) {
432 if (GetFullPathName(outputparam, MAX_PATH, fullfilename, NULL) == 0)
433 return;
435 exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
436 &fileInfo);
438 /* 2. Handle 'a' : Output attributes */
439 if (exists &&
440 memchrW(firstModifier, 'a', modifierLen) != NULL) {
442 WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
443 doneModifier = TRUE;
444 strcpyW(thisoutput, defaults);
445 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
446 thisoutput[0]='d';
447 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
448 thisoutput[1]='r';
449 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
450 thisoutput[2]='a';
451 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
452 thisoutput[3]='h';
453 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
454 thisoutput[4]='s';
455 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
456 thisoutput[5]='c';
457 /* FIXME: What are 6 and 7? */
458 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
459 thisoutput[8]='l';
460 strcatW(finaloutput, thisoutput);
463 /* 3. Handle 't' : Date+time */
464 if (exists &&
465 memchrW(firstModifier, 't', modifierLen) != NULL) {
467 SYSTEMTIME systime;
468 int datelen;
470 doneModifier = TRUE;
471 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
473 /* Format the time */
474 FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
475 GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
476 NULL, thisoutput, MAX_PATH);
477 strcatW(thisoutput, space);
478 datelen = strlenW(thisoutput);
479 GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
480 NULL, (thisoutput+datelen), MAX_PATH-datelen);
481 strcatW(finaloutput, thisoutput);
484 /* 4. Handle 'z' : File length */
485 if (exists &&
486 memchrW(firstModifier, 'z', modifierLen) != NULL) {
487 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
488 ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
489 fileInfo.nFileSizeLow;
490 static const WCHAR fmt[] = {'%','u','\0'};
492 doneModifier = TRUE;
493 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
494 wsprintf(thisoutput, fmt, fullsize);
495 strcatW(finaloutput, thisoutput);
498 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
499 if (memchrW(firstModifier, 's', modifierLen) != NULL) {
500 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
501 /* Don't flag as doneModifier - %~s on its own is processed later */
502 GetShortPathName(outputparam, outputparam, sizeof(outputparam));
505 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
506 /* Note this overrides d,p,n,x */
507 if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
508 doneModifier = TRUE;
509 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
510 strcatW(finaloutput, fullfilename);
511 } else {
513 WCHAR drive[10];
514 WCHAR dir[MAX_PATH];
515 WCHAR fname[MAX_PATH];
516 WCHAR ext[MAX_PATH];
517 BOOL doneFileModifier = FALSE;
519 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
521 /* Split into components */
522 WCMD_splitpath(fullfilename, drive, dir, fname, ext);
524 /* 5. Handle 'd' : Drive Letter */
525 if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
526 strcatW(finaloutput, drive);
527 doneModifier = TRUE;
528 doneFileModifier = TRUE;
531 /* 6. Handle 'p' : Path */
532 if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
533 strcatW(finaloutput, dir);
534 doneModifier = TRUE;
535 doneFileModifier = TRUE;
538 /* 7. Handle 'n' : Name */
539 if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
540 strcatW(finaloutput, fname);
541 doneModifier = TRUE;
542 doneFileModifier = TRUE;
545 /* 8. Handle 'x' : Ext */
546 if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
547 strcatW(finaloutput, ext);
548 doneModifier = TRUE;
549 doneFileModifier = TRUE;
552 /* If 's' but no other parameter, dump the whole thing */
553 if (!doneFileModifier &&
554 memchrW(firstModifier, 's', modifierLen) != NULL) {
555 doneModifier = TRUE;
556 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
557 strcatW(finaloutput, outputparam);
562 /* If No other modifier processed, just add in parameter */
563 if (!doneModifier) strcpyW(finaloutput, outputparam);
565 /* Finish by inserting the replacement into the string */
566 pos = WCMD_strdupW(lastModifier+1);
567 strcpyW(*start, finaloutput);
568 strcatW(*start, pos);
569 free(pos);
572 /*******************************************************************
573 * WCMD_call - processes a batch call statement
575 * If there is a leading ':', calls within this batch program
576 * otherwise launches another program.
578 void WCMD_call (WCHAR *command) {
580 /* Run other program if no leading ':' */
581 if (*command != ':') {
582 WCMD_run_program(command, 1);
583 } else {
585 WCHAR gotoLabel[MAX_PATH];
587 strcpyW(gotoLabel, param1);
589 if (context) {
591 LARGE_INTEGER li;
593 /* Save the current file position, call the same file,
594 restore position */
595 li.QuadPart = 0;
596 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
597 &li.u.HighPart, FILE_CURRENT);
599 WCMD_batch (param1, command, 1, gotoLabel, context->h);
601 SetFilePointer(context -> h, li.u.LowPart,
602 &li.u.HighPart, FILE_BEGIN);
603 } else {
604 WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT));