2 * CMD - Wine-compatible command line interface - batch interface.
4 * Copyright (C) 1999 D A Pickles
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
23 void WCMD_batch_command (char *line
);
24 void WCMD_HandleTildaModifiers(char **start
, char *forVariable
);
27 extern char quals
[MAX_PATH
], param1
[MAX_PATH
], param2
[MAX_PATH
];
28 extern BATCH_CONTEXT
*context
;
29 extern DWORD errorlevel
;
31 /* msdn specified max for Win XP */
32 #define MAXSTRING 8192
34 /****************************************************************************
37 * Open and execute a batch file.
38 * On entry *command includes the complete command line beginning with the name
39 * of the batch file (if a CALL command was entered the CALL has been removed).
40 * *file is the name of the file, which might not exist and may not have the
41 * .BAT suffix on. Called is 1 for a CALL, 0 otherwise.
43 * We need to handle recursion correctly, since one batch program might call another.
44 * So parameters for this batch file are held in a BATCH_CONTEXT structure.
47 void WCMD_batch (char *file
, char *command
, int called
) {
49 #define WCMD_BATCH_EXT_SIZE 5
51 HANDLE h
= INVALID_HANDLE_VALUE
;
52 char string
[MAXSTRING
];
53 char extension_batch
[][WCMD_BATCH_EXT_SIZE
] = {".bat",".cmd"};
54 char extension_exe
[WCMD_BATCH_EXT_SIZE
] = ".exe";
56 BATCH_CONTEXT
*prev_context
;
58 for(i
=0; (i
<(sizeof(extension_batch
)/WCMD_BATCH_EXT_SIZE
)) &&
59 (h
== INVALID_HANDLE_VALUE
); i
++) {
60 strcpy (string
, file
);
62 if (strstr (string
, extension_batch
[i
]) == NULL
) strcat (string
, extension_batch
[i
]);
63 h
= CreateFile (string
, GENERIC_READ
, FILE_SHARE_READ
,
64 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
, NULL
);
66 if (h
== INVALID_HANDLE_VALUE
) {
67 strcpy (string
, file
);
69 if (strstr (string
, extension_exe
) == NULL
) strcat (string
, extension_exe
);
70 h
= CreateFile (string
, GENERIC_READ
, FILE_SHARE_READ
,
71 NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
, NULL
);
72 if (h
!= INVALID_HANDLE_VALUE
) {
73 WCMD_run_program (command
, 0);
75 SetLastError (ERROR_FILE_NOT_FOUND
);
82 * Create a context structure for this batch file.
85 prev_context
= context
;
86 context
= (BATCH_CONTEXT
*)LocalAlloc (LMEM_FIXED
, sizeof (BATCH_CONTEXT
));
88 context
-> command
= command
;
89 context
-> shift_count
= 0;
90 context
-> prev_context
= prev_context
;
91 context
-> skip_rest
= FALSE
;
94 * Work through the file line by line. Specific batch commands are processed here,
95 * the rest are handled by the main command processor.
98 while (context
-> skip_rest
== FALSE
&& WCMD_fgets (string
, sizeof(string
), h
)) {
99 if (strlen(string
) == MAXSTRING
-1) {
100 WCMD_output_asis( "Line in Batch processing possibly truncated. Using:\n");
101 WCMD_output_asis( string
);
102 WCMD_output_asis( "\n");
104 if (string
[0] != ':') { /* Skip over labels */
105 WCMD_batch_command (string
);
111 * If invoked by a CALL, we return to the context of our caller. Otherwise return
112 * to the caller's caller.
115 LocalFree ((HANDLE
)context
);
116 if ((prev_context
!= NULL
) && (!called
)) {
117 CloseHandle (prev_context
-> h
);
118 context
= prev_context
-> prev_context
;
119 LocalFree ((HANDLE
)prev_context
);
122 context
= prev_context
;
126 /****************************************************************************
129 * Execute one line from a batch file, expanding parameters.
132 void WCMD_batch_command (char *line
) {
135 char cmd1
[MAXSTRING
],cmd2
[MAXSTRING
];
139 /* Get working version of command line */
142 /* Expand environment variables in a batch file %{0-9} first */
143 /* Then env vars, and if any left (ie use of undefined vars,*/
144 /* replace with spaces */
145 /* FIXME: Winnt would replace %1%fred%1 with first parm, then */
146 /* contents of fred, then the digit 1. Would need to remove */
147 /* ExpandEnvStrings to achieve this */
149 /* Replace use of %0...%9 and errorlevel*/
151 while ((p
= strchr(p
, '%'))) {
154 WCMD_HandleTildaModifiers(&p
, NULL
);
156 } else if ((i
>= 0) && (i
<= 9)) {
158 t
= WCMD_parameter (context
-> command
, i
+ context
-> shift_count
, NULL
);
162 } else if (*(p
+1)=='*') {
163 char *startOfParms
= NULL
;
165 t
= WCMD_parameter (context
-> command
, 1, &startOfParms
);
166 if (startOfParms
!= NULL
) strcpy (p
, startOfParms
);
171 /* Handle DATE, TIME, ERRORLEVEL and CD replacements allowing */
172 /* override if existing env var called that name */
173 } else if ((CompareString (LOCALE_USER_DEFAULT
,
174 NORM_IGNORECASE
| SORT_STRINGSORT
,
175 (p
+1), 11, "ERRORLEVEL%", -1) == 2) &&
176 (GetEnvironmentVariable("ERRORLEVEL", cmd2
, 1) == 0) &&
177 (GetLastError() == ERROR_ENVVAR_NOT_FOUND
)) {
179 sprintf(output
, "%d", errorlevel
);
184 } else if ((CompareString (LOCALE_USER_DEFAULT
,
185 NORM_IGNORECASE
| SORT_STRINGSORT
,
186 (p
+1), 5, "DATE%", -1) == 2) &&
187 (GetEnvironmentVariable("DATE", cmd2
, 1) == 0) &&
188 (GetLastError() == ERROR_ENVVAR_NOT_FOUND
)) {
190 GetDateFormat(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, NULL
,
191 NULL
, cmd2
, MAXSTRING
);
196 } else if ((CompareString (LOCALE_USER_DEFAULT
,
197 NORM_IGNORECASE
| SORT_STRINGSORT
,
198 (p
+1), 5, "TIME%", -1) == 2) &&
199 (GetEnvironmentVariable("TIME", cmd2
, 1) == 0) &&
200 (GetLastError() == ERROR_ENVVAR_NOT_FOUND
)) {
201 GetTimeFormat(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, NULL
,
202 NULL
, cmd2
, MAXSTRING
);
207 } else if ((CompareString (LOCALE_USER_DEFAULT
,
208 NORM_IGNORECASE
| SORT_STRINGSORT
,
209 (p
+1), 3, "CD%", -1) == 2) &&
210 (GetEnvironmentVariable("CD", cmd2
, 1) == 0) &&
211 (GetLastError() == ERROR_ENVVAR_NOT_FOUND
)) {
212 GetCurrentDirectory (MAXSTRING
, cmd2
);
222 /* Now replace environment variables */
223 status
= ExpandEnvironmentStrings(cmd1
, cmd2
, sizeof(cmd2
));
229 /* In a batch program, unknown variables are replace by nothing */
230 /* so remove any remaining %var% */
232 while ((p
= strchr(p
, '%'))) {
233 s
= strchr(p
+1, '%');
243 /* Show prompt before batch line IF echo is on */
244 if (echo_mode
&& (line
[0] != '@')) {
246 WCMD_output_asis ( cmd2
);
247 WCMD_output_asis ( "\n");
250 WCMD_process_command (cmd2
);
253 /*******************************************************************
254 * WCMD_parameter - extract a parameter from a command line.
256 * Returns the 'n'th space-delimited parameter on the command line (zero-based).
257 * Parameter is in static storage overwritten on the next call.
258 * Parameters in quotes (and brackets) are handled.
259 * Also returns a pointer to the location of the parameter in the command line.
262 char *WCMD_parameter (char *s
, int n
, char **where
) {
265 static char param
[MAX_PATH
];
275 if (where
!= NULL
) *where
= s
;
277 while ((*s
!= '\0') && (*s
!= '"')) {
290 if (where
!= NULL
) *where
= s
;
292 while ((*s
!= '\0') && (*s
!= ')')) {
307 /* Only return where if it is for the right parameter */
308 if (where
!= NULL
&& i
==n
) *where
= s
;
309 while ((*s
!= '\0') && (*s
!= ' ')) {
323 /****************************************************************************
326 * Get one line from a batch file. We can't use the native f* functions because
327 * of the filename syntax differences between DOS and Unix. Also need to lose
328 * the LF (or CRLF) from the line.
331 char *WCMD_fgets (char *s
, int n
, HANDLE h
) {
339 status
= ReadFile (h
, s
, 1, &bytes
, NULL
);
340 if ((status
== 0) || ((bytes
== 0) && (s
== p
))) return NULL
;
341 if (*s
== '\n') bytes
= 0;
342 else if (*s
!= '\r') {
347 } while ((bytes
== 1) && (n
> 1));
351 /* _splitpath - copied from winefile as no obvious way to use it otherwise */
352 void _splitpath(const CHAR
* path
, CHAR
* drv
, CHAR
* dir
, CHAR
* name
, CHAR
* ext
)
354 const CHAR
* end
; /* end of processed string */
355 const CHAR
* p
; /* search pointer */
356 const CHAR
* s
; /* copy pointer */
358 /* extract drive name */
359 if (path
[0] && path
[1]==':') {
368 /* search for end of string or stream separator */
369 for(end
=path
; *end
&& *end
!=':'; )
372 /* search for begin of file extension */
373 for(p
=end
; p
>path
&& *--p
!='\\' && *p
!='/'; )
380 for(s
=end
; (*ext
=*s
++); )
383 /* search for end of directory name */
385 if (*--p
=='\\' || *p
=='/') {
405 /****************************************************************************
406 * WCMD_HandleTildaModifiers
408 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
409 * %~xxxxxV (V=0-9 or A-Z)
410 * Where xxxx is any combination of:
412 * f - Fully qualified path (assumes current dir if not drive\dir)
417 * s - path with shortnames
421 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
424 * To work out the length of the modifier:
426 * Note: In the case of %0-9 knowing the end of the modifier is easy,
427 * but in a for loop, the for end character may also be a modifier
428 * eg. for %a in (c:\a.a) do echo XXX
429 * where XXX = %~a (just ~)
430 * %~aa (~ and attributes)
431 * %~aaxa (~, attributes and extension)
432 * BUT %~aax (~ and attributes followed by 'x')
434 * Hence search forwards until find an invalid modifier, and then
435 * backwards until find for variable or 0-9
437 void WCMD_HandleTildaModifiers(char **start
, char *forVariable
) {
439 #define NUMMODIFIERS 11
440 const char validmodifiers
[NUMMODIFIERS
] = {
441 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
444 WIN32_FILE_ATTRIBUTE_DATA fileInfo
;
445 char outputparam
[MAX_PATH
];
446 char finaloutput
[MAX_PATH
];
447 char fullfilename
[MAX_PATH
];
448 char thisoutput
[MAX_PATH
];
449 char *pos
= *start
+1;
450 char *firstModifier
= pos
;
451 char *lastModifier
= NULL
;
453 BOOL finished
= FALSE
;
456 BOOL skipFileParsing
= FALSE
;
457 BOOL doneModifier
= FALSE
;
459 /* Search forwards until find invalid character modifier */
462 /* Work on the previous character */
463 if (lastModifier
!= NULL
) {
465 for (i
=0; i
<NUMMODIFIERS
; i
++) {
466 if (validmodifiers
[i
] == *lastModifier
) {
468 /* Special case '$' to skip until : found */
469 if (*lastModifier
== '$') {
470 while (*pos
!= ':' && *pos
) pos
++;
471 if (*pos
== 0x00) return; /* Invalid syntax */
472 pos
++; /* Skip ':' */
478 if (i
==NUMMODIFIERS
) {
483 /* Save this one away */
490 /* Now make sure the position we stopped at is a valid parameter */
491 if (!(*lastModifier
>= '0' || *lastModifier
<= '9') &&
492 (forVariable
!= NULL
) &&
493 (toupper(*lastModifier
) != toupper(*forVariable
))) {
495 /* Its not... Step backwards until it matches or we get to the start */
496 while (toupper(*lastModifier
) != toupper(*forVariable
) &&
497 lastModifier
> firstModifier
) {
500 if (lastModifier
== firstModifier
) return; /* Invalid syntax */
503 /* Extract the parameter to play with */
504 if ((*lastModifier
>= '0' && *lastModifier
<= '9')) {
505 strcpy(outputparam
, WCMD_parameter (context
-> command
,
506 *lastModifier
-'0' + context
-> shift_count
, NULL
));
508 /* FIXME: Retrieve 'for' variable %c\n", *lastModifier); */
509 /* Need to get 'for' loop variable into outputparam */
513 /* So now, firstModifier points to beginning of modifiers, lastModifier
514 points to the variable just after the modifiers. Process modifiers
515 in a specific order, remembering there could be duplicates */
516 modifierLen
= lastModifier
- firstModifier
;
517 finaloutput
[0] = 0x00;
519 /* Useful for debugging purposes: */
520 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
521 (modifierLen), (modifierLen), firstModifier, *lastModifier,
524 /* 1. Handle '~' : Strip surrounding quotes */
525 if (outputparam
[0]=='"' &&
526 memchr(firstModifier
, '~', modifierLen
) != NULL
) {
527 int len
= strlen(outputparam
);
528 if (outputparam
[len
-1] == '"') {
529 outputparam
[len
-1]=0x00;
532 memmove(outputparam
, &outputparam
[1], len
-1);
535 /* 2. Handle the special case of a $ */
536 if (memchr(firstModifier
, '$', modifierLen
) != NULL
) {
537 /* Special Case: Search envar specified in $[envvar] for outputparam
538 Note both $ and : are guaranteed otherwise check above would fail */
539 char *start
= strchr(firstModifier
, '$') + 1;
540 char *end
= strchr(firstModifier
, ':');
542 char fullpath
[MAX_PATH
];
544 /* Extract the env var */
545 strncpy(env
, start
, (end
-start
));
546 env
[(end
-start
)] = 0x00;
548 /* If env var not found, return emptry string */
549 if ((GetEnvironmentVariable(env
, fullpath
, MAX_PATH
) == 0) ||
550 (SearchPath(fullpath
, outputparam
, NULL
,
551 MAX_PATH
, outputparam
, NULL
) == 0)) {
552 finaloutput
[0] = 0x00;
553 outputparam
[0] = 0x00;
554 skipFileParsing
= TRUE
;
558 /* After this, we need full information on the file,
559 which is valid not to exist. */
560 if (!skipFileParsing
) {
561 if (GetFullPathName(outputparam
, MAX_PATH
, fullfilename
, NULL
) == 0)
564 exists
= GetFileAttributesExA(fullfilename
, GetFileExInfoStandard
,
567 /* 2. Handle 'a' : Output attributes */
569 memchr(firstModifier
, 'a', modifierLen
) != NULL
) {
572 strcpy(thisoutput
, "---------");
573 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
575 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
)
577 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_ARCHIVE
)
579 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
)
581 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_SYSTEM
)
583 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_COMPRESSED
)
585 /* FIXME: What are 6 and 7? */
586 if (fileInfo
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
)
588 strcat(finaloutput
, thisoutput
);
591 /* 3. Handle 't' : Date+time */
593 memchr(firstModifier
, 't', modifierLen
) != NULL
) {
599 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
601 /* Format the time */
602 FileTimeToSystemTime(&fileInfo
.ftLastWriteTime
, &systime
);
603 GetDateFormat(LOCALE_USER_DEFAULT
, DATE_SHORTDATE
, &systime
,
604 NULL
, thisoutput
, MAX_PATH
);
605 strcat(thisoutput
, " ");
606 datelen
= strlen(thisoutput
);
607 GetTimeFormat(LOCALE_USER_DEFAULT
, TIME_NOSECONDS
, &systime
,
608 NULL
, (thisoutput
+datelen
), MAX_PATH
-datelen
);
609 strcat(finaloutput
, thisoutput
);
612 /* 4. Handle 'z' : File length */
614 memchr(firstModifier
, 'z', modifierLen
) != NULL
) {
615 /* FIXME: Output full 64 bit size (sprintf not support I64 here) */
616 ULONG
/*64*/ fullsize
= /*(fileInfo.nFileSizeHigh << 32) +*/
617 fileInfo
.nFileSizeLow
;
620 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
621 sprintf(thisoutput
, "%u", fullsize
);
622 strcat(finaloutput
, thisoutput
);
625 /* 4. Handle 's' : Use short paths (File doesnt have to exist) */
626 if (memchr(firstModifier
, 's', modifierLen
) != NULL
) {
627 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
628 /* Dont flag as doneModifier - %~s on its own is processed later */
629 GetShortPathName(outputparam
, outputparam
, sizeof(outputparam
));
632 /* 5. Handle 'f' : Fully qualified path (File doesnt have to exist) */
633 /* Note this overrides d,p,n,x */
634 if (memchr(firstModifier
, 'f', modifierLen
) != NULL
) {
636 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
637 strcat(finaloutput
, fullfilename
);
642 char fname
[MAX_PATH
];
644 BOOL doneFileModifier
= FALSE
;
646 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
648 /* Split into components */
649 _splitpath(fullfilename
, drive
, dir
, fname
, ext
);
651 /* 5. Handle 'd' : Drive Letter */
652 if (memchr(firstModifier
, 'd', modifierLen
) != NULL
) {
653 strcat(finaloutput
, drive
);
655 doneFileModifier
= TRUE
;
658 /* 6. Handle 'p' : Path */
659 if (memchr(firstModifier
, 'p', modifierLen
) != NULL
) {
660 strcat(finaloutput
, dir
);
662 doneFileModifier
= TRUE
;
665 /* 7. Handle 'n' : Name */
666 if (memchr(firstModifier
, 'n', modifierLen
) != NULL
) {
667 strcat(finaloutput
, fname
);
669 doneFileModifier
= TRUE
;
672 /* 8. Handle 'x' : Ext */
673 if (memchr(firstModifier
, 'x', modifierLen
) != NULL
) {
674 strcat(finaloutput
, ext
);
676 doneFileModifier
= TRUE
;
679 /* If 's' but no other parameter, dump the whole thing */
680 if (!doneFileModifier
&&
681 memchr(firstModifier
, 's', modifierLen
) != NULL
) {
683 if (finaloutput
[0] != 0x00) strcat(finaloutput
, " ");
684 strcat(finaloutput
, outputparam
);
689 /* If No other modifier processed, just add in parameter */
690 if (!doneModifier
) strcpy(finaloutput
, outputparam
);
692 /* Finish by inserting the replacement into the string */
693 pos
= strdup (lastModifier
+1);
694 strcpy(*start
, finaloutput
);