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, const WCHAR *forVariable,
295 const WCHAR *forValue, BOOL justFors) {
297 #define NUMMODIFIERS 11
298 static const WCHAR validmodifiers[NUMMODIFIERS] = {
299 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
301 static const WCHAR space[] = {' ', '\0'};
303 WIN32_FILE_ATTRIBUTE_DATA fileInfo;
304 WCHAR outputparam[MAX_PATH];
305 WCHAR finaloutput[MAX_PATH];
306 WCHAR fullfilename[MAX_PATH];
307 WCHAR thisoutput[MAX_PATH];
308 WCHAR *pos = *start+1;
309 WCHAR *firstModifier = pos;
310 WCHAR *lastModifier = NULL;
312 BOOL finished = FALSE;
315 BOOL skipFileParsing = FALSE;
316 BOOL doneModifier = FALSE;
318 /* Search forwards until find invalid character modifier */
321 /* Work on the previous character */
322 if (lastModifier != NULL) {
324 for (i=0; i<NUMMODIFIERS; i++) {
325 if (validmodifiers[i] == *lastModifier) {
327 /* Special case '$' to skip until : found */
328 if (*lastModifier == '$') {
329 while (*pos != ':' && *pos) pos++;
330 if (*pos == 0x00) return; /* Invalid syntax */
331 pos++; /* Skip ':' */
337 if (i==NUMMODIFIERS) {
342 /* Save this one away */
349 while (lastModifier > firstModifier) {
350 WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
351 wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
353 if (!justFors && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
354 /* Its a valid parameter identifier - OK */
357 } else if (forVariable && *lastModifier == *(forVariable+1)) {
358 /* Its a valid parameter identifier - OK */
365 if (lastModifier == firstModifier) return; /* Invalid syntax */
367 /* Extract the parameter to play with */
368 if (*lastModifier == '0') {
369 strcpyW(outputparam, context->batchfileW);
370 } else if ((*lastModifier >= '1' && *lastModifier <= '9')) {
371 strcpyW(outputparam, WCMD_parameter (context -> command,
372 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
374 strcpyW(outputparam, forValue);
377 /* So now, firstModifier points to beginning of modifiers, lastModifier
378 points to the variable just after the modifiers. Process modifiers
379 in a specific order, remembering there could be duplicates */
380 modifierLen = lastModifier - firstModifier;
381 finaloutput[0] = 0x00;
383 /* Useful for debugging purposes: */
384 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
385 (modifierLen), (modifierLen), firstModifier, *lastModifier,
388 /* 1. Handle '~' : Strip surrounding quotes */
389 if (outputparam[0]=='"' &&
390 memchrW(firstModifier, '~', modifierLen) != NULL) {
391 int len = strlenW(outputparam);
392 if (outputparam[len-1] == '"') {
393 outputparam[len-1]=0x00;
396 memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
399 /* 2. Handle the special case of a $ */
400 if (memchrW(firstModifier, '$', modifierLen) != NULL) {
401 /* Special Case: Search envar specified in $[envvar] for outputparam
402 Note both $ and : are guaranteed otherwise check above would fail */
403 WCHAR *begin = strchrW(firstModifier, '$') + 1;
404 WCHAR *end = strchrW(firstModifier, ':');
406 WCHAR fullpath[MAX_PATH];
408 /* Extract the env var */
409 memcpy(env, begin, (end-begin) * sizeof(WCHAR));
410 env[(end-begin)] = 0x00;
412 /* If env var not found, return empty string */
413 if ((GetEnvironmentVariableW(env, fullpath, MAX_PATH) == 0) ||
414 (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0)) {
415 finaloutput[0] = 0x00;
416 outputparam[0] = 0x00;
417 skipFileParsing = TRUE;
421 /* After this, we need full information on the file,
422 which is valid not to exist. */
423 if (!skipFileParsing) {
424 if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, NULL) == 0)
427 exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
430 /* 2. Handle 'a' : Output attributes */
432 memchrW(firstModifier, 'a', modifierLen) != NULL) {
434 WCHAR defaults[] = {'-','-','-','-','-','-','-','-','-','\0'};
436 strcpyW(thisoutput, defaults);
437 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
439 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
441 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
443 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
445 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
447 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
449 /* FIXME: What are 6 and 7? */
450 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
452 strcatW(finaloutput, thisoutput);
455 /* 3. Handle 't' : Date+time */
457 memchrW(firstModifier, 't', modifierLen) != NULL) {
463 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
465 /* Format the time */
466 FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
467 GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
468 NULL, thisoutput, MAX_PATH);
469 strcatW(thisoutput, space);
470 datelen = strlenW(thisoutput);
471 GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
472 NULL, (thisoutput+datelen), MAX_PATH-datelen);
473 strcatW(finaloutput, thisoutput);
476 /* 4. Handle 'z' : File length */
478 memchrW(firstModifier, 'z', modifierLen) != NULL) {
479 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
480 ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
481 fileInfo.nFileSizeLow;
482 static const WCHAR fmt[] = {'%','u','\0'};
485 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
486 wsprintfW(thisoutput, fmt, fullsize);
487 strcatW(finaloutput, thisoutput);
490 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
491 if (memchrW(firstModifier, 's', modifierLen) != NULL) {
492 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
493 /* Don't flag as doneModifier - %~s on its own is processed later */
494 GetShortPathNameW(outputparam, outputparam, sizeof(outputparam)/sizeof(outputparam[0]));
497 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
498 /* Note this overrides d,p,n,x */
499 if (memchrW(firstModifier, 'f', modifierLen) != NULL) {
501 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
502 strcatW(finaloutput, fullfilename);
507 WCHAR fname[MAX_PATH];
509 BOOL doneFileModifier = FALSE;
511 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
513 /* Split into components */
514 WCMD_splitpath(fullfilename, drive, dir, fname, ext);
516 /* 5. Handle 'd' : Drive Letter */
517 if (memchrW(firstModifier, 'd', modifierLen) != NULL) {
518 strcatW(finaloutput, drive);
520 doneFileModifier = TRUE;
523 /* 6. Handle 'p' : Path */
524 if (memchrW(firstModifier, 'p', modifierLen) != NULL) {
525 strcatW(finaloutput, dir);
527 doneFileModifier = TRUE;
530 /* 7. Handle 'n' : Name */
531 if (memchrW(firstModifier, 'n', modifierLen) != NULL) {
532 strcatW(finaloutput, fname);
534 doneFileModifier = TRUE;
537 /* 8. Handle 'x' : Ext */
538 if (memchrW(firstModifier, 'x', modifierLen) != NULL) {
539 strcatW(finaloutput, ext);
541 doneFileModifier = TRUE;
544 /* If 's' but no other parameter, dump the whole thing */
545 if (!doneFileModifier &&
546 memchrW(firstModifier, 's', modifierLen) != NULL) {
548 if (finaloutput[0] != 0x00) strcatW(finaloutput, space);
549 strcatW(finaloutput, outputparam);
554 /* If No other modifier processed, just add in parameter */
555 if (!doneModifier) strcpyW(finaloutput, outputparam);
557 /* Finish by inserting the replacement into the string */
558 WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
561 /*******************************************************************
562 * WCMD_call - processes a batch call statement
564 * If there is a leading ':', calls within this batch program
565 * otherwise launches another program.
567 void WCMD_call (WCHAR *command) {
569 /* Run other program if no leading ':' */
570 if (*command != ':') {
571 WCMD_run_program(command, 1);
574 WCHAR gotoLabel[MAX_PATH];
576 strcpyW(gotoLabel, param1);
582 /* Save the current file position, call the same file,
585 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
586 &li.u.HighPart, FILE_CURRENT);
588 WCMD_batch (param1, command, 1, gotoLabel, context->h);
590 SetFilePointer(context -> h, li.u.LowPart,
591 &li.u.HighPart, FILE_BEGIN);
593 WCMD_output_asis( WCMD_LoadMessage(WCMD_CALLINSCRIPT));