cmd.exe: Support SHIFT /n option.
[wine.git] / programs / cmd / batch.c
blob0824c27ef8af9b2e4db143cdcca2f97145d5e629
1 /*
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
21 #include "wcmd.h"
23 extern int echo_mode;
24 extern char quals[MAX_PATH], param1[MAX_PATH], param2[MAX_PATH];
25 extern BATCH_CONTEXT *context;
26 extern DWORD errorlevel;
28 /****************************************************************************
29 * WCMD_batch
31 * Open and execute a batch file.
32 * On entry *command includes the complete command line beginning with the name
33 * of the batch file (if a CALL command was entered the CALL has been removed).
34 * *file is the name of the file, which might not exist and may not have the
35 * .BAT suffix on. Called is 1 for a CALL, 0 otherwise.
37 * We need to handle recursion correctly, since one batch program might call another.
38 * So parameters for this batch file are held in a BATCH_CONTEXT structure.
40 * To support call within the same batch program, another input parameter is
41 * a label to goto once opened.
44 void WCMD_batch (char *file, char *command, int called, char *startLabel, HANDLE pgmHandle) {
46 #define WCMD_BATCH_EXT_SIZE 5
48 HANDLE h = INVALID_HANDLE_VALUE;
49 char string[MAXSTRING];
50 char extension_batch[][WCMD_BATCH_EXT_SIZE] = {".bat",".cmd"};
51 char extension_exe[WCMD_BATCH_EXT_SIZE] = ".exe";
52 unsigned int i;
53 BATCH_CONTEXT *prev_context;
55 if (startLabel == NULL) {
56 for(i=0; (i<(sizeof(extension_batch)/WCMD_BATCH_EXT_SIZE)) &&
57 (h == INVALID_HANDLE_VALUE); i++) {
58 strcpy (string, file);
59 CharLower (string);
60 if (strstr (string, extension_batch[i]) == NULL) strcat (string, extension_batch[i]);
61 h = CreateFile (string, GENERIC_READ, FILE_SHARE_READ,
62 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
64 if (h == INVALID_HANDLE_VALUE) {
65 strcpy (string, file);
66 CharLower (string);
67 if (strstr (string, extension_exe) == NULL) strcat (string, extension_exe);
68 h = CreateFile (string, GENERIC_READ, FILE_SHARE_READ,
69 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
70 if (h != INVALID_HANDLE_VALUE) {
71 WCMD_run_program (command, 0);
72 } else {
73 SetLastError (ERROR_FILE_NOT_FOUND);
74 WCMD_print_error ();
76 return;
78 } else {
79 DuplicateHandle(GetCurrentProcess(), pgmHandle,
80 GetCurrentProcess(), &h,
81 0, FALSE, DUPLICATE_SAME_ACCESS);
85 * Create a context structure for this batch file.
88 prev_context = context;
89 context = (BATCH_CONTEXT *)LocalAlloc (LMEM_FIXED, sizeof (BATCH_CONTEXT));
90 context -> h = h;
91 context -> command = command;
92 memset(context -> shift_count, 0x00, sizeof(context -> shift_count));
93 context -> prev_context = prev_context;
94 context -> skip_rest = FALSE;
96 /* If processing a call :label, 'goto' the label in question */
97 if (startLabel) {
98 strcpy(param1, startLabel);
99 WCMD_goto();
103 * Work through the file line by line. Specific batch commands are processed here,
104 * the rest are handled by the main command processor.
107 while (context -> skip_rest == FALSE && WCMD_fgets (string, sizeof(string), h)) {
108 if (strlen(string) == MAXSTRING -1) {
109 WCMD_output_asis( "Line in Batch processing possibly truncated. Using:\n");
110 WCMD_output_asis( string);
111 WCMD_output_asis( "\n");
113 if (string[0] != ':') { /* Skip over labels */
114 WCMD_process_command (string);
117 CloseHandle (h);
120 * If invoked by a CALL, we return to the context of our caller. Otherwise return
121 * to the caller's caller.
124 LocalFree ((HANDLE)context);
125 if ((prev_context != NULL) && (!called)) {
126 CloseHandle (prev_context -> h);
127 context = prev_context -> prev_context;
128 LocalFree ((HANDLE)prev_context);
130 else {
131 context = prev_context;
135 /*******************************************************************
136 * WCMD_parameter - extract a parameter from a command line.
138 * Returns the 'n'th space-delimited parameter on the command line (zero-based).
139 * Parameter is in static storage overwritten on the next call.
140 * Parameters in quotes (and brackets) are handled.
141 * Also returns a pointer to the location of the parameter in the command line.
144 char *WCMD_parameter (char *s, int n, char **where) {
146 int i = 0;
147 static char param[MAX_PATH];
148 char *p;
150 if (where != NULL) *where = NULL;
151 p = param;
152 while (TRUE) {
153 switch (*s) {
154 case ' ':
155 s++;
156 break;
157 case '"':
158 if (where != NULL && i==n) *where = s;
159 s++;
160 while ((*s != '\0') && (*s != '"')) {
161 *p++ = *s++;
163 if (i == n) {
164 *p = '\0';
165 return param;
167 if (*s == '"') s++;
168 param[0] = '\0';
169 i++;
170 p = param;
171 break;
172 case '(':
173 if (where != NULL && i==n) *where = s;
174 s++;
175 while ((*s != '\0') && (*s != ')')) {
176 *p++ = *s++;
178 if (i == n) {
179 *p = '\0';
180 return param;
182 if (*s == ')') s++;
183 param[0] = '\0';
184 i++;
185 p = param;
186 break;
187 case '\0':
188 return param;
189 default:
190 /* Only return where if it is for the right parameter */
191 if (where != NULL && i==n) *where = s;
192 while ((*s != '\0') && (*s != ' ')) {
193 *p++ = *s++;
195 if (i == n) {
196 *p = '\0';
197 return param;
199 param[0] = '\0';
200 i++;
201 p = param;
206 /****************************************************************************
207 * WCMD_fgets
209 * Get one line from a batch file. We can't use the native f* functions because
210 * of the filename syntax differences between DOS and Unix. Also need to lose
211 * the LF (or CRLF) from the line.
214 char *WCMD_fgets (char *s, int n, HANDLE h) {
216 DWORD bytes;
217 BOOL status;
218 char *p;
220 p = s;
221 do {
222 status = ReadFile (h, s, 1, &bytes, NULL);
223 if ((status == 0) || ((bytes == 0) && (s == p))) return NULL;
224 if (*s == '\n') bytes = 0;
225 else if (*s != '\r') {
226 s++;
227 n--;
229 *s = '\0';
230 } while ((bytes == 1) && (n > 1));
231 return p;
234 /* WCMD_splitpath - copied from winefile as no obvious way to use it otherwise */
235 void WCMD_splitpath(const CHAR* path, CHAR* drv, CHAR* dir, CHAR* name, CHAR* ext)
237 const CHAR* end; /* end of processed string */
238 const CHAR* p; /* search pointer */
239 const CHAR* s; /* copy pointer */
241 /* extract drive name */
242 if (path[0] && path[1]==':') {
243 if (drv) {
244 *drv++ = *path++;
245 *drv++ = *path++;
246 *drv = '\0';
248 } else if (drv)
249 *drv = '\0';
251 /* search for end of string or stream separator */
252 for(end=path; *end && *end!=':'; )
253 end++;
255 /* search for begin of file extension */
256 for(p=end; p>path && *--p!='\\' && *p!='/'; )
257 if (*p == '.') {
258 end = p;
259 break;
262 if (ext)
263 for(s=end; (*ext=*s++); )
264 ext++;
266 /* search for end of directory name */
267 for(p=end; p>path; )
268 if (*--p=='\\' || *p=='/') {
269 p++;
270 break;
273 if (name) {
274 for(s=p; s<end; )
275 *name++ = *s++;
277 *name = '\0';
280 if (dir) {
281 for(s=path; s<p; )
282 *dir++ = *s++;
284 *dir = '\0';
288 /****************************************************************************
289 * WCMD_HandleTildaModifiers
291 * Handle the ~ modifiers when expanding %0-9 or (%a-z in for command)
292 * %~xxxxxV (V=0-9 or A-Z)
293 * Where xxxx is any combination of:
294 * ~ - Removes quotes
295 * f - Fully qualified path (assumes current dir if not drive\dir)
296 * d - drive letter
297 * p - path
298 * n - filename
299 * x - file extension
300 * s - path with shortnames
301 * a - attributes
302 * t - date/time
303 * z - size
304 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
305 * qualified path
307 * To work out the length of the modifier:
309 * Note: In the case of %0-9 knowing the end of the modifier is easy,
310 * but in a for loop, the for end character may also be a modifier
311 * eg. for %a in (c:\a.a) do echo XXX
312 * where XXX = %~a (just ~)
313 * %~aa (~ and attributes)
314 * %~aaxa (~, attributes and extension)
315 * BUT %~aax (~ and attributes followed by 'x')
317 * Hence search forwards until find an invalid modifier, and then
318 * backwards until find for variable or 0-9
320 void WCMD_HandleTildaModifiers(char **start, char *forVariable) {
322 #define NUMMODIFIERS 11
323 const char validmodifiers[NUMMODIFIERS] = {
324 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
327 WIN32_FILE_ATTRIBUTE_DATA fileInfo;
328 char outputparam[MAX_PATH];
329 char finaloutput[MAX_PATH];
330 char fullfilename[MAX_PATH];
331 char thisoutput[MAX_PATH];
332 char *pos = *start+1;
333 char *firstModifier = pos;
334 char *lastModifier = NULL;
335 int modifierLen = 0;
336 BOOL finished = FALSE;
337 int i = 0;
338 BOOL exists = TRUE;
339 BOOL skipFileParsing = FALSE;
340 BOOL doneModifier = FALSE;
342 /* Search forwards until find invalid character modifier */
343 while (!finished) {
345 /* Work on the previous character */
346 if (lastModifier != NULL) {
348 for (i=0; i<NUMMODIFIERS; i++) {
349 if (validmodifiers[i] == *lastModifier) {
351 /* Special case '$' to skip until : found */
352 if (*lastModifier == '$') {
353 while (*pos != ':' && *pos) pos++;
354 if (*pos == 0x00) return; /* Invalid syntax */
355 pos++; /* Skip ':' */
357 break;
361 if (i==NUMMODIFIERS) {
362 finished = TRUE;
366 /* Save this one away */
367 if (!finished) {
368 lastModifier = pos;
369 pos++;
373 /* Now make sure the position we stopped at is a valid parameter */
374 if (!(*lastModifier >= '0' || *lastModifier <= '9') &&
375 (forVariable != NULL) &&
376 (toupper(*lastModifier) != toupper(*forVariable))) {
378 /* Its not... Step backwards until it matches or we get to the start */
379 while (toupper(*lastModifier) != toupper(*forVariable) &&
380 lastModifier > firstModifier) {
381 lastModifier--;
383 if (lastModifier == firstModifier) return; /* Invalid syntax */
386 /* Extract the parameter to play with */
387 if ((*lastModifier >= '0' && *lastModifier <= '9')) {
388 strcpy(outputparam, WCMD_parameter (context -> command,
389 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
390 } else {
391 /* FIXME: Retrieve 'for' variable %c\n", *lastModifier); */
392 /* Need to get 'for' loop variable into outputparam */
393 return;
396 /* So now, firstModifier points to beginning of modifiers, lastModifier
397 points to the variable just after the modifiers. Process modifiers
398 in a specific order, remembering there could be duplicates */
399 modifierLen = lastModifier - firstModifier;
400 finaloutput[0] = 0x00;
402 /* Useful for debugging purposes: */
403 /*printf("Modifier string '%*.*s' and variable is %c\n Param starts as '%s'\n",
404 (modifierLen), (modifierLen), firstModifier, *lastModifier,
405 outputparam);*/
407 /* 1. Handle '~' : Strip surrounding quotes */
408 if (outputparam[0]=='"' &&
409 memchr(firstModifier, '~', modifierLen) != NULL) {
410 int len = strlen(outputparam);
411 if (outputparam[len-1] == '"') {
412 outputparam[len-1]=0x00;
413 len = len - 1;
415 memmove(outputparam, &outputparam[1], len-1);
418 /* 2. Handle the special case of a $ */
419 if (memchr(firstModifier, '$', modifierLen) != NULL) {
420 /* Special Case: Search envar specified in $[envvar] for outputparam
421 Note both $ and : are guaranteed otherwise check above would fail */
422 char *start = strchr(firstModifier, '$') + 1;
423 char *end = strchr(firstModifier, ':');
424 char env[MAX_PATH];
425 char fullpath[MAX_PATH];
427 /* Extract the env var */
428 strncpy(env, start, (end-start));
429 env[(end-start)] = 0x00;
431 /* If env var not found, return emptry string */
432 if ((GetEnvironmentVariable(env, fullpath, MAX_PATH) == 0) ||
433 (SearchPath(fullpath, outputparam, NULL,
434 MAX_PATH, outputparam, NULL) == 0)) {
435 finaloutput[0] = 0x00;
436 outputparam[0] = 0x00;
437 skipFileParsing = TRUE;
441 /* After this, we need full information on the file,
442 which is valid not to exist. */
443 if (!skipFileParsing) {
444 if (GetFullPathName(outputparam, MAX_PATH, fullfilename, NULL) == 0)
445 return;
447 exists = GetFileAttributesExA(fullfilename, GetFileExInfoStandard,
448 &fileInfo);
450 /* 2. Handle 'a' : Output attributes */
451 if (exists &&
452 memchr(firstModifier, 'a', modifierLen) != NULL) {
454 doneModifier = TRUE;
455 strcpy(thisoutput, "---------");
456 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
457 thisoutput[0]='d';
458 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
459 thisoutput[1]='r';
460 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
461 thisoutput[2]='a';
462 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
463 thisoutput[3]='h';
464 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
465 thisoutput[4]='s';
466 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
467 thisoutput[5]='c';
468 /* FIXME: What are 6 and 7? */
469 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
470 thisoutput[8]='l';
471 strcat(finaloutput, thisoutput);
474 /* 3. Handle 't' : Date+time */
475 if (exists &&
476 memchr(firstModifier, 't', modifierLen) != NULL) {
478 SYSTEMTIME systime;
479 int datelen;
481 doneModifier = TRUE;
482 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
484 /* Format the time */
485 FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
486 GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
487 NULL, thisoutput, MAX_PATH);
488 strcat(thisoutput, " ");
489 datelen = strlen(thisoutput);
490 GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
491 NULL, (thisoutput+datelen), MAX_PATH-datelen);
492 strcat(finaloutput, thisoutput);
495 /* 4. Handle 'z' : File length */
496 if (exists &&
497 memchr(firstModifier, 'z', modifierLen) != NULL) {
498 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
499 ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
500 fileInfo.nFileSizeLow;
502 doneModifier = TRUE;
503 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
504 sprintf(thisoutput, "%u", fullsize);
505 strcat(finaloutput, thisoutput);
508 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
509 if (memchr(firstModifier, 's', modifierLen) != NULL) {
510 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
511 /* Don't flag as doneModifier - %~s on its own is processed later */
512 GetShortPathName(outputparam, outputparam, sizeof(outputparam));
515 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
516 /* Note this overrides d,p,n,x */
517 if (memchr(firstModifier, 'f', modifierLen) != NULL) {
518 doneModifier = TRUE;
519 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
520 strcat(finaloutput, fullfilename);
521 } else {
523 char drive[10];
524 char dir[MAX_PATH];
525 char fname[MAX_PATH];
526 char ext[MAX_PATH];
527 BOOL doneFileModifier = FALSE;
529 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
531 /* Split into components */
532 WCMD_splitpath(fullfilename, drive, dir, fname, ext);
534 /* 5. Handle 'd' : Drive Letter */
535 if (memchr(firstModifier, 'd', modifierLen) != NULL) {
536 strcat(finaloutput, drive);
537 doneModifier = TRUE;
538 doneFileModifier = TRUE;
541 /* 6. Handle 'p' : Path */
542 if (memchr(firstModifier, 'p', modifierLen) != NULL) {
543 strcat(finaloutput, dir);
544 doneModifier = TRUE;
545 doneFileModifier = TRUE;
548 /* 7. Handle 'n' : Name */
549 if (memchr(firstModifier, 'n', modifierLen) != NULL) {
550 strcat(finaloutput, fname);
551 doneModifier = TRUE;
552 doneFileModifier = TRUE;
555 /* 8. Handle 'x' : Ext */
556 if (memchr(firstModifier, 'x', modifierLen) != NULL) {
557 strcat(finaloutput, ext);
558 doneModifier = TRUE;
559 doneFileModifier = TRUE;
562 /* If 's' but no other parameter, dump the whole thing */
563 if (!doneFileModifier &&
564 memchr(firstModifier, 's', modifierLen) != NULL) {
565 doneModifier = TRUE;
566 if (finaloutput[0] != 0x00) strcat(finaloutput, " ");
567 strcat(finaloutput, outputparam);
572 /* If No other modifier processed, just add in parameter */
573 if (!doneModifier) strcpy(finaloutput, outputparam);
575 /* Finish by inserting the replacement into the string */
576 pos = strdup (lastModifier+1);
577 strcpy(*start, finaloutput);
578 strcat(*start, pos);
579 free(pos);
582 /*******************************************************************
583 * WCMD_call - processes a batch call statement
585 * If there is a leading ':', calls within this batch program
586 * otherwise launches another program.
588 void WCMD_call (char *command) {
590 /* Run other program if no leading ':' */
591 if (*command != ':') {
592 WCMD_run_program(command, 1);
593 } else {
595 char gotoLabel[MAX_PATH];
597 strcpy(gotoLabel, param1);
599 if (context) {
601 LARGE_INTEGER li;
603 /* Save the current file position, call the same file,
604 restore position */
605 li.QuadPart = 0;
606 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
607 &li.u.HighPart, FILE_CURRENT);
609 WCMD_batch (param1, command, 1, gotoLabel, context->h);
611 SetFilePointer(context -> h, li.u.LowPart,
612 &li.u.HighPart, FILE_BEGIN);
613 } else {
614 printf("Cannot call batch label outside of a batch script\n");