winemaker: Canonicalize directory name if used as target name.
[wine/hacks.git] / programs / cmd / batch.c
blob0044f70dcf0d1fd422250e715fe8057c515c090c
1 /*
2 * CMD - Wine-compatible command line interface - batch interface.
4 * Copyright (C) 1999 D A Pickles
5 * Copyright (C) 2007 J Edmeades
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.
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.
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
22 #include "wcmd.h"
23 #include "wine/debug.h"
25 WINE_DEFAULT_DEBUG_CHANNEL(cmd);
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;
32 /****************************************************************************
33 * WCMD_batch
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.
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.
44 * To support call within the same batch program, another input parameter is
45 * a label to goto once opened.
48 void WCMD_batch (WCHAR *file, WCHAR *command, int called, WCHAR *startLabel, HANDLE pgmHandle) {
50 #define WCMD_BATCH_EXT_SIZE 5
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;
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);
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 ();
79 return;
81 } else {
82 DuplicateHandle(GetCurrentProcess(), pgmHandle,
83 GetCurrentProcess(), &h,
84 0, FALSE, DUPLICATE_SAME_ACCESS);
88 * Create a context structure for this batch file.
91 prev_context = context;
92 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;
99 /* If processing a call :label, 'goto' the label in question */
100 if (startLabel) {
101 strcpyW(param1, startLabel);
102 WCMD_goto(NULL);
106 * Work through the file line by line. Specific batch commands are processed here,
107 * the rest are handled by the main command processor.
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;
118 CloseHandle (h);
121 * If invoked by a CALL, we return to the context of our caller. Otherwise return
122 * to the caller's caller.
125 LocalFree (context);
126 if ((prev_context != NULL) && (!called)) {
127 prev_context -> skip_rest = TRUE;
128 context = prev_context;
130 context = prev_context;
133 /*******************************************************************
134 * WCMD_parameter - extract a parameter from a command line.
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.
142 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **where) {
144 int i = 0;
145 static WCHAR param[MAX_PATH];
146 WCHAR *p;
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++;
161 if (i == n) {
162 *p = '\0';
163 return param;
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++;
181 if (i == n && (p!=param)) {
182 *p = '\0';
183 return param;
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 delimiter */
192 p = param;
197 /****************************************************************************
198 * WCMD_fgets
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.
205 WCHAR *WCMD_fgets (WCHAR *s, int noChars, HANDLE h) {
207 DWORD bytes;
208 BOOL status;
209 WCHAR *p;
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--;
220 *s = '\0';
221 } while ((bytes == 1) && (noChars > 1));
222 return p;
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)
228 const WCHAR* end; /* end of processed string */
229 const WCHAR* p; /* search pointer */
230 const WCHAR* s; /* copy pointer */
232 /* extract drive name */
233 if (path[0] && path[1]==':') {
234 if (drv) {
235 *drv++ = *path++;
236 *drv++ = *path++;
237 *drv = '\0';
239 } else if (drv)
240 *drv = '\0';
242 /* search for end of string or stream separator */
243 for(end=path; *end && *end!=':'; )
244 end++;
246 /* search for begin of file extension */
247 for(p=end; p>path && *--p!='\\' && *p!='/'; )
248 if (*p == '.') {
249 end = p;
250 break;
253 if (ext)
254 for(s=end; (*ext=*s++); )
255 ext++;
257 /* search for end of directory name */
258 for(p=end; p>path; )
259 if (*--p=='\\' || *p=='/') {
260 p++;
261 break;
264 if (name) {
265 for(s=p; s<end; )
266 *name++ = *s++;
268 *name = '\0';
271 if (dir) {
272 for(s=path; s<p; )
273 *dir++ = *s++;
275 *dir = '\0';
279 /****************************************************************************
280 * WCMD_HandleTildaModifiers
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
298 * To work out the length of the modifier:
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')
308 * Hence search forwards until find an invalid modifier, and then
309 * backwards until find for variable or 0-9
311 void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable, WCHAR *forValue, BOOL justFors) {
313 #define NUMMODIFIERS 11
314 static const WCHAR validmodifiers[NUMMODIFIERS] = {
315 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
317 static const WCHAR space[] = {' ', '\0'};
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;
334 /* Search forwards until find invalid character modifier */
335 while (!finished) {
337 /* Work on the previous character */
338 if (lastModifier != NULL) {
340 for (i=0; i<NUMMODIFIERS; i++) {
341 if (validmodifiers[i] == *lastModifier) {
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 ':' */
349 break;
353 if (i==NUMMODIFIERS) {
354 finished = TRUE;
358 /* Save this one away */
359 if (!finished) {
360 lastModifier = pos;
361 pos++;
365 while (lastModifier > firstModifier) {
366 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
367 wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
369 if (!justFors && context && (*lastModifier >= '0' || *lastModifier <= '9')) {
370 /* Its a valid parameter identifier - OK */
371 break;
373 } else if (forVariable && *lastModifier == *(forVariable+1)) {
374 /* Its a valid parameter identifier - OK */
375 break;
377 } else {
378 lastModifier--;
381 if (lastModifier == firstModifier) return; /* Invalid syntax */
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);
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;
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);*/
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;
410 memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
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];
422 /* Extract the env var */
423 memcpy(env, start, (end-start) * sizeof(WCHAR));
424 env[(end-start)] = 0x00;
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;
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;
442 exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
443 &fileInfo);
445 /* 2. Handle 'a' : Output attributes */
446 if (exists &&
447 memchrW(firstModifier, 'a', modifierLen) != NULL) {
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);
470 /* 3. Handle 't' : Date+time */
471 if (exists &&
472 memchrW(firstModifier, 't', modifierLen) != NULL) {
474 SYSTEMTIME systime;
475 int datelen;
477 doneModifier = TRUE;
478 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
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);
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'};
499 doneModifier = TRUE;
500 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
501 wsprintf(thisoutput, fmt, fullsize);
502 strcatW(finaloutput, thisoutput);
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,
510 sizeof(outputparam)/sizeof(outputparam[0]));
513 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
514 /* Note this overrides d,p,n,x */
515 if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
516 doneModifier = TRUE;
517 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
518 strcatW(finaloutput, fullfilename);
519 } else {
521 WCHAR drive[10];
522 WCHAR dir[MAX_PATH];
523 WCHAR fname[MAX_PATH];
524 WCHAR ext[MAX_PATH];
525 BOOL doneFileModifier = FALSE;
527 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
529 /* Split into components */
530 WCMD_splitpath(fullfilename, drive, dir, fname, ext);
532 /* 5. Handle 'd' : Drive Letter */
533 if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
534 strcatW(finaloutput, drive);
535 doneModifier = TRUE;
536 doneFileModifier = TRUE;
539 /* 6. Handle 'p' : Path */
540 if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
541 strcatW(finaloutput, dir);
542 doneModifier = TRUE;
543 doneFileModifier = TRUE;
546 /* 7. Handle 'n' : Name */
547 if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
548 strcatW(finaloutput, fname);
549 doneModifier = TRUE;
550 doneFileModifier = TRUE;
553 /* 8. Handle 'x' : Ext */
554 if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
555 strcatW(finaloutput, ext);
556 doneModifier = TRUE;
557 doneFileModifier = TRUE;
560 /* If 's' but no other parameter, dump the whole thing */
561 if (!doneFileModifier &&
562 memchrW(firstModifier, 's', modifierLen) != NULL) {
563 doneModifier = TRUE;
564 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
565 strcatW(finaloutput, outputparam);
570 /* If No other modifier processed, just add in parameter */
571 if (!doneModifier) strcpyW(finaloutput, outputparam);
573 /* Finish by inserting the replacement into the string */
574 pos = WCMD_strdupW(lastModifier+1);
575 strcpyW(*start, finaloutput);
576 strcatW(*start, pos);
577 free(pos);
580 /*******************************************************************
581 * WCMD_call - processes a batch call statement
583 * If there is a leading ':', calls within this batch program
584 * otherwise launches another program.
586 void WCMD_call (WCHAR *command) {
588 /* Run other program if no leading ':' */
589 if (*command != ':') {
590 WCMD_run_program(command, 1);
591 } else {
593 WCHAR gotoLabel[MAX_PATH];
595 strcpyW(gotoLabel, param1);
597 if (context) {
599 LARGE_INTEGER li;
601 /* Save the current file position, call the same file,
602 restore position */
603 li.QuadPart = 0;
604 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
605 &li.u.HighPart, FILE_CURRENT);
607 WCMD_batch (param1, command, 1, gotoLabel, context->h);
609 SetFilePointer(context -> h, li.u.LowPart,
610 &li.u.HighPart, FILE_BEGIN);
611 } else {
612 WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT));