/* * FOR.C - for internal batch command. * * * History: * * 16-Jul-1998 (Hans B Pufal) * Started. * * 16-Jul-1998 (John P Price) * Separated commands into individual files. * * 19-Jul-1998 (Hans B Pufal) * Implementation of FOR. * * 27-Jul-1998 (John P Price ) * Added config.h include. * * 20-Jan-1999 (Eric Kohl) * Unicode and redirection safe! * * 01-Sep-1999 (Eric Kohl) * Added help text. * * 23-Feb-2001 (Carl Nettelblad ) * Implemented preservation of echo flag. Some other for related * code in other files fixed, too. * * 28-Apr-2005 (Magnus Olsen ) * Remove all hardcoded strings in En.rc */ #include "precomp.h" /* Enable this define for "buggy" Windows' CMD FOR-command compatibility. * Currently, this enables the buggy behaviour of FOR /F token parsing. */ #define MSCMD_FOR_QUIRKS /* FOR is a special command, so this function is only used for showing help now */ INT cmd_for(LPTSTR param) { TRACE("cmd_for(\'%s\')\n", debugstr_aw(param)); if (!_tcsncmp(param, _T("/?"), 2)) { ConOutResPaging(TRUE, STRING_FOR_HELP1); return 0; } ParseErrorEx(param); return 1; } /* The stack of current FOR contexts. * NULL when no FOR command is active */ PFOR_CONTEXT fc = NULL; /* Get the next element of the FOR's list */ static BOOL GetNextElement(TCHAR **pStart, TCHAR **pEnd) { TCHAR *p = *pEnd; BOOL InQuotes = FALSE; while (_istspace(*p)) p++; if (!*p) return FALSE; *pStart = p; while (*p && (InQuotes || !_istspace(*p))) InQuotes ^= (*p++ == _T('"')); *pEnd = p; return TRUE; } /* Execute a single instance of a FOR command */ /* Just run the command (variable expansion is done in DoDelayedExpansion) */ #define RunInstance(Cmd) \ ExecuteCommandWithEcho((Cmd)->Subcommands) /* Check if this FOR should be terminated early */ #define Exiting(Cmd) \ /* Someone might have removed our context */ \ (bCtrlBreak || (fc != (Cmd)->For.Context)) /* Take also GOTO jumps into account */ #define ExitingOrGoto(Cmd) \ (Exiting(Cmd) || (bc && bc->current == NULL)) /* Read the contents of a text file into memory, * dynamically allocating enough space to hold it all */ static LPTSTR ReadFileContents(FILE *InputFile, TCHAR *Buffer) { SIZE_T Len = 0; SIZE_T AllocLen = 1000; LPTSTR Contents = cmd_alloc(AllocLen * sizeof(TCHAR)); if (!Contents) { WARN("Cannot allocate memory for Contents!\n"); return NULL; } while (_fgetts(Buffer, CMDLINE_LENGTH, InputFile)) { ULONG_PTR CharsRead = _tcslen(Buffer); while (Len + CharsRead >= AllocLen) { LPTSTR OldContents = Contents; Contents = cmd_realloc(Contents, (AllocLen *= 2) * sizeof(TCHAR)); if (!Contents) { WARN("Cannot reallocate memory for Contents!\n"); cmd_free(OldContents); return NULL; } } _tcscpy(&Contents[Len], Buffer); Len += CharsRead; } Contents[Len] = _T('\0'); return Contents; } /* FOR /F: Parse the contents of each file */ static INT ForF(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer) { LPTSTR Delims = _T(" \t"); PTCHAR DelimsEndPtr = NULL; TCHAR DelimsEndChr = _T('\0'); TCHAR Eol = _T(';'); INT SkipLines = 0; DWORD TokensMask = (1 << 1); #ifdef MSCMD_FOR_QUIRKS DWORD NumTokens = 1; DWORD RemainderVar = 0; #else DWORD NumTokens = 0; #endif TCHAR StringQuote = _T('"'); TCHAR CommandQuote = _T('\''); LPTSTR Variables[32]; PTCHAR Start, End; INT Ret = 0; if (Cmd->For.Params) { TCHAR Quote = 0; PTCHAR Param = Cmd->For.Params; if (*Param == _T('"') || *Param == _T('\'')) Quote = *Param++; while (*Param && *Param != Quote) { if (*Param <= _T(' ')) { Param++; } else if (_tcsnicmp(Param, _T("delims="), 7) == 0) { Param += 7; /* * delims=xxx: Specifies the list of characters that separate tokens. * This option does not cumulate: only the latest 'delims=' specification * is taken into account. */ Delims = Param; DelimsEndPtr = NULL; while (*Param && *Param != Quote) { if (*Param == _T(' ')) { PTCHAR FirstSpace = Param; Param += _tcsspn(Param, _T(" ")); /* Exclude trailing spaces if this is not the last parameter */ if (*Param && *Param != Quote) { /* Save where the delimiters specification string ends */ DelimsEndPtr = FirstSpace; } break; } Param++; } if (*Param == Quote) { /* Save where the delimiters specification string ends */ DelimsEndPtr = Param++; } } else if (_tcsnicmp(Param, _T("eol="), 4) == 0) { Param += 4; /* eol=c: Lines starting with this character (may be * preceded by delimiters) are skipped. */ Eol = *Param; if (Eol != _T('\0')) Param++; } else if (_tcsnicmp(Param, _T("skip="), 5) == 0) { /* skip=n: Number of lines to skip at the beginning of each file */ SkipLines = _tcstol(Param + 5, &Param, 0); if (SkipLines <= 0) goto error; } else if (_tcsnicmp(Param, _T("tokens="), 7) == 0) { #ifdef MSCMD_FOR_QUIRKS DWORD NumToksInSpec = 0; // Number of tokens in this specification. #endif Param += 7; /* * tokens=x,y,m-n: List of token numbers (must be between 1 and 31) * that will be assigned into variables. This option does not cumulate: * only the latest 'tokens=' specification is taken into account. * * NOTE: In MSCMD_FOR_QUIRKS mode, for Windows' CMD compatibility, * not all the tokens-state is reset. This leads to subtle bugs. */ TokensMask = 0; #ifdef MSCMD_FOR_QUIRKS NumToksInSpec = 0; // Windows' CMD compatibility: bug: the asterisk-token's position is not reset! // RemainderVar = 0; #else NumTokens = 0; #endif while (*Param && *Param != Quote && *Param != _T('*')) { INT First = _tcstol(Param, &Param, 0); INT Last = First; #ifdef MSCMD_FOR_QUIRKS if (First < 1) #else if ((First < 1) || (First > 31)) #endif goto error; if (*Param == _T('-')) { /* It's a range of tokens */ Last = _tcstol(Param + 1, &Param, 0); #ifdef MSCMD_FOR_QUIRKS /* Ignore the range if the endpoints are not in correct order */ if (Last < 1) #else if ((Last < First) || (Last > 31)) #endif goto error; } #ifdef MSCMD_FOR_QUIRKS /* Ignore the range if the endpoints are not in correct order */ if ((First <= Last) && (Last <= 31)) { #endif TokensMask |= (2 << Last) - (1 << First); #ifdef MSCMD_FOR_QUIRKS NumToksInSpec += (Last - First + 1); } #endif if (*Param != _T(',')) break; Param++; } /* With an asterisk at the end, an additional variable * will be created to hold the remainder of the line * (after the last specified token). */ if (*Param == _T('*')) { #ifdef MSCMD_FOR_QUIRKS RemainderVar = ++NumToksInSpec; #else ++NumTokens; #endif Param++; } #ifdef MSCMD_FOR_QUIRKS NumTokens = max(NumTokens, NumToksInSpec); #endif } else if (_tcsnicmp(Param, _T("useback"), 7) == 0) { Param += 7; /* usebackq: Use alternate quote characters */ StringQuote = _T('\''); CommandQuote = _T('`'); /* Can be written as either "useback" or "usebackq" */ if (_totlower(*Param) == _T('q')) Param++; } else { error: error_syntax(Param); return 1; } } } #ifdef MSCMD_FOR_QUIRKS /* Windows' CMD compatibility: use the wrongly evaluated number of tokens */ fc->varcount = NumTokens; /* Allocate a large enough variables array if needed */ if (NumTokens <= ARRAYSIZE(Variables)) { fc->values = Variables; } else { fc->values = cmd_alloc(fc->varcount * sizeof(*fc->values)); if (!fc->values) { error_out_of_memory(); return 1; } } #else /* Count how many variables will be set: one for each token, * plus maybe one for the remainder. */ fc->varcount = NumTokens; for (NumTokens = 1; NumTokens < 32; ++NumTokens) fc->varcount += (TokensMask >> NumTokens) & 1; fc->values = Variables; #endif if (*List == StringQuote || *List == CommandQuote) { /* Treat the entire "list" as one single element */ Start = List; End = &List[_tcslen(List)]; goto single_element; } /* Loop over each file */ End = List; while (!ExitingOrGoto(Cmd) && GetNextElement(&Start, &End)) { FILE* InputFile; LPTSTR FullInput, In, NextLine; INT Skip; single_element: if (*Start == StringQuote && End[-1] == StringQuote) { /* Input given directly as a string */ End[-1] = _T('\0'); FullInput = cmd_dup(Start + 1); } else if (*Start == CommandQuote && End[-1] == CommandQuote) { /* * Read input from a command. We let the CRT do the ANSI/UNICODE conversion. * NOTE: Should we do that, or instead read in binary mode and * do the conversion by ourselves, using *OUR* current codepage?? */ End[-1] = _T('\0'); InputFile = _tpopen(Start + 1, _T("r")); if (!InputFile) { error_bad_command(Start + 1); Ret = 1; goto Quit; } FullInput = ReadFileContents(InputFile, Buffer); _pclose(InputFile); } else { /* Read input from a file */ TCHAR Temp = *End; *End = _T('\0'); StripQuotes(Start); InputFile = _tfopen(Start, _T("r")); *End = Temp; if (!InputFile) { error_sfile_not_found(Start); Ret = 1; goto Quit; } FullInput = ReadFileContents(InputFile, Buffer); fclose(InputFile); } if (!FullInput) { error_out_of_memory(); Ret = 1; goto Quit; } /* Patch the delimiters string */ if (DelimsEndPtr) { DelimsEndChr = *DelimsEndPtr; *DelimsEndPtr = _T('\0'); } /* Loop over the input line by line */ for (In = FullInput, Skip = SkipLines; !ExitingOrGoto(Cmd) && (In != NULL); In = NextLine) { DWORD RemainingTokens = TokensMask; LPTSTR* CurVar = fc->values; ZeroMemory(fc->values, fc->varcount * sizeof(*fc->values)); #ifdef MSCMD_FOR_QUIRKS NumTokens = fc->varcount; #endif NextLine = _tcschr(In, _T('\n')); if (NextLine) *NextLine++ = _T('\0'); if (--Skip >= 0) continue; /* Ignore lines where the first token starts with the eol character */ In += _tcsspn(In, Delims); if (*In == Eol) continue; /* Loop as long as we have not reached the end of * the line, and that we have tokens available. * A maximum of 31 tokens will be enumerated. */ while (*In && ((RemainingTokens >>= 1) != 0)) { /* Save pointer to this token in a variable if requested */ if (RemainingTokens & 1) { #ifdef MSCMD_FOR_QUIRKS --NumTokens; #endif *CurVar++ = In; } /* Find end of token */ In += _tcscspn(In, Delims); /* NULL-terminate it and advance to next token */ if (*In) { *In++ = _T('\0'); In += _tcsspn(In, Delims); } } /* Save pointer to remainder of the line if we need to do so */ if (*In) #ifdef MSCMD_FOR_QUIRKS if (RemainderVar && (fc->varcount - NumTokens + 1 == RemainderVar)) #endif { /* NOTE: This sets fc->values[0] at least, if no tokens * were initialized so far, since CurVar is initialized * originally to point to fc->values. */ *CurVar = In; } /* Don't run unless we have at least one variable filled */ if (fc->values[0]) Ret = RunInstance(Cmd); } /* Restore the delimiters string */ if (DelimsEndPtr) *DelimsEndPtr = DelimsEndChr; cmd_free(FullInput); } Quit: #ifdef MSCMD_FOR_QUIRKS if (fc->values && (fc->values != Variables)) cmd_free(fc->values); #endif return Ret; } /* FOR /L: Do a numeric loop */ static INT ForLoop(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer) { enum { START, STEP, END }; INT params[3] = { 0, 0, 0 }; INT i; INT Ret = 0; TCHAR *Start, *End = List; for (i = 0; i < 3 && GetNextElement(&Start, &End); ++i) params[i] = _tcstol(Start, NULL, 0); i = params[START]; /* * Windows' CMD compatibility: * Contrary to the other FOR-loops, FOR /L does not check * whether a GOTO has been done, and will continue to loop. */ while (!Exiting(Cmd) && (params[STEP] >= 0 ? (i <= params[END]) : (i >= params[END]))) { _itot(i, Buffer, 10); Ret = RunInstance(Cmd); i += params[STEP]; } return Ret; } /* Process a FOR in one directory. Stored in Buffer (up to BufPos) is a * string which is prefixed to each element of the list. In a normal FOR * it will be empty, but in FOR /R it will be the directory name. */ static INT ForDir(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer, TCHAR *BufPos) { INT Ret = 0; TCHAR *Start, *End = List; while (!ExitingOrGoto(Cmd) && GetNextElement(&Start, &End)) { if (BufPos + (End - Start) > &Buffer[CMDLINE_LENGTH]) continue; memcpy(BufPos, Start, (End - Start) * sizeof(TCHAR)); BufPos[End - Start] = _T('\0'); if (_tcschr(BufPos, _T('?')) || _tcschr(BufPos, _T('*'))) { WIN32_FIND_DATA w32fd; HANDLE hFind; TCHAR *FilePart; StripQuotes(BufPos); hFind = FindFirstFile(Buffer, &w32fd); if (hFind == INVALID_HANDLE_VALUE) continue; FilePart = _tcsrchr(BufPos, _T('\\')); FilePart = FilePart ? FilePart + 1 : BufPos; do { if (w32fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) continue; if (!(w32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != !(Cmd->For.Switches & FOR_DIRS)) continue; if (_tcscmp(w32fd.cFileName, _T(".")) == 0 || _tcscmp(w32fd.cFileName, _T("..")) == 0) continue; _tcscpy(FilePart, w32fd.cFileName); Ret = RunInstance(Cmd); } while (!ExitingOrGoto(Cmd) && FindNextFile(hFind, &w32fd)); FindClose(hFind); } else { Ret = RunInstance(Cmd); } } return Ret; } /* FOR /R: Process a FOR in each directory of a tree, recursively */ static INT ForRecursive(PARSED_COMMAND *Cmd, LPTSTR List, TCHAR *Buffer, TCHAR *BufPos) { INT Ret = 0; HANDLE hFind; WIN32_FIND_DATA w32fd; if (BufPos[-1] != _T('\\')) { *BufPos++ = _T('\\'); *BufPos = _T('\0'); } Ret = ForDir(Cmd, List, Buffer, BufPos); /* NOTE (We don't apply Windows' CMD compatibility here): * Windows' CMD does not check whether a GOTO has been done, * and will continue to loop. */ if (ExitingOrGoto(Cmd)) return Ret; _tcscpy(BufPos, _T("*")); hFind = FindFirstFile(Buffer, &w32fd); if (hFind == INVALID_HANDLE_VALUE) return Ret; do { if (!(w32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue; if (_tcscmp(w32fd.cFileName, _T(".")) == 0 || _tcscmp(w32fd.cFileName, _T("..")) == 0) continue; Ret = ForRecursive(Cmd, List, Buffer, _stpcpy(BufPos, w32fd.cFileName)); /* NOTE (We don't apply Windows' CMD compatibility here): * Windows' CMD does not check whether a GOTO has been done, * and will continue to loop. */ } while (!ExitingOrGoto(Cmd) && FindNextFile(hFind, &w32fd)); FindClose(hFind); return Ret; } INT ExecuteFor(PARSED_COMMAND *Cmd) { INT Ret; LPTSTR List; PFOR_CONTEXT lpNew; TCHAR Buffer[CMDLINE_LENGTH]; /* Buffer to hold the variable value */ LPTSTR BufferPtr = Buffer; List = DoDelayedExpansion(Cmd->For.List); if (!List) return 1; /* Create our FOR context */ lpNew = cmd_alloc(sizeof(FOR_CONTEXT)); if (!lpNew) { WARN("Cannot allocate memory for lpNew!\n"); cmd_free(List); return 1; } lpNew->prev = fc; lpNew->firstvar = Cmd->For.Variable; lpNew->varcount = 1; lpNew->values = &BufferPtr; Cmd->For.Context = lpNew; fc = lpNew; /* Run the extended FOR syntax only if extensions are enabled */ if (bEnableExtensions) { if (Cmd->For.Switches & FOR_F) { Ret = ForF(Cmd, List, Buffer); } else if (Cmd->For.Switches & FOR_LOOP) { Ret = ForLoop(Cmd, List, Buffer); } else if (Cmd->For.Switches & FOR_RECURSIVE) { DWORD Len = GetFullPathName(Cmd->For.Params ? Cmd->For.Params : _T("."), MAX_PATH, Buffer, NULL); Ret = ForRecursive(Cmd, List, Buffer, &Buffer[Len]); } else { Ret = ForDir(Cmd, List, Buffer, Buffer); } } else { Ret = ForDir(Cmd, List, Buffer, Buffer); } /* Remove our context, unless someone already did that */ if (fc == lpNew) fc = lpNew->prev; cmd_free(lpNew); cmd_free(List); return Ret; } /* EOF */