' ' & '"' handling in the command line

Francois Gouget fgouget at free.fr
Sun Sep 2 21:37:43 CDT 2001


   Hi,


   Some time ago I realized that the command line handling in Wine did
not work properly when arguments with spaces or double-quotes are
involved. For instance I would start a Wine application like this:

   main "a b" c

   and it would tell me something like this:

GetCommandLine=[./main a b c]


   Or I would call CreateProcess(... "main \"a b\" c" ...) and main
would get:

main -> argc=4
0 [./main]
1 [a]
2 [b]
3 [c]

   So I investigated this and found that 
 * double-quotes, '"', have the same meaning in win32 as in unix
   (tested on Win95 & Win2000)
 * backslash, '\', work as on unix, especially wrt double-quotes
 * single-quotes, ''', have no special meaning

   So CreateProcess(... "main \"a b\" c" ...) should give me:
GetCommandLine=[./main "a b" c]
main -> argc=3
0 [./main]
1 [a b]
2 [c]


   I searched for places where we convert command lines to argv lists
and reciprocally and modified them to work as on windows. With this
patch things should be correct, whether you call a Wine application from
Unix, from another Wine application via CreateProcess, or if you call a
Unix application from Wine. There's just one little bit in the msvcrt
code which I treated in a separate patch (p20010902-cmdline2.diff).

   In the process I wrote a few test applications:
 * hello
   regular hello world unix app
 * main
   A Wine console application which dumps its argc/argv and then calls 
   GetCommandLine, ...
   You can also call it with a '-i' parameter to test
   CommandLineToArgvW, except that _getws is not implemented in Wine so
   you can only do that on windows
 * winmain
   A Wine Gui application which dumps lpCmdLine and then calls 
   __getmainargs, CommandLineToArgvW(GetCommandLineW), ...
 * createp
   Which reads a command line from stdin and calls CreateProcess with it
 * spawnv
   Reads a list of arguments from stdin and calls spawnv with it
   (spawn* don't handle ' ' and '"', see p20010902-cmdline3.diff)
 * spawn
   Same but the command is fixed


   I know that there has been discussions about the command line
handling before and that some applications do very weird things. Please
test this patch and let me know how it goes.


--
Francois Gouget         fgouget at free.fr        http://fgouget.free.fr/
           If it stinks, it's chemistry. If it moves, it's biology.
                  If it does not work, It's computer science.
-------------- next part --------------
Index: memory/environ.c
===================================================================
RCS file: /home/wine/wine/memory/environ.c,v
retrieving revision 1.25
diff -u -r1.25 environ.c
--- memory/environ.c	2001/07/18 21:04:25	1.25
+++ memory/environ.c	2001/09/02 21:33:54
@@ -174,34 +174,121 @@
  *
  * Note that it does NOT necessarily include the file name.
  * Sometimes we don't even have any command line options at all.
+ *
+ * We must quote and escape characters so that the argv array can be rebuilt 
+ * from the command line:
+ * - spaces and tabs must be quoted
+ *   'a b'   -> '"a b"'
+ * - quotes must be escaped
+ *   '"'     -> '\"'
+ * - if '\'s are followed by a '"', they must be doubled and followed by '\"', 
+ *   resulting in an odd number of '\' followed by a '"'
+ *   '\"'    -> '\\\"'
+ *   '\\"'   -> '\\\\\"'
+ * - '\'s that are not followed by a '"' can be left as is
+ *   'a\b'   == 'a\b'
+ *   'a\\b'  == 'a\\b'
  */
 BOOL ENV_BuildCommandLine( char **argv )
 {
-    int len, quote = 0;
+    int len;
     char *p, **arg;
 
-    for (arg = argv, len = 0; *arg; arg++) len += strlen(*arg) + 1;
-    if ((argv[0]) && (quote = (strchr( argv[0], ' ' ) != NULL))) len += 2;
-    if (!(p = current_envdb.cmd_line = HeapAlloc( GetProcessHeap(), 0, len ))) return FALSE;
-    arg = argv;
-    if (quote)
+    len = 0;
+    for (arg = argv; *arg; arg++)
     {
-        *p++ = '\"';
-        strcpy( p, *arg );
-        p += strlen(p);
-        *p++ = '\"';
-        *p++ = ' ';
-        arg++;
+        int has_space,bcount;
+        char* a;
+
+        has_space=0;
+        bcount=0;
+        a=*arg;
+        while (*a!='\0') {
+            if (*a=='\\') {
+                bcount++;
+            } else {
+                if (*a==' ' || *a=='\t') {
+                    has_space=1;
+                } else if (*a=='"') {
+                    /* doubling of '\' preceeding a '"', 
+                     * plus escaping of said '"'
+                     */
+                    len+=2*bcount+1;
+                }
+                bcount=0;
+            }
+            a++;
+        }
+        len+=(a-*arg)+1 /* for the separating space */;
+        if (has_space)
+            len+=2; /* for the quotes */
     }
-    while (*arg)
+
+    if (!(current_envdb.cmd_line = HeapAlloc( GetProcessHeap(), 0, len )))
+        return FALSE;
+
+    p = current_envdb.cmd_line;
+    for (arg = argv; *arg; arg++)
     {
-        strcpy( p, *arg );
-        p += strlen(p);
-        *p++ = ' ';
-        arg++;
+        int has_space,has_quote;
+        char* a;
+
+        /* Check for quotes and spaces in this argument */
+        has_space=has_quote=0;
+        a=*arg;
+        while (*a!='\0') {
+            if (*a==' ' || *a=='\t') {
+                has_space=1;
+                if (has_quote)
+                    break;
+            } else if (*a=='"') {
+                has_quote=1;
+                if (has_space)
+                    break;
+            }
+            a++;
+        }
+
+        /* Now transfer it to the command line */
+        if (has_space)
+            *p++='"';
+        if (has_quote) {
+            int bcount;
+            char* a;
+
+            bcount=0;
+            a=*arg;
+            while (*a!='\0') {
+                if (*a=='\\') {
+                    *p++=*a;
+                    bcount++;
+                } else {
+                    if (*a=='"') {
+                        int i;
+
+                        /* Double all the '\\' preceeding this '"', plus one */
+                        for (i=0;i<=bcount;i++)
+                            *p++='\\';
+                        *p++='"';
+                    } else {
+                        *p++=*a;
+                    }
+                    bcount=0;
+                }
+                a++;
+            }
+        } else {
+            strcpy(p,*arg);
+            p+=strlen(*arg);
+        }
+        if (has_space)
+            *p++='"';
+        *p++=' ';
     }
-    if (p > current_envdb.cmd_line) p--;  /* remove last space */
-    *p = 0;
+    if (p > current_envdb.cmd_line)
+        p--;  /* remove last space */
+    *p = '\0';
+
     /* now allocate the Unicode version */
     len = MultiByteToWideChar( CP_ACP, 0, current_envdb.cmd_line, -1, NULL, 0 );
     if (!(cmdlineW = HeapAlloc( GetProcessHeap(), 0, len * sizeof(WCHAR) )))
Index: scheduler/process.c
===================================================================
RCS file: /home/wine/wine/scheduler/process.c,v
retrieving revision 1.160
diff -u -r1.160 process.c
--- scheduler/process.c	2001/08/06 17:48:17	1.160
+++ scheduler/process.c	2001/09/03 00:55:15
@@ -531,50 +531,96 @@
  */
 static char **build_argv( char *cmdline, int reserved )
 {
-    char **argv;
-    int count = reserved + 1;
-    char *p = cmdline;
+    int argc;
+    char** argv;
+    char *arg,*s,*d;
+    int in_quotes,bcount;
 
-    /* if first word is quoted store it as a single arg */
-    if (*cmdline == '\"')
-    {
-        if ((p = strchr( cmdline + 1, '\"' )))
-        {
-            p++;
-            count++;
+    argc=reserved+1;
+    bcount=0;
+    in_quotes=0;
+    s=cmdline;
+    while (1) {
+        if (*s=='\0' || ((*s==' ' || *s=='\t') && !in_quotes)) {
+            /* space */
+            argc++;
+            /* skip the remaining spaces */
+            while (*s==' ' || *s=='\t') {
+                s++;
+            }
+            if (*s=='\0')
+                break;
+            bcount=0;
+            continue;
+        } else if (*s=='\\') {
+            /* '\', count them */
+            bcount++;
+        } else if ((*s=='"') && ((bcount & 1)==0)) {
+            /* unescaped '"' */
+            in_quotes=!in_quotes;
+            bcount=0;
+        } else {
+            /* a regular character */
+            bcount=0;
         }
-        else p = cmdline;
-    }
-    while (*p)
-    {
-        while (*p && isspace(*p)) p++;
-        if (!*p) break;
-        count++;
-        while (*p && !isspace(*p)) p++;
+        s++;
     }
+    argv=malloc(argc*sizeof(*argv));
+    if (!argv)
+        return NULL;
+
+    arg=d=s=cmdline;
+    bcount=0;
+    in_quotes=0;
+    argc=reserved;
+    while (*s) {
+        if ((*s==' ' || *s=='\t') && !in_quotes) {
+            /* Close the argument and copy it */
+            *d=0;
+            argv[argc++]=arg;
+
+            /* skip the remaining spaces */
+            do {
+                s++;
+            } while (*s==' ' || *s=='\t');
 
-    if ((argv = malloc( count * sizeof(*argv) )))
-    {
-        char **argvptr = argv + reserved;
-        p = cmdline;
-        if (*cmdline == '\"')
-        {
-            if ((p = strchr( cmdline + 1, '\"' )))
-            {
-                *argvptr++ = cmdline + 1;
-                *p++ = 0;
+            /* Start with a new argument */
+            arg=d=s;
+            bcount=0;
+        } else if (*s=='\\') {
+            /* '\\' */
+            *d++=*s++;
+            bcount++;
+        } else if (*s=='"') {
+            /* '"' */
+            if ((bcount & 1)==0) {
+                /* Preceeded by an even number of '\', this is half that 
+                 * number of '\', plus a '"' which we discard.
+                 */
+                d-=bcount/2;
+                s++;
+                in_quotes=!in_quotes;
+            } else {
+                /* Preceeded by an odd number of '\', this is half that 
+                 * number of '\' followed by a '"'
+                 */
+                d=d-bcount/2-1;
+                *d++='"';
+                s++;
             }
-            else p = cmdline;
+            bcount=0;
+        } else {
+            /* a regular character */
+            *d++=*s++;
+            bcount=0;
         }
-        while (*p)
-        {
-            while (*p && isspace(*p)) *p++ = 0;
-            if (!*p) break;
-            *argvptr++ = p;
-            while (*p && !isspace(*p)) p++;
-        }
-        *argvptr = 0;
     }
+    if (*arg) {
+        *d='\0';
+        argv[argc++]=arg;
+    }
+    argv[argc]=NULL;
+
     return argv;
 }
 
Index: dlls/shell32/shell32_main.c
===================================================================
RCS file: /home/wine/wine/dlls/shell32/shell32_main.c,v
retrieving revision 1.81
diff -u -r1.81 shell32_main.c
--- dlls/shell32/shell32_main.c	2001/08/16 18:49:57	1.81
+++ dlls/shell32/shell32_main.c	2001/09/03 00:55:15
@@ -34,50 +34,152 @@
 #define MORE_DEBUG 1
 /*************************************************************************
  * CommandLineToArgvW			[SHELL32.@]
+ *
+ * We must interpret the quotes in the command line to rebuild the argv 
+ * array correctly:
+ * - arguments are separated by spaces or tabs
+ * - quotes serve as optional argument delimiters
+ *   '"a b"'   -> 'a b'
+ * - escaped quotes must be converted back to '"'
+ *   '\"'      -> '"'
+ * - an odd number of '\'s followed by '"' correspond to half that number 
+ *   of '\' followed by a '"' (extension of the above)
+ *   '\\\"'    -> '\"'
+ *   '\\\\\"'  -> '\\"'
+ * - an even number of '\'s followed by a '"' correspond to half that number
+ *   of '\', plus a regular quote serving as an argument delimiter (which 
+ *   means it does not appear in the result)
+ *   'a\\"b c"'   -> 'a\b c'
+ *   'a\\\\"b c"' -> 'a\\b c'
+ * - '\' that are not followed by a '"' are copied literally
+ *   'a\b'     -> 'a\b'
+ *   'a\\b'    -> 'a\\b'
+ *
+ * Note:
+ * '\t' == 0x0009
+ * ' '  == 0x0020
+ * '"'  == 0x0022
+ * '\\' == 0x005c
  */
 LPWSTR* WINAPI CommandLineToArgvW(LPCWSTR lpCmdline, int* numargs)
-{	LPWSTR  *argv,s,t;
-	LPWSTR  cmdline;
-	int	i;
-	TRACE("\n");
-
-	/* to get writeable copy */
-        if (!(cmdline = HeapAlloc( GetProcessHeap(), 0, (strlenW(lpCmdline)+1) * sizeof(WCHAR) )))
-            return NULL;
-        strcpyW( cmdline, lpCmdline );
-        s=cmdline;
-        i=0;
-	while (*s)
-	{ /* space */
-	  if (*s==0x0020) 
-	  { i++;
-	    s++;
-	    while (*s && *s==0x0020)
-	      s++;
-	    continue;
-	  }
-	  s++;
-	}
-	argv=(LPWSTR*)HeapAlloc( GetProcessHeap(), 0, sizeof(LPWSTR)*(i+1) );
-	s=t=cmdline;
-	i=0;
-	while (*s)
-	{
-            if (*s==0x0020)
-            {
-                argv[i++]=t;
-                while (*s==0x0020) *s++ = 0;
-                t=s;
-                continue;
+{
+    DWORD argc;
+    LPWSTR  *argv;
+    LPWSTR arg,s,d;
+    LPWSTR cmdline;
+    int in_quotes,bcount;
+
+    /* FIXME: same thing if we only have spaces */
+    if (*lpCmdline==0) {
+        /* Return the path to the executable */
+        DWORD size;
+
+        argv=HeapAlloc(GetProcessHeap(), 0, 2*sizeof(LPWSTR));
+        argv[0]=NULL;
+        size=16;
+        do {
+            size*=2;
+            argv[0]=HeapReAlloc(GetProcessHeap(), 0, argv[0], size);
+        } while (GetModuleFileNameW((HMODULE)0, argv[0], size) == 0);
+        argv[1]=NULL;
+        if (numargs)
+            *numargs=2;
+
+        return argv;
+    }
+
+    /* to get a writeable copy */
+    cmdline = HeapAlloc(GetProcessHeap(), 0, (strlenW(lpCmdline)+1) * sizeof(WCHAR));
+    if (!cmdline)
+        return NULL;
+    strcpyW(cmdline, lpCmdline);
+    argc=0;
+    bcount=0;
+    in_quotes=0;
+    s=cmdline;
+    while (1) {
+        if (*s==0 || ((*s==0x0009 || *s==0x0020) && !in_quotes)) {
+            /* space */
+            argc++;
+            /* skip the remaining spaces */
+            while (*s==0x0009 || *s==0x0020) {
+                s++;
             }
-            s++;
-	}
-	if (*t)
-	  argv[i++]=t;
-
-	argv[i]=NULL;
-	*numargs=i;
-	return argv;
+            if (*s==0)
+                break;
+            bcount=0;
+            continue;
+        } else if (*s==0x005c) {
+            /* '\', count them */
+            bcount++;
+        } else if ((*s==0x0022) && ((bcount & 1)==0)) {
+            /* unescaped '"' */
+            in_quotes=!in_quotes;
+            bcount=0;
+        } else {
+            /* a regular character */
+            bcount=0;
+        }
+        s++;
+    }
+    argv=HeapAlloc(GetProcessHeap(), 0, (argc+1)*sizeof(LPWSTR));
+
+    argc=0;
+    bcount=0;
+    in_quotes=0;
+    arg=d=s=cmdline;
+    while (*s) {
+        if ((*s==0x0009 || *s==0x0020) && !in_quotes) {
+            /* Close the argument and copy it */
+            *d=0;
+            argv[argc++]=arg;
+
+            /* skip the remaining spaces */
+            do {
+                s++;
+            } while (*s==0x0009 || *s==0x0020);
+
+            /* Start with a new argument */
+            arg=d=s;
+            bcount=0;
+        } else if (*s==0x005c) {
+            /* '\\' */
+            *d++=*s++;
+            bcount++;
+        } else if (*s==0x0022) {
+            /* '"' */
+            if ((bcount & 1)==0) {
+                /* Preceeded by an even number of '\', this is half that 
+                 * number of '\', plus a quote which we erase.
+                 */
+                d-=bcount/2;
+                in_quotes=!in_quotes;
+                s++;
+            } else {
+                /* Preceeded by an odd number of '\', this is half that 
+                 * number of '\' followed by a '"'
+                 */
+                d=d-bcount/2-1;
+                *d++='"';
+                s++;
+            }
+            bcount=0;
+        } else {
+            /* a regular character */
+            *d++=*s++;
+            bcount=0;
+        }
+    }
+    if (*arg) {
+        *d='\0';
+        argv[argc++]=arg;
+    }
+    argv[argc]=NULL;
+    if (numargs)
+        *numargs=argc;
+
+    HeapFree(GetProcessHeap(), 0, cmdline);
+    return argv;
 }
 
 /*************************************************************************
-------------- next part --------------
A non-text attachment was scrubbed...
Name: cmdline-test.tar.gz
Type: application/octet-stream
Size: 10272 bytes
Desc: cmdline-test.tar.gz
Url : http://www.winehq.org/pipermail/wine-devel/attachments/20010902/1e0a0618/cmdline-test.tar.obj


More information about the wine-devel mailing list