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 HANDLE h
= INVALID_HANDLE_VALUE
;
51 BATCH_CONTEXT
*prev_context
;
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
);
62 DuplicateHandle(GetCurrentProcess(), pgmHandle
,
63 GetCurrentProcess(), &h
,
64 0, FALSE
, DUPLICATE_SAME_ACCESS
);
68 * Create a context structure for this batch file.
71 prev_context
= context
;
72 context
= LocalAlloc (LMEM_FIXED
, sizeof (BATCH_CONTEXT
));
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
;
80 /* If processing a call :label, 'goto' the label in question */
82 strcpyW(param1
, startLabel
);
87 * Work through the file line by line. Specific batch commands are processed here,
88 * the rest are handled by the main command processor.
91 while (context
-> skip_rest
== FALSE
) {
92 CMD_LIST
*toExecute
= NULL
; /* Commands left to be executed */
93 if (WCMD_ReadAndParseLine(NULL
, &toExecute
, h
) == NULL
)
95 WCMD_process_commands(toExecute
, FALSE
, NULL
, NULL
);
96 WCMD_free_commands(toExecute
);
102 * If invoked by a CALL, we return to the context of our caller. Otherwise return
103 * to the caller's caller.
106 HeapFree(GetProcessHeap(), 0, context
->batchfileW
);
108 if ((prev_context
!= NULL
) && (!called
)) {
109 prev_context
-> skip_rest
= TRUE
;
110 context
= prev_context
;
112 context
= prev_context
;
115 /*******************************************************************
116 * WCMD_parameter - extract a parameter from a command line.
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.
124 WCHAR
*WCMD_parameter (WCHAR
*s
, int n
, WCHAR
**where
) {
127 static WCHAR param
[MAX_PATH
];
130 if (where
!= NULL
) *where
= NULL
;
134 case ' ': /* Skip leading spaces */
135 case '\t': /* Treat tabs as spaces */
139 if (where
!= NULL
&& i
==n
) *where
= s
;
141 while ((*s
!= '\0') && (*s
!= '"')) {
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. */
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')) {
164 if (i
== n
&& (p
!=param
)) {
168 /* Skip double delimiters, eg. dir a.a,,,,,b.b */
173 s
++; /* Skip delimiter */
180 /****************************************************************************
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.
188 WCHAR
*WCMD_fgets (WCHAR
*s
, int noChars
, HANDLE h
) {
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') {
204 } while ((bytes
== 1) && (noChars
> 1));
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
)
211 const WCHAR
* end
; /* end of processed string */
212 const WCHAR
* p
; /* search pointer */
213 const WCHAR
* s
; /* copy pointer */
215 /* extract drive name */
216 if (path
[0] && path
[1]==':') {
225 /* search for end of string or stream separator */
226 for(end
=path
; *end
&& *end
!=':'; )
229 /* search for begin of file extension */
230 for(p
=end
; p
>path
&& *--p
!='\\' && *p
!='/'; )
237 for(s
=end
; (*ext
=*s
++); )
240 /* search for end of directory name */
242 if (*--p
=='\\' || *p
=='/') {
262 /****************************************************************************
263 * WCMD_HandleTildaModifiers
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:
269 * f - Fully qualified path (assumes current dir if not drive\dir)
274 * s - path with shortnames
278 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
281 * To work out the length of the modifier:
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')
291 * Hence search forwards until find an invalid modifier, and then
292 * backwards until find for variable or 0-9
294 void WCMD_HandleTildaModifiers(WCHAR
**start
, WCHAR
*forVariable
, WCHAR
*forValue
, BOOL justFors
) {
296 #define NUMMODIFIERS 11
297 static const WCHAR validmodifiers
[NUMMODIFIERS
] = {
298 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
300 static const WCHAR space
[] = {' ', '\0'};
302 WIN32_FILE_ATTRIBUTE_DATA fileInfo
;
303 WCHAR outputparam
[MAX_PATH
];
304 WCHAR finaloutput
[MAX_PATH
];
305 WCHAR fullfilename
[MAX_PATH
];
306 WCHAR thisoutput
[MAX_PATH
];
307 WCHAR
*pos
= *start
+1;
308 WCHAR
*firstModifier
= pos
;
309 WCHAR
*lastModifier
= NULL
;
311 BOOL finished
= FALSE
;
314 BOOL skipFileParsing
= FALSE
;
315 BOOL doneModifier
= FALSE
;
317 /* Search forwards until find invalid character modifier */
320 /* Work on the previous character */
321 if (lastModifier
!= NULL
) {
323 for (i
=0; i
<NUMMODIFIERS
; i
++) {
324 if (validmodifiers
[i
] == *lastModifier
) {
326 /* Special case '$' to skip until : found */
327 if (*lastModifier
== '$') {
328 while (*pos
!= ':' && *pos
) pos
++;
329 if (*pos
== 0x00) return; /* Invalid syntax */
330 pos
++; /* Skip ':' */
336 if (i
==NUMMODIFIERS
) {
341 /* Save this one away */
348 while (lastModifier
> firstModifier
) {
349 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
350 wine_dbgstr_w(lastModifier
), wine_dbgstr_w(forVariable
));
352 if (!justFors
&& context
&& (*lastModifier
>= '0' && *lastModifier
<= '9')) {
353 /* Its a valid parameter identifier - OK */
356 } else if (forVariable
&& *lastModifier
== *(forVariable
+1)) {
357 /* Its a valid parameter identifier - OK */
364 if (lastModifier
== firstModifier
) return; /* Invalid syntax */
366 /* Extract the parameter to play with */
367 if (*lastModifier
== '0') {
368 strcpyW(outputparam
, context
->batchfileW
);
369 } else if ((*lastModifier
>= '1' && *lastModifier
<= '9')) {
370 strcpyW(outputparam
, WCMD_parameter (context
-> command
,
371 *lastModifier
-'0' + context
-> shift_count
[*lastModifier
-'0'], NULL
));
373 strcpyW(outputparam
, forValue
);
376 /* So now, firstModifier points to beginning of modifiers, lastModifier
377 points to the variable just after the modifiers. Process modifiers
378 in a specific order, remembering there could be duplicates */
379 modifierLen
= lastModifier
- firstModifier
;
380 finaloutput
[0] = 0x00;
382 /* Useful for debugging purposes: */
383 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
384 (modifierLen), (modifierLen), firstModifier, *lastModifier,
387 /* 1. Handle '~' : Strip surrounding quotes */
388 if (outputparam
[0]=='"' &&
389 memchrW(firstModifier
, '~', modifierLen
) != NULL
) {
390 int len
= strlenW(outputparam
);
391 if (outputparam
[len
-1] == '"') {
392 outputparam
[len
-1]=0x00;
395 memmove(outputparam
, &outputparam
[1], (len
* sizeof(WCHAR
))-1);
398 /* 2. Handle the special case of a $ */
399 if (memchrW(firstModifier
, '$', modifierLen
) != NULL
) {
400 /* Special Case: Search envar specified in $[envvar] for outputparam
401 Note both $ and : are guaranteed otherwise check above would fail */
402 WCHAR
*begin
= strchrW(firstModifier
, '$') + 1;
403 WCHAR
*end
= strchrW(firstModifier
, ':');
405 WCHAR fullpath
[MAX_PATH
];
407 /* Extract the env var */
408 memcpy(env
, begin
, (end
-begin
) * sizeof(WCHAR
));
409 env
[(end
-begin
)] = 0x00;
411 /* If env var not found, return empty string */
412 if ((GetEnvironmentVariableW(env
, fullpath
, MAX_PATH
) == 0) ||
413 (SearchPathW(fullpath
, outputparam
, NULL
, MAX_PATH
, outputparam
, NULL
) == 0)) {
414 finaloutput
[0] = 0x00;
415 outputparam
[0] = 0x00;
416 skipFileParsing
= TRUE
;
420 /* After this, we need full information on the file,
421 which is valid not to exist. */
422 if (!skipFileParsing
) {
423 if (GetFullPathNameW(outputparam
, MAX_PATH
, fullfilename
, NULL
) == 0)
426 exists
= GetFileAttributesExW(fullfilename
, GetFileExInfoStandard
,
429 /* 2. Handle 'a' : Output attributes */
431 memchrW(firstModifier
, 'a', modifierLen
) != NULL
) {
433 WCHAR defaults
[] = {'-','-','-','-','-','-','-','-','-','\0'};
435 strcpyW(thisoutput
, defaults
);
436 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
438 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
)
440 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_ARCHIVE
)
442 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
)
444 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_SYSTEM
)
446 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_COMPRESSED
)
448 /* FIXME: What are 6 and 7? */
449 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
)
451 strcatW(finaloutput
, thisoutput
);
454 /* 3. Handle 't' : Date+time */
456 memchrW(firstModifier
, 't', modifierLen
) != NULL
) {
462 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
464 /* Format the time */
465 FileTimeToSystemTime(&fileInfo
.ftLastWriteTime
, &systime
);
466 GetDateFormatW(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, &systime
,
467 NULL
, thisoutput
, MAX_PATH
);
468 strcatW(thisoutput
, space
);
469 datelen
= strlenW(thisoutput
);
470 GetTimeFormatW(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, &systime
,
471 NULL
, (thisoutput
+datelen
), MAX_PATH
-datelen
);
472 strcatW(finaloutput
, thisoutput
);
475 /* 4. Handle 'z' : File length */
477 memchrW(firstModifier
, 'z', modifierLen
) != NULL
) {
478 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
479 ULONG
/*64*/ fullsize
= /*(fileInfo.nFileSizeHigh << 32) +*/
480 fileInfo
.nFileSizeLow
;
481 static const WCHAR fmt
[] = {'%','u','\0'};
484 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
485 wsprintfW(thisoutput
, fmt
, fullsize
);
486 strcatW(finaloutput
, thisoutput
);
489 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
490 if (memchrW(firstModifier
, 's', modifierLen
) != NULL
) {
491 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
492 /* Don't flag as doneModifier - %~s on its own is processed later */
493 GetShortPathNameW(outputparam
, outputparam
, sizeof(outputparam
)/sizeof(outputparam
[0]));
496 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
497 /* Note this overrides d,p,n,x */
498 if (memchrW(firstModifier
, 'f', modifierLen
) != NULL
) {
500 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
501 strcatW(finaloutput
, fullfilename
);
506 WCHAR fname
[MAX_PATH
];
508 BOOL doneFileModifier
= FALSE
;
510 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
512 /* Split into components */
513 WCMD_splitpath(fullfilename
, drive
, dir
, fname
, ext
);
515 /* 5. Handle 'd' : Drive Letter */
516 if (memchrW(firstModifier
, 'd', modifierLen
) != NULL
) {
517 strcatW(finaloutput
, drive
);
519 doneFileModifier
= TRUE
;
522 /* 6. Handle 'p' : Path */
523 if (memchrW(firstModifier
, 'p', modifierLen
) != NULL
) {
524 strcatW(finaloutput
, dir
);
526 doneFileModifier
= TRUE
;
529 /* 7. Handle 'n' : Name */
530 if (memchrW(firstModifier
, 'n', modifierLen
) != NULL
) {
531 strcatW(finaloutput
, fname
);
533 doneFileModifier
= TRUE
;
536 /* 8. Handle 'x' : Ext */
537 if (memchrW(firstModifier
, 'x', modifierLen
) != NULL
) {
538 strcatW(finaloutput
, ext
);
540 doneFileModifier
= TRUE
;
543 /* If 's' but no other parameter, dump the whole thing */
544 if (!doneFileModifier
&&
545 memchrW(firstModifier
, 's', modifierLen
) != NULL
) {
547 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, space
);
548 strcatW(finaloutput
, outputparam
);
553 /* If No other modifier processed, just add in parameter */
554 if (!doneModifier
) strcpyW(finaloutput
, outputparam
);
556 /* Finish by inserting the replacement into the string */
557 WCMD_strsubstW(*start
, lastModifier
+1, finaloutput
, -1);
560 /*******************************************************************
561 * WCMD_call - processes a batch call statement
563 * If there is a leading ':', calls within this batch program
564 * otherwise launches another program.
566 void WCMD_call (WCHAR
*command
) {
568 /* Run other program if no leading ':' */
569 if (*command
!= ':') {
570 WCMD_run_program(command
, 1);
573 WCHAR gotoLabel
[MAX_PATH
];
575 strcpyW(gotoLabel
, param1
);
581 /* Save the current file position, call the same file,
584 li
.u
.LowPart
= SetFilePointer(context
-> h
, li
.u
.LowPart
,
585 &li
.u
.HighPart
, FILE_CURRENT
);
587 WCMD_batch (param1
, command
, 1, gotoLabel
, context
->h
);
589 SetFilePointer(context
-> h
, li
.u
.LowPart
,
590 &li
.u
.HighPart
, FILE_BEGIN
);
592 WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT
));