[PATCH 2/7] CMD.EXE: Expand for variables at last with tilda modifications

Jason Edmeades jason.edmeades at googlemail.com
Tue Sep 11 15:43:03 CDT 2007


Note: This patch finally reworks the parameter expansion (and probably
puts in place how to handle delayed expansion at a later date if needed).
It was needed to expand for variables correctly and avoid the dodgy hacks
we had up until now to tollerate % and %% on for commands.

Basically in the command shell, a line read from a file will be expanded
then (including %1, %var%) and %% -> %. Then when the command is executed
any for variable is then replaced.

This is a large change in terms of the command line but finally gets me
towards where I think we should be heading. I've run as many tests as I
can to ensure this does not regress anything, but I could not guarantee it.
All I can say is if anything breaks I'll try to fix it (and the code should
be easier to fix now!)
---
 programs/cmd/batch.c    |   34 ++++---
 programs/cmd/builtins.c |   36 +--------
 programs/cmd/wcmd.h     |    3 +-
 programs/cmd/wcmdmain.c |  220 +++++++++++++++++++++++++++++-----------------
 4 files changed, 161 insertions(+), 132 deletions(-)

diff --git a/programs/cmd/batch.c b/programs/cmd/batch.c
index 8faa297..1e60848 100644
--- a/programs/cmd/batch.c
+++ b/programs/cmd/batch.c
@@ -19,6 +19,9 @@
  */
 
 #include "wcmd.h"
+#include "wine/debug.h"
+
+WINE_DEFAULT_DEBUG_CHANNEL(cmd);
 
 extern int echo_mode;
 extern WCHAR quals[MAX_PATH], param1[MAX_PATH], param2[MAX_PATH];
@@ -301,7 +304,7 @@ void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHA
  *  Hence search forwards until find an invalid modifier, and then
  *  backwards until find for variable or 0-9
  */
-void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable) {
+void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable, WCHAR *forValue, BOOL justFors) {
 
 #define NUMMODIFIERS 11
   static const WCHAR validmodifiers[NUMMODIFIERS] = {
@@ -324,10 +327,10 @@ void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable) {
   BOOL  skipFileParsing = FALSE;
   BOOL  doneModifier    = FALSE;
 
-  /* Search forwards until find invalid WCHARacter modifier */
+  /* Search forwards until find invalid character modifier */
   while (!finished) {
 
-    /* Work on the previous WCHARacter */
+    /* Work on the previous character */
     if (lastModifier != NULL) {
 
       for (i=0; i<NUMMODIFIERS; i++) {
@@ -355,27 +358,30 @@ void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable) {
     }
   }
 
-  /* Now make sure the position we stopped at is a valid parameter */
-  if (!(*lastModifier >= '0' || *lastModifier <= '9') &&
-      (forVariable != NULL) &&
-      (toupperW(*lastModifier) != toupperW(*forVariable)))  {
+  while (lastModifier > firstModifier) {
+    WINE_TRACE("Looking backwards for parameter id: %s / %s\n",
+               wine_dbgstr_w(lastModifier), wine_dbgstr_w(forVariable));
+
+    if (!justFors && context && (*lastModifier >= '0' || *lastModifier <= '9')) {
+      /* Its a valid parameter identifier - OK */
+      break;
 
-    /* Its not... Step backwards until it matches or we get to the start */
-    while (toupperW(*lastModifier) != toupperW(*forVariable) &&
-          lastModifier > firstModifier) {
+    } else if (forVariable && *lastModifier == *(forVariable+1)) {
+      /* Its a valid parameter identifier - OK */
+      break;
+
+    } else {
       lastModifier--;
     }
-    if (lastModifier == firstModifier) return; /* Invalid syntax */
   }
+  if (lastModifier == firstModifier) return; /* Invalid syntax */
 
   /* Extract the parameter to play with */
   if ((*lastModifier >= '0' && *lastModifier <= '9')) {
     strcpyW(outputparam, WCMD_parameter (context -> command,
                  *lastModifier-'0' + context -> shift_count[*lastModifier-'0'], NULL));
   } else {
-    /* FIXME: Retrieve 'for' variable %c\n", *lastModifier); */
-    /* Need to get 'for' loop variable into outputparam      */
-    return;
+    strcpyW(outputparam, forValue);
   }
 
   /* So now, firstModifier points to beginning of modifiers, lastModifier
diff --git a/programs/cmd/builtins.c b/programs/cmd/builtins.c
index 027f2bd..8fbf292 100644
--- a/programs/cmd/builtins.c
+++ b/programs/cmd/builtins.c
@@ -992,40 +992,6 @@ void WCMD_part_execute(CMD_LIST **cmdList, WCHAR *firstcmd, WCHAR *variable,
   return;
 }
 
-/*****************************************************************************
- * WCMD_Execute
- *
- *	Execute a command after substituting variable text for the supplied parameter
- */
-
-void WCMD_execute (WCHAR *orig_cmd, WCHAR *param, WCHAR *subst, CMD_LIST **cmdList) {
-
-  WCHAR *new_cmd, *p, *s, *dup;
-  int size;
-
-  if (param) {
-    size = (strlenW (orig_cmd) + 1) * sizeof(WCHAR);
-    new_cmd = (WCHAR *) LocalAlloc (LMEM_FIXED | LMEM_ZEROINIT, size);
-    dup = s = WCMD_strdupW(orig_cmd);
-
-    while ((p = strstrW (s, param))) {
-      *p = '\0';
-      size += strlenW (subst) * sizeof(WCHAR);
-      new_cmd = (WCHAR *) LocalReAlloc ((HANDLE)new_cmd, size, 0);
-      strcatW (new_cmd, s);
-      strcatW (new_cmd, subst);
-      s = p + strlenW (param);
-    }
-    strcatW (new_cmd, s);
-    WCMD_process_command (new_cmd, cmdList);
-    free (dup);
-    LocalFree ((HANDLE)new_cmd);
-  } else {
-    WCMD_process_command (orig_cmd, cmdList);
-  }
-}
-
-
 /**************************************************************************
  * WCMD_give_help
  *
@@ -1067,7 +1033,7 @@ void WCMD_goto (CMD_LIST **cmdList) {
   WCHAR string[MAX_PATH];
 
   /* Do not process any more parts of a processed multipart or multilines command */
-  *cmdList = NULL;
+  if (cmdList) *cmdList = NULL;
 
   if (param1[0] == 0x00) {
     WCMD_output (WCMD_LoadMessage(WCMD_NOARG));
diff --git a/programs/cmd/wcmd.h b/programs/cmd/wcmd.h
index 0781fb6..1d82289 100644
--- a/programs/cmd/wcmd.h
+++ b/programs/cmd/wcmd.h
@@ -65,7 +65,6 @@ void WCMD_pause (void);
 void WCMD_pipe (CMD_LIST **command, WCHAR *var, WCHAR *val);
 void WCMD_popd (void);
 void WCMD_print_error (void);
-void WCMD_process_command (WCHAR *command, CMD_LIST **cmdList);
 void WCMD_pushd (WCHAR *);
 int  WCMD_read_console (WCHAR *string, int str_len);
 void WCMD_remove_dir (WCHAR *command);
@@ -92,7 +91,7 @@ WCHAR *WCMD_parameter (WCHAR *s, int n, WCHAR **where);
 WCHAR *WCMD_strtrim_leading_spaces (WCHAR *string);
 void WCMD_strtrim_trailing_spaces (WCHAR *string);
 void WCMD_opt_s_strip_quotes(WCHAR *cmd);
-void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable);
+void WCMD_HandleTildaModifiers(WCHAR **start, WCHAR *forVariable, WCHAR *forValue, BOOL justFors);
 BOOL WCMD_ask_confirm (WCHAR *message, BOOL showSureText, BOOL *optionAll);
 
 void WCMD_splitpath(const WCHAR* path, WCHAR* drv, WCHAR* dir, WCHAR* name, WCHAR* ext);
diff --git a/programs/cmd/wcmdmain.c b/programs/cmd/wcmdmain.c
index 8318590..b44b0ed 100644
--- a/programs/cmd/wcmdmain.c
+++ b/programs/cmd/wcmdmain.c
@@ -95,7 +95,7 @@ static char  *output_bufA = NULL;
 #define MAX_WRITECONSOLE_SIZE 65535
 BOOL unicodePipes = FALSE;
 
-static WCHAR *WCMD_expand_envvar(WCHAR *start);
+static WCHAR *WCMD_expand_envvar(WCHAR *start, WCHAR *forvar, WCHAR *forVal);
 
 /*****************************************************************************
  * Main entry point. This is a console application so we have a main() not a
@@ -456,6 +456,87 @@ int wmain (int argc, WCHAR *argvW[])
   return 0;
 }
 
+/*****************************************************************************
+ * Expand the command. Native expands lines from batch programs as they are
+ * read in and not again, except for 'for' variable substitution.
+ * eg. As evidence, "echo %1 && shift && echo %1" or "echo %%path%%"
+ */
+void handleExpansion(WCHAR *cmd, BOOL justFors, WCHAR *forVariable, WCHAR *forValue) {
+
+  /* For commands in a context (batch program):                  */
+  /*   Expand environment variables in a batch file %{0-9} first */
+  /*     including support for any ~ modifiers                   */
+  /* Additionally:                                               */
+  /*   Expand the DATE, TIME, CD, RANDOM and ERRORLEVEL special  */
+  /*     names allowing environment variable overrides           */
+  /* NOTE: To support the %PATH:xxx% syntax, also perform        */
+  /*   manual expansion of environment variables here            */
+
+  WCHAR *p = cmd;
+  WCHAR *s, *t;
+  int   i;
+
+  while ((p = strchrW(p, '%'))) {
+
+    WINE_TRACE("Translate command:%s %d (at: %s)\n",
+                   wine_dbgstr_w(cmd), justFors, wine_dbgstr_w(p));
+    i = *(p+1) - '0';
+
+    /* Don't touch %% unless its in Batch */
+    if (!justFors && *(p+1) == '%') {
+      if (context) {
+        s = WCMD_strdupW(p+1);
+        strcpyW (p, s);
+        free (s);
+      }
+      p+=1;
+
+    /* Replace %~ modifications if in batch program */
+    } else if (*(p+1) == '~') {
+      WCMD_HandleTildaModifiers(&p, forVariable, forValue, justFors);
+      p++;
+
+    /* Replace use of %0...%9 if in batch program*/
+    } else if (!justFors && context && (i >= 0) && (i <= 9)) {
+      s = WCMD_strdupW(p+2);
+      t = WCMD_parameter (context -> command, i + context -> shift_count[i], NULL);
+      strcpyW (p, t);
+      strcatW (p, s);
+      free (s);
+
+    /* Replace use of %* if in batch program*/
+    } else if (!justFors && context && *(p+1)=='*') {
+      WCHAR *startOfParms = NULL;
+      s = WCMD_strdupW(p+2);
+      t = WCMD_parameter (context -> command, 1, &startOfParms);
+      if (startOfParms != NULL) strcpyW (p, startOfParms);
+      else *p = 0x00;
+      strcatW (p, s);
+      free (s);
+
+    } else if (forVariable &&
+               (CompareString (LOCALE_USER_DEFAULT,
+                               SORT_STRINGSORT,
+                               p,
+                               strlenW(forVariable),
+                               forVariable, -1) == 2)) {
+      s = WCMD_strdupW(p + strlenW(forVariable));
+      strcpyW(p, forValue);
+      strcatW(p, s);
+      free(s);
+
+    } else if (!justFors) {
+      p = WCMD_expand_envvar(p, forVariable, forValue);
+
+    /* In a FOR loop, see if this is the variable to replace */
+    } else { /* Ignore %'s on second pass of batch program */
+      p++;
+    }
+  }
+
+  return;
+}
+
 
 /*****************************************************************************
  * Process one command. If the command is EXIT this routine does not return.
@@ -463,9 +544,11 @@ int wmain (int argc, WCHAR *argvW[])
  */
 
 
-void WCMD_process_command (WCHAR *command, CMD_LIST **cmdList)
+void WCMD_execute (WCHAR *command,
+                   WCHAR *forVariable, WCHAR *forValue,
+                   CMD_LIST **cmdList)
 {
-    WCHAR *cmd, *p, *s, *t, *redir;
+    WCHAR *cmd, *p, *redir;
     int status, i;
     DWORD count, creationDisposition;
     HANDLE h;
@@ -480,81 +563,24 @@ void WCMD_process_command (WCHAR *command, CMD_LIST **cmdList)
                                 STD_OUTPUT_HANDLE,
                                 STD_ERROR_HANDLE};
 
+    WINE_TRACE("command on entry:%s (%p), with '%s'='%s'\n",
+               wine_dbgstr_w(command), cmdList,
+               wine_dbgstr_w(forVariable), wine_dbgstr_w(forValue));
+
     /* Move copy of the command onto the heap so it can be expanded */
     new_cmd = HeapAlloc( GetProcessHeap(), 0, MAXSTRING * sizeof(WCHAR));
     strcpyW(new_cmd, command);
 
-    /* For commands in a context (batch program):                  */
-    /*   Expand environment variables in a batch file %{0-9} first */
-    /*     including support for any ~ modifiers                   */
-    /* Additionally:                                               */
-    /*   Expand the DATE, TIME, CD, RANDOM and ERRORLEVEL special  */
-    /*     names allowing environment variable overrides           */
-    /* NOTE: To support the %PATH:xxx% syntax, also perform        */
-    /*   manual expansion of environment variables here            */
-
-    p = new_cmd;
-    while ((p = strchrW(p, '%'))) {
-      i = *(p+1) - '0';
-
-      /* Don't touch %% */
-      if (*(p+1) == '%') {
-        p+=2;
-
-      /* Replace %~ modifications if in batch program */
-      } else if (context && *(p+1) == '~') {
-        WCMD_HandleTildaModifiers(&p, NULL);
-        p++;
-
-      /* Replace use of %0...%9 if in batch program*/
-      } else if (context && (i >= 0) && (i <= 9)) {
-        s = WCMD_strdupW(p+2);
-        t = WCMD_parameter (context -> command, i + context -> shift_count[i], NULL);
-        strcpyW (p, t);
-        strcatW (p, s);
-        free (s);
-
-      /* Replace use of %* if in batch program*/
-      } else if (context && *(p+1)=='*') {
-        WCHAR *startOfParms = NULL;
-        s = WCMD_strdupW(p+2);
-        t = WCMD_parameter (context -> command, 1, &startOfParms);
-        if (startOfParms != NULL) strcpyW (p, startOfParms);
-        else *p = 0x00;
-        strcatW (p, s);
-        free (s);
-
-      } else {
-        p = WCMD_expand_envvar(p);
-      }
-    }
+    /* Expand variables in command line mode only (batch mode will
+       be expanded as the line is read in, except for for loops)   */
+    handleExpansion(new_cmd, (context != NULL), forVariable, forValue);
     cmd = new_cmd;
 
-    /* In a batch program, unknown variables are replace by nothing */
-    /* so remove any remaining %var%                                */
-    if (context) {
-      p = cmd;
-      while ((p = strchrW(p, '%'))) {
-        if (*(p+1) == '%') {
-          p+=2;
-        } else {
-          s = strchrW(p+1, '%');
-          if (!s) {
-            *p=0x00;
-          } else {
-            t = WCMD_strdupW(s+1);
-            strcpyW(p, t);
-            free(t);
-          }
-        }
-      }
-
-      /* Show prompt before batch line IF echo is on and in batch program */
-      if (echo_mode && (cmd[0] != '@')) {
-        WCMD_show_prompt();
-        WCMD_output_asis ( cmd);
-        WCMD_output_asis ( newline);
-      }
+    /* Show prompt before batch line IF echo is on and in batch program */
+    if (context && echo_mode && (cmd[0] != '@')) {
+      WCMD_show_prompt();
+      WCMD_output_asis ( cmd);
+      WCMD_output_asis ( newline);
     }
 
 /*
@@ -1533,7 +1559,7 @@ void WCMD_pipe (CMD_LIST **cmdEntry, WCHAR *var, WCHAR *val) {
  *
  *	Expands environment variables, allowing for WCHARacter substitution
  */
-static WCHAR *WCMD_expand_envvar(WCHAR *start) {
+static WCHAR *WCMD_expand_envvar(WCHAR *start, WCHAR *forVar, WCHAR *forVal) {
     WCHAR *endOfVar = NULL, *s;
     WCHAR *colonpos = NULL;
     WCHAR thisVar[MAXSTRING];
@@ -1551,20 +1577,38 @@ static WCHAR *WCMD_expand_envvar(WCHAR *start) {
     static const WCHAR CdP[]       = {'%','C','D','%','\0'};
     static const WCHAR Random[]    = {'R','A','N','D','O','M','\0'};
     static const WCHAR RandomP[]   = {'%','R','A','N','D','O','M','%','\0'};
+    static const WCHAR Delims[]    = {'%',' ',':','\0'};
+
+    WINE_TRACE("Expanding: %s (%s,%s)\n", wine_dbgstr_w(start),
+               wine_dbgstr_w(forVal), wine_dbgstr_w(forVar));
 
     /* Find the end of the environment variable, and extract name */
-    endOfVar = strchrW(start+1, '%');
-    if (endOfVar == NULL) {
+    endOfVar = strpbrkW(start+1, Delims);
+
+    if (endOfVar == NULL || *endOfVar==' ') {
+
       /* In batch program, missing terminator for % and no following
          ':' just removes the '%'                                   */
-      s = WCMD_strdupW(start + 1);
-      strcpyW (start, s);
-      free(s);
+      if (context) {
+        s = WCMD_strdupW(start + 1);
+        strcpyW (start, s);
+        free(s);
+        return start;
+      } else {
 
-      /* FIXME: Some other special conditions here depending on whether
-         in batch, complex or not, and whether env var exists or not! */
-      return start;
+        /* In command processing, just ignore it - allows command line
+           syntax like: for %i in (a.a) do echo %i                     */
+        return start+1;
+      }
     }
+
+    /* If ':' found, process remaining up until '%' (or stop at ':' if
+       a missing '%' */
+    if (*endOfVar==':') {
+        WCHAR *endOfVar2 = strchrW(endOfVar+1, '%');
+        if (endOfVar2 != NULL) endOfVar = endOfVar2;
+    }
+
     memcpy(thisVar, start, ((endOfVar - start) + 1) * sizeof(WCHAR));
     thisVar[(endOfVar - start)+1] = 0x00;
     colonpos = strchrW(thisVar+1, ':');
@@ -1627,6 +1671,16 @@ static WCHAR *WCMD_expand_envvar(WCHAR *start) {
       wsprintf(thisVarContents, fmt, rand() % 32768);
       len = strlenW(thisVarContents);
 
+    /* Look for a matching 'for' variable */
+    } else if (forVar &&
+               (CompareString (LOCALE_USER_DEFAULT,
+                               SORT_STRINGSORT,
+                               thisVar,
+                               (colonpos - thisVar) - 1,
+                               forVar, -1) == 2)) {
+      strcpyW(thisVarContents, forVal);
+      len = strlenW(thisVarContents);
+
     } else {
 
       len = ExpandEnvironmentStrings(thisVar, thisVarContents,
@@ -1952,6 +2006,9 @@ WCHAR *WCMD_ReadAndParseLine(WCHAR *optionalcmd, CMD_LIST **output, HANDLE readF
         WCMD_output_asis(newline);
     }
 
+    /* Replace env vars if in a batch context */
+    if (context) handleExpansion(extraSpace, FALSE, NULL, NULL);
+
     /* Start with an empty string */
     curLen = 0;
 
@@ -2234,6 +2291,7 @@ WCHAR *WCMD_ReadAndParseLine(WCHAR *optionalcmd, CMD_LIST **output, HANDLE readF
           if (WCMD_fgets(extraSpace, MAXSTRING, readFrom) == NULL) break;
         }
         curPos = extraSpace;
+        if (context) handleExpansion(extraSpace, FALSE, NULL, NULL);
       }
     }
 
-- 
1.5.0




More information about the wine-patches mailing list