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 extern struct env_stack
*saved_environment
;
27 WINE_DEFAULT_DEBUG_CHANNEL(cmd
);
29 /****************************************************************************
32 * Open and execute a batch file.
33 * On entry *command includes the complete command line beginning with the name
34 * of the batch file (if a CALL command was entered the CALL has been removed).
35 * *file is the name of the file, which might not exist and may not have the
36 * .BAT suffix on. Called is 1 for a CALL, 0 otherwise.
38 * We need to handle recursion correctly, since one batch program might call another.
39 * So parameters for this batch file are held in a BATCH_CONTEXT structure.
41 * To support call within the same batch program, another input parameter is
42 * a label to goto once opened.
45 void WCMD_batch (WCHAR
*file
, WCHAR
*command
, BOOL called
, WCHAR
*startLabel
, HANDLE pgmHandle
)
47 HANDLE h
= INVALID_HANDLE_VALUE
;
48 BATCH_CONTEXT
*prev_context
;
50 if (startLabel
== NULL
) {
51 h
= CreateFileW (file
, GENERIC_READ
, FILE_SHARE_READ
|FILE_SHARE_WRITE
|FILE_SHARE_DELETE
,
52 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
, NULL
);
53 if (h
== INVALID_HANDLE_VALUE
) {
54 SetLastError (ERROR_FILE_NOT_FOUND
);
59 DuplicateHandle(GetCurrentProcess(), pgmHandle
,
60 GetCurrentProcess(), &h
,
61 0, FALSE
, DUPLICATE_SAME_ACCESS
);
65 * Create a context structure for this batch file.
68 prev_context
= context
;
69 context
= LocalAlloc (LMEM_FIXED
, sizeof (BATCH_CONTEXT
));
71 context
->batchfileW
= WCMD_strdupW(file
);
72 context
-> command
= command
;
73 memset(context
-> shift_count
, 0x00, sizeof(context
-> shift_count
));
74 context
-> prev_context
= prev_context
;
75 context
-> skip_rest
= FALSE
;
77 /* If processing a call :label, 'goto' the label in question */
79 strcpyW(param1
, startLabel
);
84 * Work through the file line by line. Specific batch commands are processed here,
85 * the rest are handled by the main command processor.
88 while (context
-> skip_rest
== FALSE
) {
89 CMD_LIST
*toExecute
= NULL
; /* Commands left to be executed */
90 if (!WCMD_ReadAndParseLine(NULL
, &toExecute
, h
))
92 /* Note: although this batch program itself may be called, we are not retrying
93 the command as a result of a call failing to find a program, hence the
94 retryCall parameter below is FALSE */
95 WCMD_process_commands(toExecute
, FALSE
, NULL
, NULL
, FALSE
);
96 WCMD_free_commands(toExecute
);
102 * If there are outstanding setlocal's to the current context, unwind them.
104 while (saved_environment
&& saved_environment
->batchhandle
== context
->h
) {
109 * If invoked by a CALL, we return to the context of our caller. Otherwise return
110 * to the caller's caller.
113 HeapFree(GetProcessHeap(), 0, context
->batchfileW
);
115 if ((prev_context
!= NULL
) && (!called
)) {
116 WINE_TRACE("Batch completed, but was not 'called' so skipping outer batch too\n");
117 prev_context
-> skip_rest
= TRUE
;
118 context
= prev_context
;
120 context
= prev_context
;
123 /*******************************************************************
124 * WCMD_parameter_with_delims
126 * Extracts a delimited parameter from an input string, providing
127 * the delimiters characters to use
130 * s [I] input string, non NULL
131 * n [I] # of the parameter to return, counted from 0
132 * start [O] Optional. Pointer to the first char of param n in s
133 * raw [I] TRUE to return the parameter in raw format (quotes maintained)
134 * FALSE to return the parameter with quotes stripped (including internal ones)
135 * wholecmdline [I] TRUE to indicate this routine is being used to parse the
136 * command line, and special logic for arg0->1 transition
137 * needs to be applied.
138 * delims[I] The delimiter characters to use
141 * Success: The nth delimited parameter found in s
142 * if start != NULL, *start points to the start of the param (quotes maintained)
143 * Failure: An empty string if the param is not found.
147 * Return value is stored in static storage (i.e. overwritten after each call).
148 * By default, the parameter is returned with quotes removed, ready for use with
149 * other API calls, e.g. c:\"a b"\c is returned as c:\a b\c. However, some commands
150 * need to preserve the exact syntax (echo, for, etc) hence the raw option.
152 WCHAR
*WCMD_parameter_with_delims (WCHAR
*s
, int n
, WCHAR
**start
,
153 BOOL raw
, BOOL wholecmdline
, const WCHAR
*delims
)
156 static WCHAR param
[MAX_PATH
];
157 WCHAR
*p
= s
, *begin
;
159 if (start
!= NULL
) *start
= NULL
;
164 /* Absorb repeated word delimiters until we get to the next token (or the end!) */
165 while (*p
&& (strchrW(delims
, *p
) != NULL
))
167 if (*p
== '\0') return param
;
169 /* If we have reached the token number we want, remember the beginning of it */
170 if (start
!= NULL
&& curParamNb
== n
) *start
= p
;
172 /* Return the whole word up to the next delimiter, handling quotes in the middle
173 of it, e.g. a"\b c\"d is a single parameter. */
176 /* Loop character by character, but just need to special case quotes */
178 /* Once we have found a delimiter, break */
179 if (strchrW(delims
, *p
) != NULL
) break;
181 /* Very odd special case - Seems as if a ( acts as a delimiter which is
182 not swallowed but is effective only when it comes between the program
183 name and the parameters. Need to avoid this triggering when used
184 to walk parameters generally. */
185 if (wholecmdline
&& curParamNb
== 0 && *p
=='(') break;
187 /* If we find a quote, copy until we get the end quote */
190 while (*p
&& *p
!= '"') p
++;
193 /* Now skip the character / quote */
197 if (curParamNb
== n
) {
198 /* Return the parameter in static storage either as-is (raw) or
199 suitable for use with other win32 api calls (quotes stripped) */
201 memcpy(param
, begin
, (p
- begin
) * sizeof(WCHAR
));
202 param
[p
-begin
] = '\0';
206 if (*begin
!= '"') param
[i
++] = *begin
;
217 /*******************************************************************
220 * Extracts a delimited parameter from an input string, using a
221 * default set of delimiter characters. For parameters, see the main
224 WCHAR
*WCMD_parameter (WCHAR
*s
, int n
, WCHAR
**start
, BOOL raw
,
227 static const WCHAR defaultDelims
[] = { ' ', '\t', ',', '=', ';', '\0' };
228 return WCMD_parameter_with_delims (s
, n
, start
, raw
, wholecmdline
, defaultDelims
);
231 /****************************************************************************
234 * Gets one line from a file/console and puts it into buffer buf
235 * Pre: buf has size noChars
236 * 1 <= noChars <= MAXSTRING
237 * Post: buf is filled with at most noChars-1 characters, and gets nul-terminated
238 buf does not include EOL terminator
241 * NULL on error or EOF
244 WCHAR
*WCMD_fgets(WCHAR
*buf
, DWORD noChars
, HANDLE h
)
250 /* We can't use the native f* functions because of the filename syntax differences
251 between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
253 if (!WCMD_is_console_handle(h
)) {
254 LARGE_INTEGER filepos
;
260 bufA
= HeapAlloc(GetProcessHeap(), 0, noChars
);
261 if (!bufA
) return NULL
;
263 /* Save current file position */
264 filepos
.QuadPart
= 0;
265 SetFilePointerEx(h
, filepos
, &filepos
, FILE_CURRENT
);
267 status
= ReadFile(h
, bufA
, noChars
, &charsRead
, NULL
);
268 if (!status
|| charsRead
== 0) {
269 HeapFree(GetProcessHeap(), 0, bufA
);
274 for (p
= bufA
; p
< (bufA
+ charsRead
); p
= CharNextExA(cp
, p
, 0)) {
275 if (*p
== '\n' || *p
== '\r')
279 /* Sets file pointer to the start of the next line, if any */
280 filepos
.QuadPart
+= p
- bufA
+ 1 + (*p
== '\r' ? 1 : 0);
281 SetFilePointerEx(h
, filepos
, NULL
, FILE_BEGIN
);
283 i
= MultiByteToWideChar(cp
, 0, bufA
, p
- bufA
, buf
, noChars
);
284 HeapFree(GetProcessHeap(), 0, bufA
);
287 status
= WCMD_ReadFile(h
, buf
, noChars
, &charsRead
);
288 if (!status
|| charsRead
== 0) return NULL
;
291 for (i
= 0; i
< charsRead
; i
++) {
292 if (buf
[i
] == '\n' || buf
[i
] == '\r')
297 /* Truncate at EOL (or end of buffer) */
306 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
307 void WCMD_splitpath(const WCHAR
* path
, WCHAR
* drv
, WCHAR
* dir
, WCHAR
* name
, WCHAR
* ext
)
309 const WCHAR
* end
; /* end of processed string */
310 const WCHAR
* p
; /* search pointer */
311 const WCHAR
* s
; /* copy pointer */
313 /* extract drive name */
314 if (path
[0] && path
[1]==':') {
323 end
= path
+ strlenW(path
);
325 /* search for begin of file extension */
326 for(p
=end
; p
>path
&& *--p
!='\\' && *p
!='/'; )
333 for(s
=end
; (*ext
=*s
++); )
336 /* search for end of directory name */
338 if (*--p
=='\\' || *p
=='/') {
358 /****************************************************************************
359 * WCMD_HandleTildaModifiers
361 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
362 * %~xxxxxV (V=0-9 or A-Z)
363 * Where xxxx is any combination of:
365 * f - Fully qualified path (assumes current dir if not drive\dir)
370 * s - path with shortnames
374 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
377 * To work out the length of the modifier:
379 * Note: In the case of %0-9 knowing the end of the modifier is easy,
380 * but in a for loop, the for end WCHARacter may also be a modifier
381 * eg. for %a in (c:\a.a) do echo XXX
382 * where XXX = %~a (just ~)
383 * %~aa (~ and attributes)
384 * %~aaxa (~, attributes and extension)
385 * BUT %~aax (~ and attributes followed by 'x')
387 * Hence search forwards until find an invalid modifier, and then
388 * backwards until find for variable or 0-9
390 void WCMD_HandleTildaModifiers(WCHAR
**start
, const WCHAR
*forVariable
,
391 const WCHAR
*forValue
, BOOL justFors
) {
393 #define NUMMODIFIERS 11
394 static const WCHAR validmodifiers
[NUMMODIFIERS
] = {
395 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
398 WIN32_FILE_ATTRIBUTE_DATA fileInfo
;
399 WCHAR outputparam
[MAX_PATH
];
400 WCHAR finaloutput
[MAX_PATH
];
401 WCHAR fullfilename
[MAX_PATH
];
402 WCHAR thisoutput
[MAX_PATH
];
403 WCHAR
*pos
= *start
+1;
404 WCHAR
*firstModifier
= pos
;
405 WCHAR
*lastModifier
= NULL
;
407 BOOL finished
= FALSE
;
410 BOOL skipFileParsing
= FALSE
;
411 BOOL doneModifier
= FALSE
;
413 /* Search forwards until find invalid character modifier */
416 /* Work on the previous character */
417 if (lastModifier
!= NULL
) {
419 for (i
=0; i
<NUMMODIFIERS
; i
++) {
420 if (validmodifiers
[i
] == *lastModifier
) {
422 /* Special case '$' to skip until : found */
423 if (*lastModifier
== '$') {
424 while (*pos
!= ':' && *pos
) pos
++;
425 if (*pos
== 0x00) return; /* Invalid syntax */
426 pos
++; /* Skip ':' */
432 if (i
==NUMMODIFIERS
) {
437 /* Save this one away */
444 while (lastModifier
> firstModifier
) {
445 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
446 wine_dbgstr_w(lastModifier
), wine_dbgstr_w(forVariable
));
448 if (!justFors
&& context
&& (*lastModifier
>= '0' && *lastModifier
<= '9')) {
449 /* Its a valid parameter identifier - OK */
452 } else if (forVariable
&& *lastModifier
== *(forVariable
+1)) {
453 /* Its a valid parameter identifier - OK */
460 if (lastModifier
== firstModifier
) return; /* Invalid syntax */
462 /* Extract the parameter to play with */
463 if (*lastModifier
== '0') {
464 strcpyW(outputparam
, context
->batchfileW
);
465 } else if ((*lastModifier
>= '1' && *lastModifier
<= '9')) {
467 WCMD_parameter (context
-> command
,
468 *lastModifier
-'0' + context
-> shift_count
[*lastModifier
-'0'],
471 strcpyW(outputparam
, forValue
);
474 /* So now, firstModifier points to beginning of modifiers, lastModifier
475 points to the variable just after the modifiers. Process modifiers
476 in a specific order, remembering there could be duplicates */
477 modifierLen
= lastModifier
- firstModifier
;
478 finaloutput
[0] = 0x00;
480 /* 1. Handle '~' : Strip surrounding quotes */
481 if (outputparam
[0]=='"' &&
482 memchrW(firstModifier
, '~', modifierLen
) != NULL
) {
483 int len
= strlenW(outputparam
);
484 if (outputparam
[len
-1] == '"') {
485 outputparam
[len
-1]=0x00;
488 memmove(outputparam
, &outputparam
[1], (len
* sizeof(WCHAR
))-1);
491 /* 2. Handle the special case of a $ */
492 if (memchrW(firstModifier
, '$', modifierLen
) != NULL
) {
493 /* Special Case: Search envar specified in $[envvar] for outputparam
494 Note both $ and : are guaranteed otherwise check above would fail */
495 WCHAR
*begin
= strchrW(firstModifier
, '$') + 1;
496 WCHAR
*end
= strchrW(firstModifier
, ':');
498 WCHAR fullpath
[MAX_PATH
];
500 /* Extract the env var */
501 memcpy(env
, begin
, (end
-begin
) * sizeof(WCHAR
));
502 env
[(end
-begin
)] = 0x00;
504 /* If env var not found, return empty string */
505 if ((GetEnvironmentVariableW(env
, fullpath
, MAX_PATH
) == 0) ||
506 (SearchPathW(fullpath
, outputparam
, NULL
, MAX_PATH
, outputparam
, NULL
) == 0)) {
507 finaloutput
[0] = 0x00;
508 outputparam
[0] = 0x00;
509 skipFileParsing
= TRUE
;
513 /* After this, we need full information on the file,
514 which is valid not to exist. */
515 if (!skipFileParsing
) {
516 if (GetFullPathNameW(outputparam
, MAX_PATH
, fullfilename
, NULL
) == 0)
519 exists
= GetFileAttributesExW(fullfilename
, GetFileExInfoStandard
,
522 /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
523 if (memchrW(firstModifier
, 'a', modifierLen
) != NULL
) {
525 WCHAR defaults
[] = {'-','-','-','-','-','-','-','-','-','\0'};
529 strcpyW(thisoutput
, defaults
);
530 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
532 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
)
534 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_ARCHIVE
)
536 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
)
538 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_SYSTEM
)
540 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_COMPRESSED
)
542 /* FIXME: What are 6 and 7? */
543 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
)
545 strcatW(finaloutput
, thisoutput
);
549 /* 3. Handle 't' : Date+time (File doesn't have to exist) */
550 if (memchrW(firstModifier
, 't', modifierLen
) != NULL
) {
558 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, spaceW
);
560 /* Format the time */
561 FileTimeToSystemTime(&fileInfo
.ftLastWriteTime
, &systime
);
562 GetDateFormatW(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, &systime
,
563 NULL
, thisoutput
, MAX_PATH
);
564 strcatW(thisoutput
, spaceW
);
565 datelen
= strlenW(thisoutput
);
566 GetTimeFormatW(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, &systime
,
567 NULL
, (thisoutput
+datelen
), MAX_PATH
-datelen
);
568 strcatW(finaloutput
, thisoutput
);
572 /* 4. Handle 'z' : File length (File doesn't have to exist) */
573 if (memchrW(firstModifier
, 'z', modifierLen
) != NULL
) {
574 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
575 ULONG
/*64*/ fullsize
= /*(fileInfo.nFileSizeHigh << 32) +*/
576 fileInfo
.nFileSizeLow
;
577 static const WCHAR fmt
[] = {'%','u','\0'};
581 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, spaceW
);
582 wsprintfW(thisoutput
, fmt
, fullsize
);
583 strcatW(finaloutput
, thisoutput
);
587 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
588 if (memchrW(firstModifier
, 's', modifierLen
) != NULL
) {
589 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, spaceW
);
590 /* Don't flag as doneModifier - %~s on its own is processed later */
591 GetShortPathNameW(outputparam
, outputparam
, sizeof(outputparam
)/sizeof(outputparam
[0]));
594 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
595 /* Note this overrides d,p,n,x */
596 if (memchrW(firstModifier
, 'f', modifierLen
) != NULL
) {
598 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, spaceW
);
599 strcatW(finaloutput
, fullfilename
);
604 WCHAR fname
[MAX_PATH
];
606 BOOL doneFileModifier
= FALSE
;
607 BOOL addSpace
= (finaloutput
[0] != 0x00);
609 /* Split into components */
610 WCMD_splitpath(fullfilename
, drive
, dir
, fname
, ext
);
612 /* 5. Handle 'd' : Drive Letter */
613 if (memchrW(firstModifier
, 'd', modifierLen
) != NULL
) {
615 strcatW(finaloutput
, spaceW
);
619 strcatW(finaloutput
, drive
);
621 doneFileModifier
= TRUE
;
624 /* 6. Handle 'p' : Path */
625 if (memchrW(firstModifier
, 'p', modifierLen
) != NULL
) {
627 strcatW(finaloutput
, spaceW
);
631 strcatW(finaloutput
, dir
);
633 doneFileModifier
= TRUE
;
636 /* 7. Handle 'n' : Name */
637 if (memchrW(firstModifier
, 'n', modifierLen
) != NULL
) {
639 strcatW(finaloutput
, spaceW
);
643 strcatW(finaloutput
, fname
);
645 doneFileModifier
= TRUE
;
648 /* 8. Handle 'x' : Ext */
649 if (memchrW(firstModifier
, 'x', modifierLen
) != NULL
) {
651 strcatW(finaloutput
, spaceW
);
655 strcatW(finaloutput
, ext
);
657 doneFileModifier
= TRUE
;
660 /* If 's' but no other parameter, dump the whole thing */
661 if (!doneFileModifier
&&
662 memchrW(firstModifier
, 's', modifierLen
) != NULL
) {
664 if (finaloutput
[0] != 0x00) strcatW(finaloutput
, spaceW
);
665 strcatW(finaloutput
, outputparam
);
670 /* If No other modifier processed, just add in parameter */
671 if (!doneModifier
) strcpyW(finaloutput
, outputparam
);
673 /* Finish by inserting the replacement into the string */
674 WCMD_strsubstW(*start
, lastModifier
+1, finaloutput
, -1);
677 /*******************************************************************
678 * WCMD_call - processes a batch call statement
680 * If there is a leading ':', calls within this batch program
681 * otherwise launches another program.
683 void WCMD_call (WCHAR
*command
) {
685 /* Run other program if no leading ':' */
686 if (*command
!= ':') {
687 WCMD_run_program(command
, TRUE
);
688 /* If the thing we try to run does not exist, call returns 1 */
689 if (errorlevel
) errorlevel
=1;
692 WCHAR gotoLabel
[MAX_PATH
];
694 strcpyW(gotoLabel
, param1
);
700 /* Save the current file position, call the same file,
703 li
.u
.LowPart
= SetFilePointer(context
-> h
, li
.u
.LowPart
,
704 &li
.u
.HighPart
, FILE_CURRENT
);
706 WCMD_batch (param1
, command
, TRUE
, gotoLabel
, context
->h
);
708 SetFilePointer(context
-> h
, li
.u
.LowPart
,
709 &li
.u
.HighPart
, FILE_BEGIN
);
711 WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT
));