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 prev_context -> skip_rest = TRUE;
117 context = prev_context;
119 context = prev_context;
122 /*******************************************************************
125 * Extracts a delimited parameter from an input string
128 * s [I] input string, non NULL
129 * n [I] # of the parameter to return, counted from 0
130 * start [O] Optional. Pointer to the first char of param n in s
131 * end [O] Optional. Pointer to the last char of param n in s
132 * raw [I] True to return the parameter in raw format (quotes maintained)
133 * False returns the parameter with quotes stripped
134 * wholecmdline [I] True to indicate this routine is being used to parse the
135 * command line, and special logic for arg0->1 transition
136 * needs to be applied.
139 * Success: The nth delimited parameter found in s
140 * if start != NULL, *start points to the start of the param
141 * if end != NULL, *end points to the end of the param
142 * Failure: An empty string if the param is not found.
143 * *start == *end == NULL
146 * Return value is stored in static storage (i.e. overwritten after each call).
147 * Specify 'start' and/or 'end' to include delimiting double quotes as well, if any.
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 (WCHAR *s, int n, WCHAR **start, WCHAR **end, BOOL raw,
155 static const WCHAR defaultDelims[] = { ' ', '\t', ',', '=', ';', '\0' };
157 static WCHAR param[MAX_PATH];
158 WCHAR *p = s, *begin;
160 if (start != NULL) *start = NULL;
161 if (end != NULL) *end = NULL;
166 /* Absorb repeated word delimiters until we get to the next token (or the end!) */
167 while (*p && (strchrW(defaultDelims, *p) != NULL))
169 if (*p == '\0') return param;
171 /* If we have reached the token number we want, remember the beginning of it */
172 if (start != NULL && curParamNb == n) *start = p;
174 /* Return the whole word up to the next delimiter, handling quotes in the middle
175 of it, e.g. a"\b c\"d is a single parameter. */
178 /* Loop character by character, but just need to special case quotes */
180 /* Once we have found a delimiter, break */
181 if (strchrW(defaultDelims, *p) != NULL) break;
183 /* Very odd special case - Seems as if a ( acts as a delimiter which is
184 not swallowed but is effective only when it comes between the program
185 name and the parameters. Need to avoid this triggering when used
186 to walk parameters generally. */
187 if (wholecmdline && curParamNb == 0 && *p=='(') break;
189 /* If we find a quote, copy until we get the end quote */
192 while (*p && *p != '"') p++;
195 /* Now skip the character / quote */
199 if (curParamNb == n) {
200 /* Return the parameter in static storage either as-is (raw) or
201 suitable for use with other win32 api calls (quotes stripped) */
203 memcpy(param, begin, (p - begin) * sizeof(WCHAR));
204 param[p-begin] = '\0';
208 if (*begin != '"') param[i++] = *begin;
213 if (end) *end = p - 1;
220 /****************************************************************************
223 * Gets one line from a file/console and puts it into buffer buf
224 * Pre: buf has size noChars
225 * 1 <= noChars <= MAXSTRING
226 * Post: buf is filled with at most noChars-1 characters, and gets nul-terminated
227 buf does not include EOL terminator
230 * NULL on error or EOF
233 WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
237 LARGE_INTEGER filepos;
240 /* We can't use the native f* functions because of the filename syntax differences
241 between DOS and Unix. Also need to lose the LF (or CRLF) from the line. */
243 if (!WCMD_is_console_handle(h)) {
244 /* Save current file position */
245 filepos.QuadPart = 0;
246 SetFilePointerEx(h, filepos, &filepos, FILE_CURRENT);
249 status = WCMD_ReadFile(h, buf, noChars, &charsRead);
250 if (!status || charsRead == 0) return NULL;
253 for (i = 0; i < charsRead; i++) {
254 if (buf[i] == '\n' || buf[i] == '\r')
258 if (!WCMD_is_console_handle(h) && i != charsRead) {
259 /* Sets file pointer to the start of the next line, if any */
260 filepos.QuadPart += i + 1 + (buf[i] == '\r' ? 1 : 0);
261 SetFilePointerEx(h, filepos, NULL, FILE_BEGIN);
264 /* Truncate at EOL (or end of buffer) */
273 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
274 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext)
276 const WCHAR* end; /* end of processed string */
277 const WCHAR* p; /* search pointer */
278 const WCHAR* s; /* copy pointer */
280 /* extract drive name */
281 if (path[0] && path[1]==':') {
290 end = path + strlenW(path);
292 /* search for begin of file extension */
293 for(p=end; p>path && *--p!='\\' && *p!='/'; )
300 for(s=end; (*ext=*s++); )
303 /* search for end of directory name */
305 if (*--p=='\\' || *p=='/') {
325 /****************************************************************************
326 * WCMD_HandleTildaModifiers
328 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
329 * %~xxxxxV (V=0-9 or A-Z)
330 * Where xxxx is any combination of:
332 * f - Fully qualified path (assumes current dir if not drive\dir)
337 * s - path with shortnames
341 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
344 * To work out the length of the modifier:
346 * Note: In the case of %0-9 knowing the end of the modifier is easy,
347 * but in a for loop, the for end WCHARacter may also be a modifier
348 * eg. for %a in (c:\a.a) do echo XXX
349 * where XXX = %~a (just ~)
350 * %~aa (~ and attributes)
351 * %~aaxa (~, attributes and extension)
352 * BUT %~aax (~ and attributes followed by 'x')
354 * Hence search forwards until find an invalid modifier, and then
355 * backwards until find for variable or 0-9
357 void WCMD_HandleTildaModifiers(WCHAR **start, const WCHAR *forVariable,
358 const WCHAR *forValue, BOOL justFors) {
360 #define NUMMODIFIERS 11
361 static const WCHAR validmodifiers[NUMMODIFIERS] = {
362 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
365 WIN32_FILE_ATTRIBUTE_DATA fileInfo;
366 WCHAR outputparam[MAX_PATH];
367 WCHAR finaloutput[MAX_PATH];
368 WCHAR fullfilename[MAX_PATH];
369 WCHAR thisoutput[MAX_PATH];
370 WCHAR *pos = *start+1;
371 WCHAR *firstModifier = pos;
372 WCHAR *lastModifier = NULL;
374 BOOL finished = FALSE;
377 BOOL skipFileParsing = FALSE;
378 BOOL doneModifier = FALSE;
380 /* Search forwards until find invalid character modifier */
383 /* Work on the previous character */
384 if (lastModifier != NULL) {
386 for (i=0; i<NUMMODIFIERS; i++) {
387 if (validmodifiers[i] == *lastModifier) {
389 /* Special case '$' to skip until : found */
390 if (*lastModifier == '$') {
391 while (*pos != ':' && *pos) pos++;
392 if (*pos == 0x00) return; /* Invalid syntax */
393 pos++; /* Skip ':' */
399 if (i==NUMMODIFIERS) {
404 /* Save this one away */
411 while (lastModifier > firstModifier) {
412 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
413 wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
415 if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
416 /* Its a valid parameter identifier - OK */
419 } else if (forVariable && *lastModifier == *(forVariable+1)) {
420 /* Its a valid parameter identifier - OK */
427 if (lastModifier == firstModifier) return; /* Invalid syntax */
429 /* Extract the parameter to play with */
430 if (*lastModifier == '0') {
431 strcpyW(outputparam, context->batchfileW);
432 } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
434 WCMD_parameter (context -> command,
435 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'],
436 NULL, NULL, FALSE, TRUE));
438 strcpyW(outputparam, forValue);
441 /* So now, firstModifier points to beginning of modifiers, lastModifier
442 points to the variable just after the modifiers. Process modifiers
443 in a specific order, remembering there could be duplicates */
444 modifierLen = lastModifier - firstModifier;
445 finaloutput[0] = 0x00;
447 /* 1. Handle '~' : Strip surrounding quotes */
448 if (outputparam[0]=='"' &&
449 memchrW(firstModifier, '~', modifierLen) != NULL) {
450 int len = strlenW(outputparam);
451 if (outputparam[len-1] == '"') {
452 outputparam[len-1]=0x00;
455 memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
458 /* 2. Handle the special case of a $ */
459 if (memchrW(firstModifier, '$', modifierLen) != NULL) {
460 /* Special Case: Search envar specified in $[envvar] for outputparam
461 Note both $ and : are guaranteed otherwise check above would fail */
462 WCHAR *begin = strchrW(firstModifier, '$') + 1;
463 WCHAR *end = strchrW(firstModifier, ':');
465 WCHAR fullpath[MAX_PATH];
467 /* Extract the env var */
468 memcpy(env, begin, (end-begin) * sizeof(WCHAR));
469 env[(end-begin)] = 0x00;
471 /* If env var not found, return empty string */
472 if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
473 (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
474 finaloutput[0] = 0x00;
475 outputparam[0] = 0x00;
476 skipFileParsing = TRUE;
480 /* After this, we need full information on the file,
481 which is valid not to exist. */
482 if (!skipFileParsing) {
483 if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
486 exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
489 /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
490 if (memchrW(firstModifier, 'a', modifierLen) != NULL) {
492 WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
496 strcpyW(thisoutput, defaults);
497 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
499 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
501 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
503 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
505 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
507 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
509 /* FIXME: What are 6 and 7? */
510 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
512 strcatW(finaloutput, thisoutput);
516 /* 3. Handle 't' : Date+time (File doesn't have to exist) */
517 if (memchrW(firstModifier, 't', modifierLen) != NULL) {
525 if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
527 /* Format the time */
528 FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
529 GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
530 NULL, thisoutput, MAX_PATH);
531 strcatW(thisoutput, spaceW);
532 datelen = strlenW(thisoutput);
533 GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
534 NULL, (thisoutput+datelen), MAX_PATH-datelen);
535 strcatW(finaloutput, thisoutput);
539 /* 4. Handle 'z' : File length (File doesn't have to exist) */
540 if (memchrW(firstModifier, 'z', modifierLen) != NULL) {
541 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
542 ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
543 fileInfo.nFileSizeLow;
544 static const WCHAR fmt[] = {'%','u','\0'};
548 if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
549 wsprintfW(thisoutput, fmt, fullsize);
550 strcatW(finaloutput, thisoutput);
554 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
555 if (memchrW(firstModifier, 's', modifierLen) != NULL) {
556 if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
557 /* Don't flag as doneModifier - %~s on its own is processed later */
558 GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
561 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
562 /* Note this overrides d,p,n,x */
563 if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
565 if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
566 strcatW(finaloutput, fullfilename);
571 WCHAR fname[MAX_PATH];
573 BOOL doneFileModifier = FALSE;
574 BOOL addSpace = (finaloutput[0] != 0x00);
576 /* Split into components */
577 WCMD_splitpath(fullfilename, drive, dir, fname, ext);
579 /* 5. Handle 'd' : Drive Letter */
580 if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
582 strcatW(finaloutput, spaceW);
586 strcatW(finaloutput, drive);
588 doneFileModifier = TRUE;
591 /* 6. Handle 'p' : Path */
592 if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
594 strcatW(finaloutput, spaceW);
598 strcatW(finaloutput, dir);
600 doneFileModifier = TRUE;
603 /* 7. Handle 'n' : Name */
604 if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
606 strcatW(finaloutput, spaceW);
610 strcatW(finaloutput, fname);
612 doneFileModifier = TRUE;
615 /* 8. Handle 'x' : Ext */
616 if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
618 strcatW(finaloutput, spaceW);
622 strcatW(finaloutput, ext);
624 doneFileModifier = TRUE;
627 /* If 's' but no other parameter, dump the whole thing */
628 if (!doneFileModifier &&
629 memchrW(firstModifier, 's', modifierLen) != NULL) {
631 if (finaloutput[0] != 0x00) strcatW(finaloutput, spaceW);
632 strcatW(finaloutput, outputparam);
637 /* If No other modifier processed, just add in parameter */
638 if (!doneModifier) strcpyW(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 strcpyW(gotoLabel, param1);
667 /* Save the current file position, call the same file,
670 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
671 &li.u.HighPart, FILE_CURRENT);
673 WCMD_batch (param1, command, TRUE, gotoLabel, context->h);
675 SetFilePointer(context -> h, li.u.LowPart,
676 &li.u.HighPart, FILE_BEGIN);
678 WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT));