kernelbase: Use IOCTL_CONDRV_GET_OUTPUT_INFO in GetConsoleCursorInfo.
[wine.git] / programs / cmd / batch.c
blob3aa40874aea0b775e979586ce628d3ddbacf3b79
1 /*
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
22 #include "wcmd.h"
23 #include "wine/debug.h"
25 extern struct env_stack *saved_environment;
27 WINE_DEFAULT_DEBUG_CHANNEL(cmd);
29 /****************************************************************************
30 * WCMD_batch
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);
55 WCMD_print_error ();
56 return;
58 } else {
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));
70 context -> h = h;
71 context->batchfileW = heap_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 */
78 if (startLabel) {
79 lstrcpyW(param1, startLabel);
80 WCMD_goto(NULL);
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))
91 break;
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);
97 toExecute = NULL;
99 CloseHandle (h);
102 * If there are outstanding setlocal's to the current context, unwind them.
104 while (saved_environment && saved_environment->batchhandle == context->h) {
105 WCMD_endlocal();
109 * If invoked by a CALL, we return to the context of our caller. Otherwise return
110 * to the caller's caller.
113 heap_free(context->batchfileW);
114 LocalFree (context);
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
129 * PARAMS
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
140 * RETURNS
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.
144 * *start == NULL
146 * NOTES
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)
155 int curParamNb = 0;
156 static WCHAR param[MAXSTRING];
157 WCHAR *p = s, *begin;
159 if (start != NULL) *start = NULL;
160 param[0] = '\0';
162 while (TRUE) {
164 /* Absorb repeated word delimiters until we get to the next token (or the end!) */
165 while (*p && (wcschr(delims, *p) != NULL))
166 p++;
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. */
174 begin = p;
176 /* Loop character by character, but just need to special case quotes */
177 while (*p) {
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 */
188 if (*p == '"') {
189 p++;
190 while (*p && *p != '"') p++;
193 /* Now skip the character / quote */
194 if (*p) p++;
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) */
200 if (raw) {
201 memcpy(param, begin, (p - begin) * sizeof(WCHAR));
202 param[p-begin] = '\0';
203 } else {
204 int i=0;
205 while (begin < p) {
206 if (*begin != '"') param[i++] = *begin;
207 begin++;
209 param[i] = '\0';
211 return param;
213 curParamNb++;
217 /*******************************************************************
218 * WCMD_parameter
220 * Extracts a delimited parameter from an input string, using a
221 * default set of delimiter characters. For parameters, see the main
222 * function above.
224 WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **start, BOOL raw,
225 BOOL wholecmdline)
227 return WCMD_parameter_with_delims (s, n, start, raw, wholecmdline, L" \t,=;");
230 /****************************************************************************
231 * WCMD_fgets
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
238 * Returns:
239 * buf on success
240 * NULL on error or EOF
243 WCHAR *WCMD_fgets(WCHAR *buf, DWORD noChars, HANDLE h)
245 DWORD charsRead;
246 BOOL status;
247 DWORD i;
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 (!WCMD_is_console_handle(h)) {
253 LARGE_INTEGER filepos;
254 char *bufA;
255 UINT cp;
256 const char *p;
258 cp = GetConsoleCP();
259 bufA = heap_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) {
267 heap_free(bufA);
268 return NULL;
271 /* Find first EOL */
272 for (p = bufA; p < (bufA + charsRead); p = CharNextExA(cp, p, 0)) {
273 if (*p == '\n' || *p == '\r')
274 break;
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);
282 heap_free(bufA);
284 else {
285 status = WCMD_ReadFile(h, buf, noChars, &charsRead);
286 if (!status || charsRead == 0) return NULL;
288 /* Find first EOL */
289 for (i = 0; i < charsRead; i++) {
290 if (buf[i] == '\n' || buf[i] == '\r')
291 break;
295 /* Truncate at EOL (or end of buffer) */
296 if (i == noChars)
297 i--;
299 buf[i] = '\0';
301 return buf;
304 /****************************************************************************
305 * WCMD_HandleTildeModifiers
307 * Handle the ~ modifiers when expanding %0-9 or (%a-z/A-Z in for command)
308 * %~xxxxxV (V=0-9 or A-Z, a-z)
309 * Where xxxx is any combination of:
310 * ~ - Removes quotes
311 * f - Fully qualified path (assumes current dir if not drive\dir)
312 * d - drive letter
313 * p - path
314 * n - filename
315 * x - file extension
316 * s - path with shortnames
317 * a - attributes
318 * t - date/time
319 * z - size
320 * $ENVVAR: - Searches ENVVAR for (contents of V) and expands to fully
321 * qualified path
323 * To work out the length of the modifier:
325 * Note: In the case of %0-9 knowing the end of the modifier is easy,
326 * but in a for loop, the for end WCHARacter may also be a modifier
327 * eg. for %a in (c:\a.a) do echo XXX
328 * where XXX = %~a (just ~)
329 * %~aa (~ and attributes)
330 * %~aaxa (~, attributes and extension)
331 * BUT %~aax (~ and attributes followed by 'x')
333 * Hence search forwards until find an invalid modifier, and then
334 * backwards until find for variable or 0-9
336 void WCMD_HandleTildeModifiers(WCHAR **start, BOOL atExecute)
339 #define NUMMODIFIERS 11
340 static const WCHAR validmodifiers[NUMMODIFIERS] = {
341 '~', 'f', 'd', 'p', 'n', 'x', 's', 'a', 't', 'z', '$'
344 WIN32_FILE_ATTRIBUTE_DATA fileInfo;
345 WCHAR outputparam[MAX_PATH];
346 WCHAR finaloutput[MAX_PATH];
347 WCHAR fullfilename[MAX_PATH];
348 WCHAR thisoutput[MAX_PATH];
349 WCHAR *filepart = NULL;
350 WCHAR *pos = *start+1;
351 WCHAR *firstModifier = pos;
352 WCHAR *lastModifier = NULL;
353 int modifierLen = 0;
354 BOOL finished = FALSE;
355 int i = 0;
356 BOOL exists = TRUE;
357 BOOL skipFileParsing = FALSE;
358 BOOL doneModifier = FALSE;
360 /* Search forwards until find invalid character modifier */
361 while (!finished) {
363 /* Work on the previous character */
364 if (lastModifier != NULL) {
366 for (i=0; i<NUMMODIFIERS; i++) {
367 if (validmodifiers[i] == *lastModifier) {
369 /* Special case '$' to skip until : found */
370 if (*lastModifier == '$') {
371 while (*pos != ':' && *pos) pos++;
372 if (*pos == 0x00) return; /* Invalid syntax */
373 pos++; /* Skip ':' */
375 break;
379 if (i==NUMMODIFIERS) {
380 finished = TRUE;
384 /* Save this one away */
385 if (!finished) {
386 lastModifier = pos;
387 pos++;
391 while (lastModifier > firstModifier) {
392 WINE_TRACE("Looking backwards for parameter id: %s\n",
393 wine_dbgstr_w(lastModifier));
395 if (!atExecute && context && (*lastModifier >= '0' && *lastModifier <= '9')) {
396 /* Its a valid parameter identifier - OK */
397 break;
399 } else {
400 int foridx = FOR_VAR_IDX(*lastModifier);
401 /* Its a valid parameter identifier - OK */
402 if ((foridx >= 0) && (forloopcontext.variable[foridx] != NULL)) break;
404 /* Its not a valid parameter identifier - step backwards */
405 lastModifier--;
408 if (lastModifier == firstModifier) return; /* Invalid syntax */
410 /* So now, firstModifier points to beginning of modifiers, lastModifier
411 points to the variable just after the modifiers. Process modifiers
412 in a specific order, remembering there could be duplicates */
413 modifierLen = lastModifier - firstModifier;
414 finaloutput[0] = 0x00;
416 /* Extract the parameter to play with
417 Special case param 0 - With %~0 you get the batch label which was called
418 whereas if you start applying other modifiers to it, you get the filename
419 the batch label is in */
420 if (*lastModifier == '0' && modifierLen > 1) {
421 lstrcpyW(outputparam, context->batchfileW);
422 } else if ((*lastModifier >= '0' && *lastModifier <= '9')) {
423 lstrcpyW(outputparam,
424 WCMD_parameter (context -> command,
425 *lastModifier-'0' + context -> shift_count[*lastModifier-'0'],
426 NULL, FALSE, TRUE));
427 } else {
428 int foridx = FOR_VAR_IDX(*lastModifier);
429 lstrcpyW(outputparam, forloopcontext.variable[foridx]);
432 /* 1. Handle '~' : Strip surrounding quotes */
433 if (outputparam[0]=='"' &&
434 wmemchr(firstModifier, '~', modifierLen) != NULL) {
435 int len = lstrlenW(outputparam);
436 if (outputparam[len-1] == '"') {
437 outputparam[len-1]=0x00;
438 len = len - 1;
440 memmove(outputparam, &outputparam[1], (len * sizeof(WCHAR))-1);
443 /* 2. Handle the special case of a $ */
444 if (wmemchr(firstModifier, '$', modifierLen) != NULL) {
445 /* Special Case: Search envar specified in $[envvar] for outputparam
446 Note both $ and : are guaranteed otherwise check above would fail */
447 WCHAR *begin = wcschr(firstModifier, '$') + 1;
448 WCHAR *end = wcschr(firstModifier, ':');
449 WCHAR env[MAX_PATH];
450 DWORD size;
452 /* Extract the env var */
453 memcpy(env, begin, (end-begin) * sizeof(WCHAR));
454 env[(end-begin)] = 0x00;
456 size = GetEnvironmentVariableW(env, NULL, 0);
457 if (size > 0) {
458 WCHAR *fullpath = heap_xalloc(size * sizeof(WCHAR));
459 if (!fullpath || (GetEnvironmentVariableW(env, fullpath, size) == 0) ||
460 (SearchPathW(fullpath, outputparam, NULL, MAX_PATH, outputparam, NULL) == 0))
461 size = 0;
462 heap_free(fullpath);
465 if (!size) {
466 /* If env var not found, return empty string */
467 finaloutput[0] = 0x00;
468 outputparam[0] = 0x00;
469 skipFileParsing = TRUE;
473 /* After this, we need full information on the file,
474 which is valid not to exist. */
475 if (!skipFileParsing) {
476 if (GetFullPathNameW(outputparam, MAX_PATH, fullfilename, &filepart) == 0) {
477 exists = FALSE;
478 fullfilename[0] = 0x00;
479 } else {
480 exists = GetFileAttributesExW(fullfilename, GetFileExInfoStandard,
481 &fileInfo);
484 /* 2. Handle 'a' : Output attributes (File doesn't have to exist) */
485 if (wmemchr(firstModifier, 'a', modifierLen) != NULL) {
487 doneModifier = TRUE;
489 if (exists) {
490 lstrcpyW(thisoutput, L"---------");
491 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
492 thisoutput[0]='d';
493 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
494 thisoutput[1]='r';
495 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE)
496 thisoutput[2]='a';
497 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
498 thisoutput[3]='h';
499 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
500 thisoutput[4]='s';
501 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED)
502 thisoutput[5]='c';
503 /* FIXME: What are 6 and 7? */
504 if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
505 thisoutput[8]='l';
506 lstrcatW(finaloutput, thisoutput);
510 /* 3. Handle 't' : Date+time (File doesn't have to exist) */
511 if (wmemchr(firstModifier, 't', modifierLen) != NULL) {
513 SYSTEMTIME systime;
514 int datelen;
516 doneModifier = TRUE;
518 if (exists) {
519 if (finaloutput[0] != 0x00) lstrcatW(finaloutput, spaceW);
521 /* Format the time */
522 FileTimeToSystemTime(&fileInfo.ftLastWriteTime, &systime);
523 GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &systime,
524 NULL, thisoutput, MAX_PATH);
525 lstrcatW(thisoutput, spaceW);
526 datelen = lstrlenW(thisoutput);
527 GetTimeFormatW(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &systime,
528 NULL, (thisoutput+datelen), MAX_PATH-datelen);
529 lstrcatW(finaloutput, thisoutput);
533 /* 4. Handle 'z' : File length (File doesn't have to exist) */
534 if (wmemchr(firstModifier, 'z', modifierLen) != NULL) {
535 /* FIXME: Output full 64 bit size (sprintf does not support I64 here) */
536 ULONG/*64*/ fullsize = /*(fileInfo.nFileSizeHigh << 32) +*/
537 fileInfo.nFileSizeLow;
539 doneModifier = TRUE;
540 if (exists) {
541 if (finaloutput[0] != 0x00) lstrcatW(finaloutput, spaceW);
542 wsprintfW(thisoutput, L"%u", fullsize);
543 lstrcatW(finaloutput, thisoutput);
547 /* 4. Handle 's' : Use short paths (File doesn't have to exist) */
548 if (wmemchr(firstModifier, 's', modifierLen) != NULL) {
549 if (finaloutput[0] != 0x00) lstrcatW(finaloutput, spaceW);
551 /* Convert fullfilename's path to a short path - Save filename away as
552 only path is valid, name may not exist which causes GetShortPathName
553 to fail if it is provided */
554 if (filepart) {
555 lstrcpyW(thisoutput, filepart);
556 *filepart = 0x00;
557 GetShortPathNameW(fullfilename, fullfilename, ARRAY_SIZE(fullfilename));
558 lstrcatW(fullfilename, thisoutput);
562 /* 5. Handle 'f' : Fully qualified path (File doesn't have to exist) */
563 /* Note this overrides d,p,n,x */
564 if (wmemchr(firstModifier, 'f', modifierLen) != NULL) {
565 doneModifier = TRUE;
566 if (finaloutput[0] != 0x00) lstrcatW(finaloutput, spaceW);
567 lstrcatW(finaloutput, fullfilename);
568 } else {
570 WCHAR drive[10];
571 WCHAR dir[MAX_PATH];
572 WCHAR fname[MAX_PATH];
573 WCHAR ext[MAX_PATH];
574 BOOL doneFileModifier = FALSE;
575 BOOL addSpace = (finaloutput[0] != 0x00);
577 /* Split into components */
578 _wsplitpath(fullfilename, drive, dir, fname, ext);
580 /* 5. Handle 'd' : Drive Letter */
581 if (wmemchr(firstModifier, 'd', modifierLen) != NULL) {
582 if (addSpace) {
583 lstrcatW(finaloutput, spaceW);
584 addSpace = FALSE;
587 lstrcatW(finaloutput, drive);
588 doneModifier = TRUE;
589 doneFileModifier = TRUE;
592 /* 6. Handle 'p' : Path */
593 if (wmemchr(firstModifier, 'p', modifierLen) != NULL) {
594 if (addSpace) {
595 lstrcatW(finaloutput, spaceW);
596 addSpace = FALSE;
599 lstrcatW(finaloutput, dir);
600 doneModifier = TRUE;
601 doneFileModifier = TRUE;
604 /* 7. Handle 'n' : Name */
605 if (wmemchr(firstModifier, 'n', modifierLen) != NULL) {
606 if (addSpace) {
607 lstrcatW(finaloutput, spaceW);
608 addSpace = FALSE;
611 lstrcatW(finaloutput, fname);
612 doneModifier = TRUE;
613 doneFileModifier = TRUE;
616 /* 8. Handle 'x' : Ext */
617 if (wmemchr(firstModifier, 'x', modifierLen) != NULL) {
618 if (addSpace) {
619 lstrcatW(finaloutput, spaceW);
620 addSpace = FALSE;
623 lstrcatW(finaloutput, ext);
624 doneModifier = TRUE;
625 doneFileModifier = TRUE;
628 /* If 's' but no other parameter, dump the whole thing */
629 if (!doneFileModifier &&
630 wmemchr(firstModifier, 's', modifierLen) != NULL) {
631 doneModifier = TRUE;
632 if (finaloutput[0] != 0x00) lstrcatW(finaloutput, spaceW);
633 lstrcatW(finaloutput, fullfilename);
638 /* If No other modifier processed, just add in parameter */
639 if (!doneModifier) lstrcpyW(finaloutput, outputparam);
641 /* Finish by inserting the replacement into the string */
642 WCMD_strsubstW(*start, lastModifier+1, finaloutput, -1);
645 /*******************************************************************
646 * WCMD_call - processes a batch call statement
648 * If there is a leading ':', calls within this batch program
649 * otherwise launches another program.
651 void WCMD_call (WCHAR *command) {
653 /* Run other program if no leading ':' */
654 if (*command != ':') {
655 WCMD_run_program(command, TRUE);
656 /* If the thing we try to run does not exist, call returns 1 */
657 if (errorlevel) errorlevel=1;
658 } else {
660 WCHAR gotoLabel[MAX_PATH];
662 lstrcpyW(gotoLabel, param1);
664 if (context) {
666 LARGE_INTEGER li;
667 FOR_CONTEXT oldcontext;
669 /* Save the for variable context, then start with an empty context
670 as for loop variables do not survive a call */
671 oldcontext = forloopcontext;
672 memset(&forloopcontext, 0, sizeof(forloopcontext));
674 /* Save the current file position, call the same file,
675 restore position */
676 li.QuadPart = 0;
677 li.u.LowPart = SetFilePointer(context -> h, li.u.LowPart,
678 &li.u.HighPart, FILE_CURRENT);
679 WCMD_batch (context->batchfileW, command, TRUE, gotoLabel, context->h);
680 SetFilePointer(context -> h, li.u.LowPart,
681 &li.u.HighPart, FILE_BEGIN);
683 /* Restore the for loop context */
684 forloopcontext = oldcontext;
685 } else {
686 WCMD_output_asis_stderr(WCMD_LoadMessage(WCMD_CALLINSCRIPT));