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
= xstrdupW(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 lstrcpyW(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
, 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 free(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
[MAXSTRING
];
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
&& (wcschr(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 (wcschr(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 return WCMD_parameter_with_delims (s
, n
, start
, raw
, wholecmdline
, L
" \t,=;");
230 /****************************************************************************
233 * Gets one line from a file/console and puts it into buffer buf
234 * Pre: buf has size noChars
235 * 1 <= noChars <= MAXSTRING
236 * Post: buf is filled with at most noChars-1 characters, and gets nul-terminated
237 buf does not include EOL terminator
240 * NULL on error or EOF
243 WCHAR
*WCMD_fgets(WCHAR
*buf
, DWORD noChars
, HANDLE h
)
249 /* We can't use the native f* functions because of the filename syntax differences
250 between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
252 if (!ReadConsoleW(h
, buf
, noChars
, &charsRead
, NULL
)) {
253 LARGE_INTEGER filepos
;
259 bufA
= xalloc(noChars
);
261 /* Save current file position */
262 filepos
.QuadPart
= 0;
263 SetFilePointerEx(h
, filepos
, &filepos
, FILE_CURRENT
);
265 status
= ReadFile(h
, bufA
, noChars
, &charsRead
, NULL
);
266 if (!status
|| charsRead
== 0) {
272 for (p
= bufA
; p
< (bufA
+ charsRead
); p
= CharNextExA(cp
, p
, 0)) {
273 if (*p
== '\n' || *p
== '\r')
277 /* Sets file pointer to the start of the next line, if any */
278 filepos
.QuadPart
+= p
- bufA
+ 1 + (*p
== '\r' ? 1 : 0);
279 SetFilePointerEx(h
, filepos
, NULL
, FILE_BEGIN
);
281 i
= MultiByteToWideChar(cp
, 0, bufA
, p
- bufA
, buf
, noChars
);
285 if (!charsRead
) return NULL
;
288 for (i
= 0; i
< charsRead
; i
++) {
289 if (buf
[i
] == '\n' || buf
[i
] == '\r')
294 /* Truncate at EOL (or end of buffer) */
303 /****************************************************************************
304 * WCMD_HandleTildeModifiers
306 * Handle the ~ modifiers when expanding %0-9 or (%a-z/A-Z in for command)
307 * %~xxxxxV (V=0-9 or A-Z, a-z)
308 * Where xxxx is any combination of:
310 * f - Fully qualified path (assumes current dir if not drive\dir)
315 * s - path with shortnames
319 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
322 * To work out the length of the modifier:
324 * Note: In the case of %0-9 knowing the end of the modifier is easy,
325 * but in a for loop, the for end WCHARacter may also be a modifier
326 * eg. for %a in (c:\a.a) do echo XXX
327 * where XXX = %~a (just ~)
328 * %~aa (~ and attributes)
329 * %~aaxa (~, attributes and extension)
330 * BUT %~aax (~ and attributes followed by 'x')
332 * Hence search forwards until find an invalid modifier, and then
333 * backwards until find for variable or 0-9
335 void WCMD_HandleTildeModifiers(WCHAR
**start
, BOOL atExecute
)
338 #define NUMMODIFIERS 11
339 static const WCHAR validmodifiers
[NUMMODIFIERS
] = {
340 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
343 WIN32_FILE_ATTRIBUTE_DATA fileInfo
;
344 WCHAR outputparam
[MAXSTRING
];
345 WCHAR finaloutput
[MAXSTRING
];
346 WCHAR fullfilename
[MAX_PATH
];
347 WCHAR thisoutput
[MAX_PATH
];
348 WCHAR
*filepart
= NULL
;
349 WCHAR
*pos
= *start
+1;
350 WCHAR
*firstModifier
= pos
;
351 WCHAR
*lastModifier
= NULL
;
353 BOOL finished
= FALSE
;
356 BOOL skipFileParsing
= FALSE
;
357 BOOL doneModifier
= FALSE
;
359 /* Search forwards until find invalid character modifier */
362 /* Work on the previous character */
363 if (lastModifier
!= NULL
) {
365 for (i
=0; i
<NUMMODIFIERS
; i
++) {
366 if (validmodifiers
[i
] == *lastModifier
) {
368 /* Special case '$' to skip until : found */
369 if (*lastModifier
== '$') {
370 while (*pos
!= ':' && *pos
) pos
++;
371 if (*pos
== 0x00) return; /* Invalid syntax */
372 pos
++; /* Skip ':' */
378 if (i
==NUMMODIFIERS
) {
383 /* Save this one away */
390 while (lastModifier
> firstModifier
) {
391 WINE_TRACE("Looking backwards for parameter id: %s\n",
392 wine_dbgstr_w(lastModifier
));
394 if (!atExecute
&& context
&& (*lastModifier
>= '0' && *lastModifier
<= '9')) {
395 /* Its a valid parameter identifier - OK */
399 int foridx
= FOR_VAR_IDX(*lastModifier
);
400 /* Its a valid parameter identifier - OK */
401 if ((foridx
>= 0) && (forloopcontext
.variable
[foridx
] != NULL
)) break;
403 /* Its not a valid parameter identifier - step backwards */
407 if (lastModifier
== firstModifier
) return; /* Invalid syntax */
409 /* So now, firstModifier points to beginning of modifiers, lastModifier
410 points to the variable just after the modifiers. Process modifiers
411 in a specific order, remembering there could be duplicates */
412 modifierLen
= lastModifier
- firstModifier
;
413 finaloutput
[0] = 0x00;
415 /* Extract the parameter to play with
416 Special case param 0 - With %~0 you get the batch label which was called
417 whereas if you start applying other modifiers to it, you get the filename
418 the batch label is in */
419 if (*lastModifier
== '0' && modifierLen
> 1) {
420 lstrcpyW(outputparam
, context
->batchfileW
);
421 } else if ((*lastModifier
>= '0' && *lastModifier
<= '9')) {
422 lstrcpyW(outputparam
,
423 WCMD_parameter (context
-> command
,
424 *lastModifier
-'0' + context
-> shift_count
[*lastModifier
-'0'],
427 int foridx
= FOR_VAR_IDX(*lastModifier
);
428 lstrcpyW(outputparam
, forloopcontext
.variable
[foridx
]);
431 /* 1. Handle '~' : Strip surrounding quotes */
432 if (outputparam
[0]=='"' &&
433 wmemchr(firstModifier
, '~', modifierLen
) != NULL
) {
434 int len
= lstrlenW(outputparam
);
435 if (outputparam
[len
-1] == '"') {
436 outputparam
[len
-1]=0x00;
439 memmove(outputparam
, &outputparam
[1], (len
* sizeof(WCHAR
))-1);
442 /* 2. Handle the special case of a $ */
443 if (wmemchr(firstModifier
, '$', modifierLen
) != NULL
) {
444 /* Special Case: Search envar specified in $[envvar] for outputparam
445 Note both $ and : are guaranteed otherwise check above would fail */
446 WCHAR
*begin
= wcschr(firstModifier
, '$') + 1;
447 WCHAR
*end
= wcschr(firstModifier
, ':');
451 /* Extract the env var */
452 memcpy(env
, begin
, (end
-begin
) * sizeof(WCHAR
));
453 env
[(end
-begin
)] = 0x00;
455 size
= GetEnvironmentVariableW(env
, NULL
, 0);
457 WCHAR
*fullpath
= malloc(size
* sizeof(WCHAR
));
458 if (!fullpath
|| (GetEnvironmentVariableW(env
, fullpath
, size
) == 0) ||
459 (SearchPathW(fullpath
, outputparam
, NULL
, MAX_PATH
, outputparam
, NULL
) == 0))
465 /* If env var not found, return empty string */
466 finaloutput
[0] = 0x00;
467 outputparam
[0] = 0x00;
468 skipFileParsing
= TRUE
;
472 /* After this, we need full information on the file,
473 which is valid not to exist. */
474 if (!skipFileParsing
) {
475 if (!WCMD_get_fullpath(outputparam
, MAX_PATH
, fullfilename
, &filepart
)) {
477 fullfilename
[0] = 0x00;
479 exists
= GetFileAttributesExW(fullfilename
, GetFileExInfoStandard
,
483 /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
484 if (wmemchr(firstModifier
, 'a', modifierLen
) != NULL
) {
489 lstrcpyW(thisoutput
, L
"---------");
490 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
492 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
)
494 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_ARCHIVE
)
496 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
)
498 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_SYSTEM
)
500 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_COMPRESSED
)
502 /* FIXME: What are 6 and 7? */
503 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
)
505 lstrcatW(finaloutput
, thisoutput
);
509 /* 3. Handle 't' : Date+time (File doesn't have to exist) */
510 if (wmemchr(firstModifier
, 't', modifierLen
) != NULL
) {
518 if (finaloutput
[0] != 0x00) lstrcatW(finaloutput
, L
" ");
520 /* Format the time */
521 FileTimeToSystemTime(&fileInfo
.ftLastWriteTime
, &systime
);
522 GetDateFormatW(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, &systime
,
523 NULL
, thisoutput
, MAX_PATH
);
524 lstrcatW(thisoutput
, L
" ");
525 datelen
= lstrlenW(thisoutput
);
526 GetTimeFormatW(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, &systime
,
527 NULL
, (thisoutput
+datelen
), MAX_PATH
-datelen
);
528 lstrcatW(finaloutput
, thisoutput
);
532 /* 4. Handle 'z' : File length (File doesn't have to exist) */
533 if (wmemchr(firstModifier
, 'z', modifierLen
) != NULL
) {
534 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
535 ULONG
/*64*/ fullsize
= /*(fileInfo.nFileSizeHigh << 32) +*/
536 fileInfo
.nFileSizeLow
;
540 if (finaloutput
[0] != 0x00) lstrcatW(finaloutput
, L
" ");
541 wsprintfW(thisoutput
, L
"%u", fullsize
);
542 lstrcatW(finaloutput
, thisoutput
);
546 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
547 if (wmemchr(firstModifier
, 's', modifierLen
) != NULL
) {
548 if (finaloutput
[0] != 0x00) lstrcatW(finaloutput
, L
" ");
550 /* Convert fullfilename's path to a short path - Save filename away as
551 only path is valid, name may not exist which causes GetShortPathName
552 to fail if it is provided */
554 lstrcpyW(thisoutput
, filepart
);
556 GetShortPathNameW(fullfilename
, fullfilename
, ARRAY_SIZE(fullfilename
));
557 lstrcatW(fullfilename
, thisoutput
);
561 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
562 /* Note this overrides d,p,n,x */
563 if (wmemchr(firstModifier
, 'f', modifierLen
) != NULL
) {
565 if (finaloutput
[0] != 0x00) lstrcatW(finaloutput
, L
" ");
566 lstrcatW(finaloutput
, fullfilename
);
571 WCHAR fname
[MAX_PATH
];
573 BOOL doneFileModifier
= FALSE
;
574 BOOL addSpace
= (finaloutput
[0] != 0x00);
576 /* Split into components */
577 _wsplitpath(fullfilename
, drive
, dir
, fname
, ext
);
579 /* 5. Handle 'd' : Drive Letter */
580 if (wmemchr(firstModifier
, 'd', modifierLen
) != NULL
) {
582 lstrcatW(finaloutput
, L
" ");
586 lstrcatW(finaloutput
, drive
);
588 doneFileModifier
= TRUE
;
591 /* 6. Handle 'p' : Path */
592 if (wmemchr(firstModifier
, 'p', modifierLen
) != NULL
) {
594 lstrcatW(finaloutput
, L
" ");
598 lstrcatW(finaloutput
, dir
);
600 doneFileModifier
= TRUE
;
603 /* 7. Handle 'n' : Name */
604 if (wmemchr(firstModifier
, 'n', modifierLen
) != NULL
) {
606 lstrcatW(finaloutput
, L
" ");
610 lstrcatW(finaloutput
, fname
);
612 doneFileModifier
= TRUE
;
615 /* 8. Handle 'x' : Ext */
616 if (wmemchr(firstModifier
, 'x', modifierLen
) != NULL
) {
618 lstrcatW(finaloutput
, L
" ");
622 lstrcatW(finaloutput
, ext
);
624 doneFileModifier
= TRUE
;
627 /* If 's' but no other parameter, dump the whole thing */
628 if (!doneFileModifier
&&
629 wmemchr(firstModifier
, 's', modifierLen
) != NULL
) {
631 if (finaloutput
[0] != 0x00) lstrcatW(finaloutput
, L
" ");
632 lstrcatW(finaloutput
, fullfilename
);
637 /* If No other modifier processed, just add in parameter */
638 if (!doneModifier
) lstrcpyW(finaloutput
, outputparam
);
640 /* Finish by inserting the replacement into the string */
641 WCMD_strsubstW(*start
, lastModifier
+1, finaloutput
, -1);
644 /*******************************************************************
645 * WCMD_call - processes a batch call statement
647 * If there is a leading ':', calls within this batch program
648 * otherwise launches another program.
650 void WCMD_call (WCHAR
*command
) {
652 /* Run other program if no leading ':' */
653 if (*command
!= ':') {
654 WCMD_run_program(command
, TRUE
);
655 /* If the thing we try to run does not exist, call returns 1 */
656 if (errorlevel
) errorlevel
=1;
659 WCHAR gotoLabel
[MAX_PATH
];
661 lstrcpyW(gotoLabel
, param1
);
666 FOR_CONTEXT oldcontext
;
668 /* Save the for variable context, then start with an empty context
669 as for loop variables do not survive a call */
670 oldcontext
= forloopcontext
;
671 memset(&forloopcontext
, 0, sizeof(forloopcontext
));
673 /* Save the current file position, call the same file,
676 li
.u
.LowPart
= SetFilePointer(context
-> h
, li
.u
.LowPart
,
677 &li
.u
.HighPart
, FILE_CURRENT
);
678 WCMD_batch (context
->batchfileW
, command
, TRUE
, gotoLabel
, context
->h
);
679 SetFilePointer(context
-> h
, li
.u
.LowPart
,
680 &li
.u
.HighPart
, FILE_BEGIN
);
682 /* Restore the for loop context */
683 forloopcontext
= oldcontext
;
685 WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT
));