JDK-6468220 : (process) Runtime.exec(String[]) does not pass command line arguments correctly (win)
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang
  • Affected Version: 1.4.2,6
  • Priority: P4
  • Status: Resolved
  • Resolution: Duplicate
  • OS: generic,windows_xp
  • CPU: generic,x86
  • Submitted: 2006-09-07
  • Updated: 2013-08-08
  • Resolved: 2013-08-08
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.4.2_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_06-b03)
Java HotSpot(TM) Client VM (build 1.4.2_06-b03, mixed mode)

java version "1.5.0_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05)
Java HotSpot(TM) Client VM (build 1.5.0_06-b05, mixed mode)

java version "1.6.0-rc"
Java(TM) SE Runtime Environment (build 1.6.0-rc-b98)
Java HotSpot(TM) Client VM (build 1.6.0-rc-b98, mixed mode, sharing)


ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
or any other Windows

A DESCRIPTION OF THE PROBLEM :
On Windows platform Runtime.exec(String[] cmdarray) does not pass correctly command line arguments, if one of them contains double quotes (").

Examples
Passing/Expected --> Actual
{ "ab\"c", "d\"ef" } --> { "abc def" }
{ "a b \" c", "d \" e f" } --> { "a b ", "c d", "e f " }
{ "a", "", "b" } --> { "a", "b" }
{ "\" a" } -->
java.lang.IllegalArgumentException
        at java.lang.ProcessImpl.<init>(ProcessImpl.java:69)
        at java.lang.ProcessImpl.start(ProcessImpl.java:30)
        at java.lang.ProcessBuilder.start(ProcessBuilder.java:452)
        at java.lang.Runtime.exec(Runtime.java:593)
        at java.lang.Runtime.exec(Runtime.java:466)

The problem is that on Windows platform command line arguments are passed not as separated strings but as one string. And it is up to launched program to parse this command line string. Taking this into account it becomes clear that just concatenating cmdarray arguments is not enough. The quote characters have to be escaped correctly.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
javac Main.java
rem expected
java CmdLine ab\"c d\"ef
java CmdLine "a b \" c" "d \" e f"
java CmdLine a "" b
java CmdLine "\" a"
rem actual
java Main ab\"c d\"ef
java Main "a b \" c" "d \" e f"
java Main a "" b
java Main "\" a"


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
expected the same array as passed
1. { "ab\"c", "d\"ef" }
2. { "a b \" c", "d \" e f" }
3. { "a", "", "b" }
4. { "\" a" }

ACTUAL -
1. { "abc def" }
2. { "a b ", "c d", "e f " }
3. { "a", "b" }
4. java.lang.IllegalArgumentException
        at java.lang.ProcessImpl.<init>(ProcessImpl.java:69)
        at java.lang.ProcessImpl.start(ProcessImpl.java:30)
        at java.lang.ProcessBuilder.start(ProcessBuilder.java:452)
        at java.lang.Runtime.exec(Runtime.java:593)
        at java.lang.Runtime.exec(Runtime.java:466)


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.io.*;

class CmdLine {
	public static void main(String[] args) {
		for (int i = 0, n = args.length; i < n; ++i)
			System.out.println(args[i]);
	}
}

public class Main {

	public static void main(String[] args) throws IOException {
		String[] lines = new String[args.length + 2];
		lines[0] = "java";
		lines[1] = "CmdLine";
		System.arraycopy(args, 0, lines, 2, args.length);

		readOut(Runtime.getRuntime().exec(lines));
	}

	private static void readOut(Process process) throws IOException {
		InputStream is = process.getInputStream();
		try {
			int b;
			while ((b = is.read()) != -1)
				System.out.write(b);
		} finally {
			is.close();
		}
	}
}

---------- END SOURCE ----------
Submitter adds:

FIX FOR BUG NUMBER:
6468220

A DESCRIPTION OF THE FIX :
  Bug number : 6468220
  Bug Description : Runtime.exec(String[] cmdarray) does not pass command line arguments correctly
Diff Baseline : JDK 6 b98.
Diff :
diff -u -r old/java/lang/ProcessImpl.java new/java/lang/ProcessImpl.java
--- old/java/lang/ProcessImpl.java	2006-09-04 14:58:47.240001400 +0300
+++ new/java/lang/ProcessImpl.java	2006-09-06 18:05:59.132961600 +0300
@@ -47,32 +47,7 @@
 	// Win32 CreateProcess requires cmd[0] to be normalized
 	cmd[0] = new File(cmd[0]).getPath();
 
-	StringBuilder cmdbuf = new StringBuilder(80);
-	for (int i = 0; i &lt; cmd.length; i++) {
-            if (i &gt; 0) {
-                cmdbuf.append(' ');
-            }
-	    String s = cmd[i];
-	    if (s.indexOf(' ') &gt;= 0 || s.indexOf('\t') &gt;= 0) {
-	        if (s.charAt(0) != '"') {
-		    cmdbuf.append('"');
-		    cmdbuf.append(s);
-		    if (s.endsWith("\\")) {
-			cmdbuf.append("\\");
-		    }
-		    cmdbuf.append('"');
-                } else if (s.endsWith("\"")) {
-		    /* The argument has already been quoted. */
-		    cmdbuf.append(s);
-		} else {
-		    /* Unmatched quote for the argument. */
-		    throw new IllegalArgumentException();
-		}
-	    } else {
-	        cmdbuf.append(s);
-	    }
-	}
-	String cmdstr = cmdbuf.toString();
+	String cmdstr = Runtime.mergeCommandLine(cmd);
 
 	stdin_fd  = new FileDescriptor();
 	stdout_fd = new FileDescriptor();
diff -u -r old/java/lang/Runtime.java new/java/lang/Runtime.java
--- old/java/lang/Runtime.java	2006-09-04 14:57:37.818685800 +0300
+++ new/java/lang/Runtime.java	2006-09-06 18:05:51.604923600 +0300
@@ -8,7 +8,9 @@
 package java.lang;
 
 import java.io.*;
-import java.util.StringTokenizer;
+
+import java.util.List;
+import java.util.ArrayList;
 
 /**
  * Every Java application has a single instance of class
@@ -25,6 +27,11 @@
  */
 
 public class Runtime {
+    private static final char SPACE = ' ';
+    private static final char TAB = '\t';
+    private static final char DOUBLEQUOTE = '\"';
+    private static final char BACKSLASH = '\\';
+
     private static Runtime currentRuntime = new Runtime();
 
     /**
@@ -369,6 +376,117 @@
         return exec(command, envp, null);
     }
 
+	/**
+	 * Splits single line into separate command line arguments with following rules:
+	 * &lt;ul&gt;
+	 * &lt;li&gt;Arguments are delimited by white space, which is either a space or a tab.&lt;/li&gt;
+	 * &lt;li&gt;The caret character (\n) is not recognized as an escape character or delimiter. The character is handled completely
+	 * by the command-line parser in the operating system before being passed to the args array in the program.&lt;/li&gt;
+	 * &lt;li&gt;A string surrounded by double quotation marks ("string") is interpreted as a single argument, regardless of white
+	 * space contained within. A quoted string can be embedded in an argument.&lt;/li&gt;
+	 * &lt;li&gt;A double quotation mark preceded by a backslash (\") is interpreted as a literal double quotation mark character (").&lt;/li&gt;
+	 * &lt;li&gt;Backslashes are interpreted literally, unless they immediately precede a double quotation mark.&lt;/li&gt;
+	 * &lt;li&gt;If an even number of backslashes is followed by a double quotation mark, one backslash is placed in the args array for every
+	 * pair of backslashes, and the double quotation mark is interpreted as a string delimiter.&lt;/li&gt;
+	 * &lt;li&gt;If an odd number of backslashes is followed by a double quotation mark, one backslash is placed in the args array
+	 * for every pair of backslashes, and the double quotation mark is escaped by the remaining backslash,
+	 * causing a literal double quotation mark (") to be placed in args. &lt;/li&gt;
+	 * &lt;/ul&gt;
+	 * &lt;/p&gt;
+	 * &lt;p&gt;Example
+	 * &lt;table border="1"&gt;
+	 * &lt;tr&gt;&lt;th&gt;line&lt;/th&gt;&lt;th&gt;args[0]&lt;/th&gt;&lt;th&gt;args[1]&lt;/th&gt;&lt;th&gt;args[2]&lt;/th&gt;&lt;/tr&gt;
+	 * &lt;tr&gt;&lt;td&gt;"abc" d e&lt;/td&gt;&lt;td&gt;abc&lt;/td&gt;&lt;td&gt;d&lt;/td&gt;&lt;td&gt;e&lt;/td&gt;&lt;/tr&gt;
+	 * &lt;tr&gt;&lt;td&gt;a\\\b d"e f"g h&lt;/td&gt;&lt;td&gt;a\\\b&lt;/td&gt;&lt;td&gt;de fg&lt;/td&gt;&lt;td&gt;h&lt;/td&gt;&lt;/tr&gt;
+	 * &lt;tr&gt;&lt;td&gt;a\\\"b c d&lt;/td&gt;&lt;td&gt;a\"b&lt;/td&gt;&lt;td&gt;c&lt;/td&gt;&lt;td&gt;d&lt;/td&gt;&lt;/tr&gt;
+	 * &lt;tr&gt;&lt;td&gt;a\\\\"b c" d e&lt;/td&gt;&lt;td&gt;a\\b c&lt;/td&gt;&lt;td&gt;d&lt;/td&gt;&lt;td&gt;e&lt;/td&gt;&lt;/tr&gt;
+	 * &lt;/table&gt;
+	 *
+	 * @see #mergeCommandLine(String[])
+	 * @param line - line to be splitted
+	 * @return separate arguments
+	 */
+	public static String[] splitCommandLine(String line) {
+		List&lt;String&gt; list = new ArrayList&lt;String&gt;();
+		StringBuilder buffer = new StringBuilder();
+		boolean inquote = false;
+		boolean flushed = true;
+		int backslashcount = 0;
+		for (int i = 0, n = line.length(); i &lt; n; ++i) {
+			char ch = line.charAt(i);
+			if (ch == BACKSLASH) {
+				++backslashcount;
+			} else {
+				int m = (ch == DOUBLEQUOTE)? backslashcount / 2:  backslashcount;
+				for (int j = 0; j &lt; m; ++j)
+					buffer.append(BACKSLASH);
+
+				if ((ch == SPACE) || (ch == TAB)) {
+					if (inquote) {
+						buffer.append(ch);
+						flushed = false;
+					} else {
+						if (!flushed) {
+							list.add(buffer.toString());
+							buffer.setLength(0);
+							flushed = true;
+						}
+					}
+				} else {
+					if ((ch == DOUBLEQUOTE) && (backslashcount % 2 == 0))
+						inquote = !inquote;
+					else
+						buffer.append(ch);
+					flushed = false;
+				}
+				backslashcount = 0;
+			}
+		}
+		for (int j = 0, m = backslashcount; j &lt; m; ++j)
+			buffer.append(BACKSLASH);
+		if (!flushed)
+			list.add(buffer.toString());
+		return list.toArray(new String[list.size()]);
+	}
+
+	/**
+	 * Merges separate command line arguments into single line.
+	 * This is opposite to &lt;code&gt;{@link #splitCommandLine(String)}&lt;/code&gt;.
+	 *
+	 * @see #splitCommandLine(String)
+	 * @param args - arguments to be merged
+	 * @return single line
+	 */
+	public static String mergeCommandLine(String... args) {
+		StringBuilder buffer = new StringBuilder();
+		for (int i = 0, n = args.length; i &lt; n; ++i) {
+			if (i &gt; 0)
+				buffer.append(SPACE);
+			String arg = args[i];
+			boolean quote = ((arg.length() == 0) || (arg.indexOf(SPACE) != -1) || (arg.indexOf(TAB) != -1));
+			if (quote)
+				buffer.append(DOUBLEQUOTE);
+			int backslashcount = 0;
+			for (int j = 0, m = arg.length(); j &lt; m; ++j) {
+				char ch = arg.charAt(j);
+				if (ch == BACKSLASH) {
+					++backslashcount;
+				} else {
+					int l = (ch == DOUBLEQUOTE)? backslashcount * 2 + 1: backslashcount;
+					for (int k = 0; k &lt; l; ++k)
+						buffer.append(BACKSLASH);
+					buffer.append(ch);
+					backslashcount = 0;
+				}
+			}
+			for (int k = 0; k &lt; backslashcount; ++k)
+				buffer.append(BACKSLASH);
+			if (quote)
+				buffer.append(DOUBLEQUOTE);
+		}
+		return buffer.toString();
+	}
+
     /**
      * Executes the specified string command in a separate process with the
      * specified environment and working directory.
@@ -381,11 +499,7 @@
      * &lt;code&gt;command&lt;/code&gt;.
      *
      * &lt;p&gt;More precisely, the &lt;code&gt;command&lt;/code&gt; string is broken
-     * into tokens using a {@link StringTokenizer} created by the call
-     * &lt;code&gt;new {@link StringTokenizer}(command)&lt;/code&gt; with no
-     * further modification of the character categories.  The tokens
-     * produced by the tokenizer are then placed in the new string
-     * array &lt;code&gt;cmdarray&lt;/code&gt;, in the same order.
+     * into tokens using a {@link #splitCommandLine(String)}.
      *
      * @param   command   a specified system command.
      *
@@ -424,11 +538,7 @@
         if (command.length() == 0)
             throw new IllegalArgumentException("Empty command");
 
-	StringTokenizer st = new StringTokenizer(command);
-	String[] cmdarray = new String[st.countTokens()];
- 	for (int i = 0; st.hasMoreTokens(); i++)
-	    cmdarray[i] = st.nextToken();
-	return exec(cmdarray, envp, dir);
+	return exec(splitCommandLine(command), envp, dir);
     }
 
     /**

Note:
The command line arguments are merged in java.lang.ProcessImpl into single line in assumption that this command line will be parsed later according to the rules described here:
http://msdn.microsoft.com/library/en-us/vclang98/html/_pluslang_parsing_c.2b2b_.command.2d.line_arguments.asp

Namely,

1. Arguments are delimited by white space, which is either a space or a tab.

2. The caret character is not recognized as an escape character or delimiter. The character is handled completely by the command-line parser in the operating system before being passed to the argv array in the program.

3. A string surrounded by double quotation marks ("string") is interpreted as a single argument, regardless of white space contained within. A quoted string can be embedded in an argument.

4. A double quotation mark preceded by a backslash ( \") is interpreted as a literal double quotation mark character (").

5. Backslashes are interpreted literally, unless they immediately precede a double quotation mark.

6. If an even number of backslashes is followed by a double quotation mark, one backslash is placed in the argv array for every pair of backslashes, and the double quotation mark is interpreted as a string delimiter.

7. If an odd number of backslashes is followed by a double quotation mark, one backslash is placed in the argv array for every pair of backslashes, and the double quotation mark is escaped by the remaining backslash, causing a literal double quotation mark (") to be placed in argv.

So, java.lang.ProcessImpl (windows specific class) uses Runtime.mergeCommandLine.

However, changes only in java.lang.ProcessImpl may affect windows users that use Runtime.exec(String command, ...) instead of Runtime.exec(String[] cmdarray, ...). Despite Runtime.exec(String command, ...) somtimes tokenizes command line arguments incorrectly, these arguments were gathered up so, they could be parsed more correctly. For example, these command "a \"b c\" d" was divided into 4 tokens { "a", "\"b", "c\"", "d" } and then they were connected to the single line "a \"b c\" d", which was parsed by launched program into 3 args { "a", "b c", "d" } as most users expect.

Thus we have also change tokenization algorithm to meet Runtime.exec(String command, ...) users expectation. So, Runtime.exec(String command, ...) has to use splitCommandLine instead of StringTokenizer. From now, these command "a \"b c\" d" is divided into 3 tokens { "a", "b c", "d" } and then they are connected to the single line "a \"b c\" d", which is parsed by launched program into 3 args { "a", "b c", "d" } as most users expect and as it was before.

As for backward compatibility, using StringTokenizer is relevant only for command line arguments without spaces and doublequotes. For these cases splitCommandLine works equally.


JUnit TESTCASE :
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import junit.framework.TestCase;

class DumpCommandLine {
	public static void main(String... args) {
		for (String arg : args) {
			System.out.println(arg);
		}
	}
}

public class RuntimeExecTest extends TestCase {
	private static final String[] PROGRAM = { "cmd", "/c", "%JDK6_HOME%\\bin\\java", DumpCommandLine.class.getName() };

	static void check(String... args) throws IOException {
		String[] cmdarray = new String[PROGRAM.length + args.length];
		System.arraycopy(PROGRAM, 0, cmdarray, 0, PROGRAM.length);
		System.arraycopy(args, 0, cmdarray, PROGRAM.length, args.length);
		Process process = Runtime.getRuntime().exec(cmdarray);
		List&lt;String&gt; actual = new ArrayList&lt;String&gt;();
		BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
		try {
			String line;
			while ((line = reader.readLine()) != null)
				actual.add(line);
		} finally {
			reader.close();
		}
		assertEquals(Arrays.asList(args), actual);
	}

	public void testSimple() throws Exception {
		check("a", "bc", "def");
	}

	public void testNothing() throws Exception {
		check();
	}

	public void testEmptySole() throws Exception {
		check("");
	}

	public void testEmptyLast() throws Exception {
		check("ab", "");
	}

	public void testEmptyFirst() throws Exception {
		check("", "cd");
	}

	public void testQuoteFirstOnly() throws Exception {
		check("\" x");
	}

	public void testSlashAndQuote() throws Exception {
		check("a\\\"b", "c", "d");
	}

	public void testSlashSlash() throws Exception {
		check("a\\\\b c", "d", "e");
	}

	public void testThreeSlash() throws Exception {
		check("a\\\\\\b", "de fg", "h");
	}

	public void testQuoteThenQuote() throws Exception {
		check("ab\"c", "d\"ef");
	}

	public void testQuoteThenQuoteWithSpaces() throws Exception {
		check("a b \" c", "d \" e f");
	}

	public static void main(String[] args) {
		junit.textui.TestRunner.run(RuntimeExecTest.class);
	}

}
Here are the latest versions of the submitters diff and test:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import junit.framework.TestCase;

class DumpCommandLine {
	public static void main(String... args) {
		for (String arg : args) {
			System.out.println(arg);
		}
	}
}

public class RuntimeExecTest extends TestCase {
	private static final String[] PROGRAM = { "cmd", "/c", "%JDK6_HOME%\\bin\\java", DumpCommandLine.class.getName() };

	static void check(String... args) throws IOException {
		String[] cmdarray = new String[PROGRAM.length + args.length];
		System.arraycopy(PROGRAM, 0, cmdarray, 0, PROGRAM.length);
		System.arraycopy(args, 0, cmdarray, PROGRAM.length, args.length);
		Process process = Runtime.getRuntime().exec(cmdarray);
		List<String> actual = new ArrayList<String>();
		BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
		try {
			String line;
			while ((line = reader.readLine()) != null)
				actual.add(line);
		} finally {
			reader.close();
		}
		assertEquals(Arrays.asList(args), actual);
	}

	public void testSimple() throws Exception {
		check("a", "bc", "def");
	}

	public void testNothing() throws Exception {
		check();
	}

	public void testEmptySole() throws Exception {
		check("");
	}

	public void testEmptyLast() throws Exception {
		check("ab", "");
	}

	public void testEmptyFirst() throws Exception {
		check("", "cd");
	}

	public void testQuoteFirstOnly() throws Exception {
		check("\" x");
	}

	public void testSlashAndQuote() throws Exception {
		check("a\\\"b", "c", "d");
	}

	public void testSlashSlash() throws Exception {
		check("a\\\\b c", "d", "e");
	}

	public void testThreeSlash() throws Exception {
		check("a\\\\\\b", "de fg", "h");
	}

	public void testQuoteThenQuote() throws Exception {
		check("ab\"c", "d\"ef");
	}

	public void testQuoteThenQuoteWithSpaces() throws Exception {
		check("a b \" c", "d \" e f");
	}

	public void testQuotedWithTrailingSlash1() throws Exception {
		check("c:\\Program Files\\");
	}

	public void testQuotedWithTrailingSlash() throws Exception {
		check("c:\\Program Files\\", "padding");
	}

	public static void main(String[] args) {
		junit.textui.TestRunner.run(RuntimeExecTest.class);
	}

}


diff -u -r old/java/lang/ProcessImpl.java new/java/lang/ProcessImpl.java
--- old/java/lang/ProcessImpl.java	2006-09-04 14:58:47.240001400 +0300
+++ new/java/lang/ProcessImpl.java	2006-09-06 18:05:59.132961600 +0300
@@ -47,32 +47,7 @@
 	// Win32 CreateProcess requires cmd[0] to be normalized
 	cmd[0] = new File(cmd[0]).getPath();
 
-	StringBuilder cmdbuf = new StringBuilder(80);
-	for (int i = 0; i < cmd.length; i++) {
-            if (i > 0) {
-                cmdbuf.append(' ');
-            }
-	    String s = cmd[i];
-	    if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0) {
-	        if (s.charAt(0) != '"') {
-		    cmdbuf.append('"');
-		    cmdbuf.append(s);
-		    if (s.endsWith("\\")) {
-			cmdbuf.append("\\");
-		    }
-		    cmdbuf.append('"');
-                } else if (s.endsWith("\"")) {
-		    /* The argument has already been quoted. */
-		    cmdbuf.append(s);
-		} else {
-		    /* Unmatched quote for the argument. */
-		    throw new IllegalArgumentException();
-		}
-	    } else {
-	        cmdbuf.append(s);
-	    }
-	}
-	String cmdstr = cmdbuf.toString();
+	String cmdstr = Runtime.mergeCommandLine(cmd);
 
 	stdin_fd  = new FileDescriptor();
 	stdout_fd = new FileDescriptor();
diff -u -r old/java/lang/Runtime.java new/java/lang/Runtime.java
--- old/java/lang/Runtime.java	2006-09-04 14:57:37.818685800 +0300
+++ new/java/lang/Runtime.java	2006-09-15 10:23:40.994669200 +0300
@@ -8,7 +8,9 @@
 package java.lang;
 
 import java.io.*;
-import java.util.StringTokenizer;
+
+import java.util.List;
+import java.util.ArrayList;
 
 /**
  * Every Java application has a single instance of class 
@@ -25,6 +27,11 @@
  */
 
 public class Runtime {
+    private static final char SPACE = ' ';
+    private static final char TAB = '\t';
+    private static final char DOUBLEQUOTE = '\"';
+    private static final char BACKSLASH = '\\';
+
     private static Runtime currentRuntime = new Runtime();
 
     /**
@@ -369,6 +376,118 @@
         return exec(command, envp, null);
     }
 
+	/**
+	 * Splits single line into separate command line arguments with following rules:
+	 * <ul>
+	 * <li>Arguments are delimited by white space, which is either a space or a tab.</li>
+	 * <li>The caret character (\n) is not recognized as an escape character or delimiter. The character is handled completely
+	 * by the command-line parser in the operating system before being passed to the args array in the program.</li>
+	 * <li>A string surrounded by double quotation marks ("string") is interpreted as a single argument, regardless of white
+	 * space contained within. A quoted string can be embedded in an argument.</li>
+	 * <li>A double quotation mark preceded by a backslash (\") is interpreted as a literal double quotation mark character (").</li>
+	 * <li>Backslashes are interpreted literally, unless they immediately precede a double quotation mark.</li>
+	 * <li>If an even number of backslashes is followed by a double quotation mark, one backslash is placed in the args array for every
+	 * pair of backslashes, and the double quotation mark is interpreted as a string delimiter.</li>
+	 * <li>If an odd number of backslashes is followed by a double quotation mark, one backslash is placed in the args array
+	 * for every pair of backslashes, and the double quotation mark is escaped by the remaining backslash,
+	 * causing a literal double quotation mark (") to be placed in args. </li>
+	 * </ul>
+	 * </p>
+	 * <p>Example
+	 * <table border="1">
+	 * <tr><th>line</th><th>args[0]</th><th>args[1]</th><th>args[2]</th></tr>
+	 * <tr><td>"abc" d e</td><td>abc</td><td>d</td><td>e</td></tr>
+	 * <tr><td>a\\\b d"e f"g h</td><td>a\\\b</td><td>de fg</td><td>h</td></tr>
+	 * <tr><td>a\\\"b c d</td><td>a\"b</td><td>c</td><td>d</td></tr>
+	 * <tr><td>a\\\\"b c" d e</td><td>a\\b c</td><td>d</td><td>e</td></tr>
+	 * </table>
+	 *
+	 * @see #mergeCommandLine(String[])
+	 * @param line - line to be splitted
+	 * @return separate arguments
+	 */
+	public static String[] splitCommandLine(String line) {
+		List<String> list = new ArrayList<String>();
+		StringBuilder buffer = new StringBuilder();
+		boolean inquote = false;
+		boolean flushed = true;
+		int backslashcount = 0;
+		for (int i = 0, n = line.length(); i < n; ++i) {
+			char ch = line.charAt(i);
+			if (ch == BACKSLASH) {
+				++backslashcount;
+			} else {
+				int m = (ch == DOUBLEQUOTE)? backslashcount / 2:  backslashcount;
+				for (int j = 0; j < m; ++j) 
+					buffer.append(BACKSLASH);
+
+				if ((ch == SPACE) || (ch == TAB)) {
+					if (inquote) {
+						buffer.append(ch);
+						flushed = false;
+					} else {
+						if (!flushed) {
+							list.add(buffer.toString());
+							buffer.setLength(0);
+							flushed = true;
+						}
+					}
+				} else {
+					if ((ch == DOUBLEQUOTE) && (backslashcount % 2 == 0))
+						inquote = !inquote;
+					else
+						buffer.append(ch);
+					flushed = false;
+				}
+				backslashcount = 0;
+			}
+		}
+		for (int j = 0, m = backslashcount; j < m; ++j)
+			buffer.append(BACKSLASH);
+		if (!flushed)
+			list.add(buffer.toString());
+		return list.toArray(new String[list.size()]);
+	}
+
+	/**
+	 * Merges separate command line arguments into single line.
+	 * This is opposite to <code>{@link #splitCommandLine(String)}</code>.
+	 *
+	 * @see #splitCommandLine(String)
+	 * @param args - arguments to be merged
+	 * @return single line
+	 */
+	public static String mergeCommandLine(String... args) {
+		StringBuilder buffer = new StringBuilder();
+		for (int i = 0, n = args.length; i < n; ++i) {
+			if (i > 0)
+				buffer.append(SPACE);
+			String arg = args[i];
+			boolean quote = ((arg.length() == 0) || (arg.indexOf(SPACE) != -1) || (arg.indexOf(TAB) != -1));
+			if (quote)
+				buffer.append(DOUBLEQUOTE);
+			int backslashcount = 0;
+			for (int j = 0, m = arg.length(); j < m; ++j) {
+				char ch = arg.charAt(j);
+				if (ch == BACKSLASH) {
+					++backslashcount;
+				} else {
+					int l = (ch == DOUBLEQUOTE)? backslashcount * 2 + 1: backslashcount;
+					for (int k = 0; k < l; ++k)
+						buffer.append(BACKSLASH);
+					buffer.append(ch);
+					backslashcount = 0;
+				}
+			}
+			int l = quote? backslashcount * 2: backslashcount;
+			for (int k = 0; k < l; ++k)
+				buffer.append(BACKSLASH);
+			if (quote)
+				buffer.append(DOUBLEQUOTE);
+		}
+		return buffer.toString();
+	}
+
     /**
      * Executes the specified string command in a separate process with the
      * specified environment and working directory.
@@ -381,11 +500,7 @@
      * <code>command</code>.
      *
      * <p>More precisely, the <code>command</code> string is broken
-     * into tokens using a {@link StringTokenizer} created by the call
-     * <code>new {@link StringTokenizer}(command)</code> with no
-     * further modification of the character categories.  The tokens
-     * produced by the tokenizer are then placed in the new string
-     * array <code>cmdarray</code>, in the same order.
+     * into tokens using a {@link #splitCommandLine(String)}.
      *
      * @param   command   a specified system command.
      *
@@ -424,11 +539,7 @@
         if (command.length() == 0)
             throw new IllegalArgumentException("Empty command");
 
-	StringTokenizer st = new StringTokenizer(command);
-	String[] cmdarray = new String[st.countTokens()];
- 	for (int i = 0; st.hasMoreTokens(); i++)
-	    cmdarray[i] = st.nextToken();
-	return exec(cmdarray, envp, dir);
+	return exec(splitCommandLine(command), envp, dir);
     }
 
     /**

Comments
The rest of issue is duplicate of JDK-6518827.
08-08-2013

All problems, except empty argument support (JDK-6518827), are resolved or would not be fixed due to MS and Java documentation coverage. So, I am closing the problem as JDK-6518827 duplicate.
08-08-2013

EVALUATION We can't change the current parsing algorithm because of compatibility constraints, and users can get the effects they need (albeit with some effort), and there is no place to put a method with a better parsing algorithm, so I reluctantly think we should close this as Will Not Fix. I am especially unhappy because of the excellent test case provided by the submitter. See the workaround for a method to include in user code to properly quote arguments before passing to ProcessBuilder or Runtime.exec. We cannot change the implementation, but we should document the behavior and workaround in a place more obvious than this CR, but there is no such obvious document currently. We need a Platform Guide!
12-09-2006

WORK AROUND Here is a way to quote arguments to the windows process API: static boolean needsQuoting(String s) { int len = s.length(); if (len == 0) // empty string have to be quoted return true; for (int i = 0; i < len; i++) { switch (s.charAt(i)) { case ' ': case '\t': case '\\': case '"': return true; } } return false; } static String winQuote(String s) { if (! needsQuoting(s)) return s; s = s.replaceAll("([\\\\]*)\"", "$1$1\\\\\""); s = s.replaceAll("([\\\\]*)\\z", "$1$1"); return "\"" + s + "\""; } Note that I'm probably not the first to have invented such a quoting mechanism. Lots of folks probably happily quote their arguments in this way, and their programs must not be broken. We could introduce a special version of ProcessBuilder that autoquoted its arguments in this way, but this is not in the spirit of the Java API (no system-specific functionality) and there is no obvious place to put such a method.
12-09-2006

EVALUATION The windows command line parsing code was last changed in 2001 by: 4491217: Runtime.exec of 1.4.0 beta75 does not work on WindowsNT/2000 4272706: Spaces in command-line parameters removed by Runtime.exec That change seems incomplete and incompatible. We generally do not make such incompatible changes anymore.
12-09-2006

EVALUATION Unfortunately, the Windows API maps most directly to an interface where Runtime.exec(String) passes the arg string directly to the OS, but that method is specified to break up its arguments using StringTokenizer, which is surprisingly rarely what you want. It's very hard to make changes in this area, because - they change the behavior, and so are incompatible - the current behavior (how to pass strings to the OS) is unspecified, and so is spec-compliant. If there's any way at all that the current behavior can be relied upon, then we really can't change it.
08-09-2006

EVALUATION Contribution forum : https://jdk-collaboration.dev.java.net/servlets/ProjectForumMessageView?forumID=1463&messageID=15367
08-09-2006