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
23 #include "wine/debug.h"
25 WINE_DEFAULT_DEBUG_CHANNEL(cmd
);
28 extern WCHAR quals
[MAX_PATH
], param1
[MAX_PATH
], param2
[MAX_PATH
];
29 extern BATCH_CONTEXT
*context
;
30 extern DWORD errorlevel
;
32 /****************************************************************************
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'};
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
);
65 if (strstrW (string
, extension_batch
[i
]) == NULL
) strcatW (string
, extension_batch
[i
]);
66 h
= CreateFileW (string
, GENERIC_READ
, FILE_SHARE_READ
,
67 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
, NULL
);
69 if (h
== INVALID_HANDLE_VALUE
) {
70 strcpyW (string
, file
);
72 if (strstrW (string
, extension_exe
) == NULL
) strcatW (string
, extension_exe
);
73 if (GetFileAttributesW (string
) != INVALID_FILE_ATTRIBUTES
) {
74 WCMD_run_program (command
, 0);
76 SetLastError (ERROR_FILE_NOT_FOUND
);
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
));
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 */
101 strcpyW(param1
, startLabel
);
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
)
114 WCMD_process_commands(toExecute
, FALSE
, NULL
, NULL
);
115 WCMD_free_commands(toExecute
);
121 * If invoked by a CALL, we return to the context of our caller. Otherwise return
122 * to the caller's caller.
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
) {
145 static WCHAR param
[MAX_PATH
];
148 if (where
!= NULL
) *where
= NULL
;
152 case ' ': /* Skip leading spaces */
153 case '\t': /* Treat tabs as spaces */
157 if (where
!= NULL
&& i
==n
) *where
= s
;
159 while ((*s
!= '\0') && (*s
!= '"')) {
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. */
177 /* Only return where if it is for the right parameter */
178 if (where
!= NULL
&& i
==n
) *where
= s
;
179 while ((*s
!= '\0') && (*s
!= ' ') && (*s
!= ',') && (*s
!= '=') && (*s
!= '\t')) {
182 if (i
== n
&& (p
!=param
)) {
186 /* Skip double delimiters, eg. dir a.a,,,,,b.b */
191 s
++; /* Skip delimiter */
198 /****************************************************************************
201 * Get one line from a batch file. We can't use the native f* functions because
202 * of the filename syntax differences between DOS and Unix. Also need to lose
203 * the LF (or CRLF) from the line.
206 WCHAR
*WCMD_fgets (WCHAR
*s
, int noChars
, HANDLE h
) {
214 status
= WCMD_ReadFile (h
, s
, 1, &bytes
, NULL
);
215 if ((status
== 0) || ((bytes
== 0) && (s
== p
))) return NULL
;
216 if (*s
== '\n') bytes
= 0;
217 else if (*s
!= '\r') {
222 } while ((bytes
== 1) && (noChars
> 1));
226 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
227 void WCMD_splitpath(const WCHAR
* path
, WCHAR
* drv
, WCHAR
* dir
, WCHAR
* name
, WCHAR
* ext
)
229 const WCHAR
* end
; /* end of processed string */
230 const WCHAR
* p
; /* search pointer */
231 const WCHAR
* s
; /* copy pointer */
233 /* extract drive name */
234 if (path
[0] && path
[1]==':') {
243 /* search for end of string or stream separator */
244 for(end
=path
; *end
&& *end
!=':'; )
247 /* search for begin of file extension */
248 for(p
=end
; p
>path
&& *--p
!='\\' && *p
!='/'; )
255 for(s
=end
; (*ext
=*s
++); )
258 /* search for end of directory name */
260 if (*--p
=='\\' || *p
=='/') {
280 /****************************************************************************
281 * WCMD_HandleTildaModifiers
283 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
284 * %~xxxxxV (V=0-9 or A-Z)
285 * Where xxxx is any combination of:
287 * f - Fully qualified path (assumes current dir if not drive\dir)
292 * s - path with shortnames
296 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
299 * To work out the length of the modifier:
301 * Note: In the case of %0-9 knowing the end of the modifier is easy,
302 * but in a for loop, the for end WCHARacter may also be a modifier
303 * eg. for %a in (c:\a.a) do echo XXX
304 * where XXX = %~a (just ~)
305 * %~aa (~ and attributes)
306 * %~aaxa (~, attributes and extension)
307 * BUT %~aax (~ and attributes followed by 'x')
309 * Hence search forwards until find an invalid modifier, and then
310 * backwards until find for variable or 0-9
312 void WCMD_HandleTildaModifiers(WCHAR
**start
, WCHAR
*forVariable
, WCHAR
*forValue
, BOOL justFors
) {
314 #define NUMMODIFIERS 11
315 static const WCHAR validmodifiers
[NUMMODIFIERS
] = {
316 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
318 static const WCHAR space
[] = {' ', '\0'};
320 WIN32_FILE_ATTRIBUTE_DATA fileInfo
;
321 WCHAR outputparam
[MAX_PATH
];
322 WCHAR finaloutput
[MAX_PATH
];
323 WCHAR fullfilename
[MAX_PATH
];
324 WCHAR thisoutput
[MAX_PATH
];
325 WCHAR
*pos
= *start
+1;
326 WCHAR
*firstModifier
= pos
;
327 WCHAR
*lastModifier
= NULL
;
329 BOOL finished
= FALSE
;
332 BOOL skipFileParsing
= FALSE
;
333 BOOL doneModifier
= FALSE
;
335 /* Search forwards until find invalid character modifier */
338 /* Work on the previous character */
339 if (lastModifier
!= NULL
) {
341 for (i
=0; i
<NUMMODIFIERS
; i
++) {
342 if (validmodifiers
[i
] == *lastModifier
) {
344 /* Special case '$' to skip until : found */
345 if (*lastModifier
== '$') {
346 while (*pos
!= ':' && *pos
) pos
++;
347 if (*pos
== 0x00) return; /* Invalid syntax */
348 pos
++; /* Skip ':' */
354 if (i
==NUMMODIFIERS
) {
359 /* Save this one away */
366 while (lastModifier
> firstModifier
) {
367 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
368 wine_dbgstr_w(lastModifier
), wine_dbgstr_w(forVariable
));
370 if (!justFors
&& context
&& (*lastModifier
>= '0' && *lastModifier
<= '9')) {
371 /* Its a valid parameter identifier - OK */
374 } else if (forVariable
&& *lastModifier
== *(forVariable
+1)) {
375 /* Its a valid parameter identifier - OK */
382 if (lastModifier
== firstModifier
) return; /* Invalid syntax */
384 /* Extract the parameter to play with */
385 if ((*lastModifier
>= '0' && *lastModifier
<= '9')) {
386 strcpyW(outputparam
, WCMD_parameter (context
-> command
,
387 *lastModifier
-'0' + context
-> shift_count
[*lastModifier
-'0'], NULL
));
389 strcpyW(outputparam
, forValue
);
392 /* So now, firstModifier points to beginning of modifiers, lastModifier
393 points to the variable just after the modifiers. Process modifiers
394 in a specific order, remembering there could be duplicates */
395 modifierLen
= lastModifier
- firstModifier
;
396 finaloutput
[0] = 0x00;
398 /* Useful for debugging purposes: */
399 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
400 (modifierLen), (modifierLen), firstModifier, *lastModifier,
403 /* 1. Handle '~' : Strip surrounding quotes */
404 if (outputparam
[0]=='"' &&
405 memchrW(firstModifier
, '~', modifierLen
) != NULL
) {
406 int len
= strlenW(outputparam
);
407 if (outputparam
[len
-1] == '"') {
408 outputparam
[len
-1]=0x00;
411 memmove(outputparam
, &outputparam
[1], (len
* sizeof(WCHAR
))-1);
414 /* 2. Handle the special case of a $ */
415 if (memchrW(firstModifier
, '$', modifierLen
) != NULL
) {
416 /* Special Case: Search envar specified in $[envvar] for outputparam
417 Note both $ and : are guaranteed otherwise check above would fail */
418 WCHAR
*start
= strchrW(firstModifier
, '$') + 1;
419 WCHAR
*end
= strchrW(firstModifier
, ':');
421 WCHAR fullpath
[MAX_PATH
];
423 /* Extract the env var */
424 memcpy(env
, start
, (end
-start
) * sizeof(WCHAR
));
425 env
[(end
-start
)] = 0x00;
427 /* If env var not found, return empty string */
428 if ((GetEnvironmentVariableW(env
, fullpath
, MAX_PATH
) == 0) ||
429 (SearchPathW(fullpath
, outputparam
, NULL
, 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 (GetFullPathNameW(outputparam
, MAX_PATH
, fullfilename
, NULL
) == 0)
442 exists
= GetFileAttributesExW(fullfilename
, GetFileExInfoStandard
,
445 /* 2. Handle 'a' : Output attributes */
447 memchrW(firstModifier
, 'a', modifierLen
) != NULL
) {
449 WCHAR defaults
[] = {'-','-','-','-','-','-','-','-','-','\0'};
451 strcpyW(thisoutput
, defaults
);
452 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
454 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
)
456 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_ARCHIVE
)
458 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
)
460 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_SYSTEM
)
462 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_COMPRESSED
)
464 /* FIXME: What are 6 and 7? */
465 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
)
467 strcatW(finaloutput
, thisoutput
);
470 /* 3. Handle 't' : Date+time */
472 memchrW(firstModifier
, 't', modifierLen
) != NULL
) {
478 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
480 /* Format the time */
481 FileTimeToSystemTime(&fileInfo
.ftLastWriteTime
, &systime
);
482 GetDateFormatW(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, &systime
,
483 NULL
, thisoutput
, MAX_PATH
);
484 strcatW(thisoutput
, space
);
485 datelen
= strlenW(thisoutput
);
486 GetTimeFormatW(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, &systime
,
487 NULL
, (thisoutput
+datelen
), MAX_PATH
-datelen
);
488 strcatW(finaloutput
, thisoutput
);
491 /* 4. Handle 'z' : File length */
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'};
500 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
501 wsprintfW(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 GetShortPathNameW(outputparam
, outputparam
, sizeof(outputparam
)/sizeof(outputparam
[0]));
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
) {
516 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
517 strcatW(finaloutput
, fullfilename
);
522 WCHAR fname
[MAX_PATH
];
524 BOOL doneFileModifier
= FALSE
;
526 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
528 /* Split into components */
529 WCMD_splitpath(fullfilename
, drive
, dir
, fname
, ext
);
531 /* 5. Handle 'd' : Drive Letter */
532 if (memchrW(firstModifier
, 'd', modifierLen
) != NULL
) {
533 strcatW(finaloutput
, drive
);
535 doneFileModifier
= TRUE
;
538 /* 6. Handle 'p' : Path */
539 if (memchrW(firstModifier
, 'p', modifierLen
) != NULL
) {
540 strcatW(finaloutput
, dir
);
542 doneFileModifier
= TRUE
;
545 /* 7. Handle 'n' : Name */
546 if (memchrW(firstModifier
, 'n', modifierLen
) != NULL
) {
547 strcatW(finaloutput
, fname
);
549 doneFileModifier
= TRUE
;
552 /* 8. Handle 'x' : Ext */
553 if (memchrW(firstModifier
, 'x', modifierLen
) != NULL
) {
554 strcatW(finaloutput
, ext
);
556 doneFileModifier
= TRUE
;
559 /* If 's' but no other parameter, dump the whole thing */
560 if (!doneFileModifier
&&
561 memchrW(firstModifier
, 's', modifierLen
) != NULL
) {
563 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
564 strcatW(finaloutput
, outputparam
);
569 /* If No other modifier processed, just add in parameter */
570 if (!doneModifier
) strcpyW(finaloutput
, outputparam
);
572 /* Finish by inserting the replacement into the string */
573 WCMD_strsubstW(*start
, lastModifier
+1, finaloutput
, -1);
576 /*******************************************************************
577 * WCMD_call - processes a batch call statement
579 * If there is a leading ':', calls within this batch program
580 * otherwise launches another program.
582 void WCMD_call (WCHAR
*command
) {
584 /* Run other program if no leading ':' */
585 if (*command
!= ':') {
586 WCMD_run_program(command
, 1);
589 WCHAR gotoLabel
[MAX_PATH
];
591 strcpyW(gotoLabel
, param1
);
597 /* Save the current file position, call the same file,
600 li
.u
.LowPart
= SetFilePointer(context
-> h
, li
.u
.LowPart
,
601 &li
.u
.HighPart
, FILE_CURRENT
);
603 WCMD_batch (param1
, command
, 1, gotoLabel
, context
->h
);
605 SetFilePointer(context
-> h
, li
.u
.LowPart
,
606 &li
.u
.HighPart
, FILE_BEGIN
);
608 WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT
));