mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-21 22:11:40 +07:00
Merge branch 'eldar/windows-command-line'
# Conflicts: # platform/platform-tests/testSrc/com/intellij/execution/GeneralCommandLineTest.java
This commit is contained in:
@@ -55,6 +55,16 @@ public class ProcessBuilder {
|
||||
}
|
||||
|
||||
// please keep an implementation in sync with [util] CommandLineUtil.toCommandLine()
|
||||
//
|
||||
// Comparing to the latter, this is a simplified version with the following limitations on Windows (neither of these
|
||||
// seems to be used in our cases though):
|
||||
//
|
||||
// - does not fully handle \" escaping (must escape quoted ["C:\Program Files\"] -> [\"C:\Program Files\\\"])
|
||||
// - does not support `cmd.exe /c call command args-with-special-chars-[&<>()@^|]
|
||||
// - does not handle special chars [&<>()@^|] interleaved with quotes ["] properly (the quote flag)
|
||||
// - mangles the output of `cmd.exe /c echo ...`
|
||||
//
|
||||
// If either of these becomes an issue, please refer to [util] CommandLineUtil.addToWindowsCommandLine() for a possible implementation.
|
||||
public Process createProcess() throws IOException {
|
||||
if (myParameters.size() < 1) {
|
||||
throw new IllegalArgumentException("Executable name not specified");
|
||||
|
||||
@@ -332,6 +332,17 @@ public class GeneralCommandLine implements UserDataHolder {
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares command (quotes and escapes all arguments) and returns it as a newline-separated list.
|
||||
*
|
||||
* @return command as a newline-separated list.
|
||||
* @see #getPreparedCommandLine(Platform)
|
||||
*/
|
||||
@NotNull
|
||||
public String getPreparedCommandLine() {
|
||||
return getPreparedCommandLine(Platform.current());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares command (quotes and escapes all arguments) and returns it as a newline-separated list
|
||||
* (suitable e.g. for passing in an environment variable).
|
||||
@@ -342,7 +353,12 @@ public class GeneralCommandLine implements UserDataHolder {
|
||||
@NotNull
|
||||
public String getPreparedCommandLine(@NotNull Platform platform) {
|
||||
String exePath = myExePath != null ? myExePath : "";
|
||||
return StringUtil.join(CommandLineUtil.toCommandLine(exePath, myProgramParams.getList(), platform), "\n");
|
||||
return StringUtil.join(prepareCommandLine(exePath, myProgramParams.getList(), platform), "\n");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected List<String> prepareCommandLine(@NotNull String command, @NotNull List<String> parameters, @NotNull Platform platform) {
|
||||
return CommandLineUtil.toCommandLine(command, parameters, platform);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -353,21 +369,20 @@ public class GeneralCommandLine implements UserDataHolder {
|
||||
LOG.debug(" charset: " + myCharset);
|
||||
}
|
||||
|
||||
List<String> commands;
|
||||
try {
|
||||
checkWorkingDirectory();
|
||||
|
||||
if (StringUtil.isEmptyOrSpaces(myExePath)) {
|
||||
throw new ExecutionException(IdeBundle.message("run.configuration.error.executable.not.specified"));
|
||||
}
|
||||
|
||||
commands = CommandLineUtil.toCommandLine(myExePath, myProgramParams.getList());
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
LOG.info(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
List<String> commands = prepareCommandLine(myExePath, myProgramParams.getList(), Platform.current());
|
||||
|
||||
try {
|
||||
return startProcess(commands);
|
||||
}
|
||||
@@ -377,9 +392,25 @@ public class GeneralCommandLine implements UserDataHolder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @implNote for subclasses:
|
||||
* On Windows the escapedCommands argument must never be modified or augmented in any way.
|
||||
* Windows command line handling is extremely fragile and vague, and the exact escaping of a particular argument may vary
|
||||
* depending on values of the preceding arguments.
|
||||
*
|
||||
* [foo] [^] -> [foo] [^^]
|
||||
*
|
||||
* but:
|
||||
* [foo] ["] [^] -> [foo] [\"] ["^"]
|
||||
*
|
||||
* Notice how the last parameter escaping changes after prepending another argument.
|
||||
*
|
||||
* If you need to alter the command line passed in, override the {@link #prepareCommandLine(String, List, Platform)} method instead.
|
||||
*/
|
||||
@NotNull
|
||||
protected Process startProcess(@NotNull List<String> commands) throws IOException {
|
||||
ProcessBuilder builder = new ProcessBuilder(commands);
|
||||
protected Process startProcess(@NotNull List<String> escapedCommands) throws IOException {
|
||||
ProcessBuilder builder = new ProcessBuilder(escapedCommands);
|
||||
setupEnvironment(builder.environment());
|
||||
builder.directory(myWorkDirectory);
|
||||
builder.redirectErrorStream(myRedirectErrorStream);
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.testFramework.PlatformTestUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.intellij.lang.annotations.MagicConstant;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -43,8 +42,9 @@ import static org.junit.Assert.*;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
public class GeneralCommandLineTest {
|
||||
private static final String[] ARGUMENTS = {
|
||||
public static final String[] ARGUMENTS = {
|
||||
"with space",
|
||||
" leading and trailing spaces ",
|
||||
"\"quoted\"",
|
||||
"\"quoted with spaces\"",
|
||||
"",
|
||||
@@ -55,8 +55,52 @@ public class GeneralCommandLineTest {
|
||||
"space \"and \"quotes\" inside",
|
||||
"\"space \"and \"quotes\" inside\"",
|
||||
"param2",
|
||||
"\\backslash",
|
||||
"trailing slash\\",
|
||||
(SystemInfo.isWindows ? "windows_sucks" : "two trailing slashes\\\\")
|
||||
"two trailing slashes\\\\",
|
||||
"trailing-slash\\",
|
||||
"two-trailing-slashes\\\\",
|
||||
"\"quoted slash\\\"",
|
||||
"\"quoted two slashes\\\\\"",
|
||||
"\"quoted-slash\\\"",
|
||||
"\"quoted-two-slashes\\\\\"",
|
||||
"some\ttab",
|
||||
"^% % %%% %\"%%",
|
||||
//"%PATH%",
|
||||
"^",
|
||||
"\\^",
|
||||
"^ ^^",
|
||||
"specials \\ & | > < ^",
|
||||
"carets: ^ ^^ ^^^ ^^^^",
|
||||
"caret escape ^\\ ^& ^| ^> ^< ^^",
|
||||
"caret escape2 ^^\\ ^^& ^^| ^^> ^^< ^^^",
|
||||
"&<>()@^|",
|
||||
"\"^\"",
|
||||
"\"^\"^\"",
|
||||
"\"^\"\"^^\"^^^",
|
||||
"\"^&<>(\")@^|\"",
|
||||
" < \" > ",
|
||||
" \" ^ \" ",
|
||||
" \" ^ \" ^ \" ",
|
||||
" \" ^ \" \" ^ ^\" ^^^ ",
|
||||
" \" ^ &< >( \" ) @ ^ | \" ",
|
||||
" < \" > ",
|
||||
"*",
|
||||
"\\*",
|
||||
"\"*\"",
|
||||
"*.*",
|
||||
"?",
|
||||
"???",
|
||||
"??????",
|
||||
"????????", // testData
|
||||
"??????????",
|
||||
"????????????",
|
||||
"??????????????",
|
||||
"????????????????",
|
||||
"??????????????????", // platform-tests.iml
|
||||
"\\?",
|
||||
"\"?\"",
|
||||
"*.???", // ^ the Xmas tree above is to catch at least one file matching those globs
|
||||
};
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection") private static final String UNICODE_RU = "Юникоде";
|
||||
@@ -141,10 +185,9 @@ public class GeneralCommandLineTest {
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void passingArgumentsToJavaApp() throws Exception {
|
||||
String[] args = ArrayUtil.mergeArrays(ARGUMENTS, "&<>()@^|", "\"&<>()@^|\"");
|
||||
Pair<GeneralCommandLine, File> command = makeHelperCommand(null, CommandTestHelper.ARG, args);
|
||||
Pair<GeneralCommandLine, File> command = makeHelperCommand(null, CommandTestHelper.ARG, ARGUMENTS);
|
||||
String output = execHelper(command);
|
||||
checkParamPassing(output, args);
|
||||
checkParamPassing(output, ARGUMENTS);
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
@@ -159,6 +202,21 @@ public class GeneralCommandLineTest {
|
||||
checkParamPassing(output, ARGUMENTS);
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void passingArgumentsToJavaAppThroughNestedWinShell() throws Exception {
|
||||
assumeTrue(SystemInfo.isWindows);
|
||||
|
||||
Pair<GeneralCommandLine, File> command = makeHelperCommand(null, CommandTestHelper.ARG, ARGUMENTS);
|
||||
String javaPath = command.first.getExePath();
|
||||
command.first.setExePath(ExecUtil.getWindowsShellName());
|
||||
command.first.getParametersList().prependAll("/D", "/C", "call",
|
||||
ExecUtil.getWindowsShellName(), "/D", "/C", "call",
|
||||
ExecUtil.getWindowsShellName(), "/D", "/C", "@call",
|
||||
javaPath);
|
||||
String output = execHelper(command);
|
||||
checkParamPassing(output, ARGUMENTS);
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void passingArgumentsToJavaAppThroughCmdScriptAndWinShell() throws Exception {
|
||||
assumeTrue(SystemInfo.isWindows);
|
||||
@@ -176,6 +234,38 @@ public class GeneralCommandLineTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void passingArgumentsToJavaAppThroughCmdScriptAndNestedWinShell() throws Exception {
|
||||
assumeTrue(SystemInfo.isWindows);
|
||||
|
||||
Pair<GeneralCommandLine, File> command = makeHelperCommand(null, CommandTestHelper.ARG);
|
||||
File script = ExecUtil.createTempExecutableScript("my script ", ".cmd", "@" + command.first.getCommandLineString() + " %*");
|
||||
try {
|
||||
GeneralCommandLine commandLine = createCommandLine(ExecUtil.getWindowsShellName(), "/D", "/C", "call",
|
||||
ExecUtil.getWindowsShellName(), "/D", "/C", "@call",
|
||||
ExecUtil.getWindowsShellName(), "/D", "/C", "call",
|
||||
script.getAbsolutePath());
|
||||
commandLine.addParameters(ARGUMENTS);
|
||||
String output = execHelper(pair(commandLine, command.second));
|
||||
checkParamPassing(output, ARGUMENTS);
|
||||
}
|
||||
finally {
|
||||
FileUtil.delete(script);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void passingArgumentsToEchoThroughWinShell() throws Exception {
|
||||
assumeTrue(SystemInfo.isWindows);
|
||||
|
||||
for (String argument : ARGUMENTS) {
|
||||
if (argument.trim().isEmpty()) continue; // would report "ECHO is on"
|
||||
GeneralCommandLine commandLine = new GeneralCommandLine(ExecUtil.getWindowsShellName(), "/D", "/C", "echo", argument);
|
||||
String output = execAndGetOutput(commandLine);
|
||||
assertEquals(commandLine.getPreparedCommandLine(), argument + "\n", output);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
public void unicodeParameters() throws Exception {
|
||||
assumeTrue(UNICODE != null);
|
||||
@@ -192,7 +282,7 @@ public class GeneralCommandLineTest {
|
||||
|
||||
String string = "http://localhost/wtf?a=b&c=d";
|
||||
String echo = ExecUtil.execAndReadLine(createCommandLine(ExecUtil.getWindowsShellName(), "/c", "echo", string));
|
||||
assertEquals('"' + string + '"', echo);
|
||||
assertEquals(string, echo);
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
@@ -201,14 +291,13 @@ public class GeneralCommandLineTest {
|
||||
|
||||
String scriptPrefix = "my_script";
|
||||
for (String scriptExt : new String[]{".cmd", ".bat"}) {
|
||||
File script = ExecUtil.createTempExecutableScript(scriptPrefix, scriptExt, "@echo %1\n");
|
||||
String param = "a&b";
|
||||
GeneralCommandLine commandLine = createCommandLine(script.getAbsolutePath(), param);
|
||||
String text = commandLine.getPreparedCommandLine(Platform.WINDOWS);
|
||||
assertEquals(commandLine.getExePath() + "\n" + StringUtil.wrapWithDoubleQuote(param), text);
|
||||
File script = ExecUtil.createTempExecutableScript(scriptPrefix, scriptExt, "@echo %*\n");
|
||||
try {
|
||||
String output = execAndGetOutput(commandLine);
|
||||
assertEquals(StringUtil.wrapWithDoubleQuote(param), output.trim());
|
||||
for (String argument : ARGUMENTS) {
|
||||
GeneralCommandLine commandLine = createCommandLine(script.getAbsolutePath(), GeneralCommandLine.inescapableQuote(argument));
|
||||
String output = execAndGetOutput(commandLine);
|
||||
assertEquals(commandLine.getPreparedCommandLine(), StringUtil.wrapWithDoubleQuote(argument), output.trim());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
FileUtil.delete(script);
|
||||
@@ -223,7 +312,7 @@ public class GeneralCommandLineTest {
|
||||
String param = "a&b";
|
||||
GeneralCommandLine commandLine = createCommandLine(ExecUtil.getWindowsShellName(), "/D", "/C", "echo", param);
|
||||
String output = execAndGetOutput(commandLine);
|
||||
assertEquals(StringUtil.wrapWithDoubleQuote(param), output.trim());
|
||||
assertEquals(param, output.trim());
|
||||
}
|
||||
|
||||
@Test(timeout = 60000)
|
||||
@@ -301,8 +390,11 @@ public class GeneralCommandLineTest {
|
||||
ProcessOutput output = ExecUtil.execAndGetOutput(commandLine);
|
||||
int ec = output.getExitCode();
|
||||
if (ec != 0) {
|
||||
fail("Command:\n" + commandLine.getCommandLineString() + "\nStdOut:\n" + output.getStdout() + "\nStdErr:\n" + output.getStderr());
|
||||
fail("Command:\n" + commandLine.getPreparedCommandLine() +
|
||||
"\nStdOut:\n" + output.getStdout() +
|
||||
"\nStdErr:\n" + output.getStderr());
|
||||
}
|
||||
assertTrue(output.getStderr(), output.getStderr().isEmpty());
|
||||
return output.getStdout();
|
||||
}
|
||||
|
||||
@@ -396,4 +488,4 @@ public class GeneralCommandLineTest {
|
||||
fail("% missed: " + pctMissed + ", missed: " + missed + ", passed: " + lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,32 @@
|
||||
package com.intellij.execution;
|
||||
|
||||
import com.intellij.openapi.util.io.FileUtilRt;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.intellij.openapi.util.text.StringUtil.*;
|
||||
|
||||
public class CommandLineUtil {
|
||||
private static final char SPECIAL_QUOTE = '\uEFEF';
|
||||
private static final String WIN_SHELL_SPECIALS = "&<>()@^|";
|
||||
private static final char INESCAPABLE_QUOTE = '\uEFEF'; // a random char, which is unlikely to encounter in an argument
|
||||
|
||||
private static final Pattern WIN_BACKSLASHES_PRECEDING_QUOTE = Pattern.compile("(\\\\+)(?=\"|$)");
|
||||
private static final Pattern WIN_CARET_SPECIAL = Pattern.compile("[&<>()@^|]");
|
||||
private static final Pattern WIN_QUOTE_SPECIAL = Pattern.compile("[ \t\"*?]");
|
||||
private static final Pattern WIN_QUIET_COMMAND = Pattern.compile("((?:@\\s*)++)(.*)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final char Q = '\"';
|
||||
private static final String QQ = "\"\"";
|
||||
|
||||
@NotNull
|
||||
public static String specialQuote(@NotNull String parameter) {
|
||||
return quote(parameter, SPECIAL_QUOTE);
|
||||
return quote(parameter, INESCAPABLE_QUOTE);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -51,47 +62,399 @@ public class CommandLineUtil {
|
||||
|
||||
commandLine.add(FileUtilRt.toSystemDependentName(command, platform.fileSeparator));
|
||||
|
||||
boolean isWindows = platform == Platform.WINDOWS;
|
||||
boolean winShell = isWindows && isWinShell(command);
|
||||
|
||||
for (String parameter : parameters) {
|
||||
if (isWindows) {
|
||||
if (parameter.contains("\"")) {
|
||||
parameter = StringUtil.replace(parameter, "\"", "\\\"");
|
||||
}
|
||||
else if (parameter.isEmpty()) {
|
||||
parameter = "\"\"";
|
||||
if (platform != Platform.WINDOWS) {
|
||||
for (String parameter : parameters) {
|
||||
if (isQuoted(parameter, INESCAPABLE_QUOTE)) {
|
||||
// TODO do we need that on non-Windows? M.b. just remove these quotes? -- Eldar
|
||||
parameter = quote(unquoteString(parameter, INESCAPABLE_QUOTE), Q);
|
||||
}
|
||||
commandLine.add(parameter);
|
||||
}
|
||||
|
||||
if (winShell && hasWinShellSpecialChars(parameter)) {
|
||||
parameter = quote(parameter, SPECIAL_QUOTE);
|
||||
}
|
||||
|
||||
if (isQuoted(parameter, SPECIAL_QUOTE)) {
|
||||
parameter = quote(parameter.substring(1, parameter.length() - 1), '"');
|
||||
}
|
||||
|
||||
commandLine.add(parameter);
|
||||
}
|
||||
else {
|
||||
addToWindowsCommandLine(parameters, commandLine);
|
||||
}
|
||||
|
||||
return commandLine;
|
||||
}
|
||||
|
||||
/*
|
||||
* Windows command line escaping rules are tricky and poorly documented, so the code below might require a bit of explanation.
|
||||
*
|
||||
* Here're the rules that define our implementation, and some peculiarities to know:
|
||||
*
|
||||
* *** On Windows, there's no ARGV concept at the OS level; all parameters are passed as a single command line
|
||||
* string, which is only parsed into the ARGV by CRT if needed (this is used by the most applications).
|
||||
*
|
||||
* CRT parsing rules are relatively simple:
|
||||
*
|
||||
* - Parameters are delimited using spaces: [foo] [bar] [baz] -> [foo bar baz]
|
||||
* - Whitespaces are escaped using double quotes: [foo bar] [baz] -> ["foo bar" baz]
|
||||
* - Double-quotes are escaped using backslashes: [foo bar] ["baz"] -> ["foo bar" \"baz\"]
|
||||
*
|
||||
* - Backslashes are treated literally unless they precede a double quote, otherwise they need to be
|
||||
* backslash-escaped as well:
|
||||
*
|
||||
* [C:\Program Files\] ["backslash quote\"]
|
||||
*
|
||||
* -> ["C:\Program Files\\" "\"backslash quote\\\""]
|
||||
*
|
||||
*
|
||||
* *** In case a command line is wrapped using CMD.EXE call (that is, `cmd /d /c call executable args...`, which
|
||||
* is quite common), additional escaping rules apply.
|
||||
*
|
||||
* CMD.EXE treat few chars in a special way (piping, command chaining, etc.), these are: [&<>()@|^].
|
||||
* The CMD.EXE command line parser has two means of escaping these special chars: quote flag and caret-escaping.
|
||||
* The main rules in a nutshell are:
|
||||
*
|
||||
* - A quote ["] toggles the quote flag; while the quote flag is active, the special chars are no longer
|
||||
* special. Note that quotes themselves are NOT removed at this stage: ["<^.^>"] => ["<^.^>"]
|
||||
* The quotes might be removed by the CRT command line parser later on when reconstructing the ARGV,
|
||||
* if the executable opts to.
|
||||
*
|
||||
* - A char following a caret [^] has no special meaning, the caret itself is removed: [^<^^.^^^>] => [<^.^>]
|
||||
*
|
||||
* These rules, in turn, have special cases that affect the implementation, see below.
|
||||
*
|
||||
*
|
||||
* *** [CMD] As already mentioned, the CMD special chars [&<>()@|^] are sensitive to the quote flag, which is toggled
|
||||
* whenever the command line parser encounters a quote, no matter whether backslash-escaped or regular one:
|
||||
*
|
||||
* [foo "bar" baz]
|
||||
*
|
||||
* -> ["foo \"bar\" baz"] # enclosed in quotes due to whitespaces inside for CRT
|
||||
* # quote flag:
|
||||
* [ ^^^^^ ^^^^ ] # ON: [&<>()@|^] lose special meaning
|
||||
* [ ^^^^ ] # OFF: [&<>()@|^] must be ^-escaped
|
||||
*
|
||||
* This gets even more confusing when dealing with caret-escaping state across multiple arguments in case some
|
||||
* of them have odd number of quotes:
|
||||
*
|
||||
* [C:\Program Files\...] ["] [f o] [b"r]
|
||||
*
|
||||
* -> ["C:\Program Files\..." \" "f o" b\"r]
|
||||
* # quote flag:
|
||||
* [ ^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^ ] # ON: [&<>()@|^] lose special meaning
|
||||
* [ ^^^^ ^^^ ^] # OFF: [&<>()@|^] must be ^-escaped
|
||||
*
|
||||
* However, this is a totally valid case considering that arguments are passed as a single command line string
|
||||
* under the hood. Anyway, the point is, we need to count all the quotes in order to properly escape the special
|
||||
* chars. In the following sections we describe our escaping approach w.r.t. the quote flag.
|
||||
*
|
||||
*
|
||||
* *** [CMD] A caret [^] is always ^-escaped with the quote flag OFF:
|
||||
*
|
||||
* [^] # original value
|
||||
* -> [^^] # escaping
|
||||
* -> [^] # execution
|
||||
*
|
||||
* Why not just use a caret within quotes ["^"] (i.e. with the quote flag ON) here?
|
||||
*
|
||||
* Because of the way CMD.EXE handles the CALL command. Due to the nature of the CALL command, CMD.EXE has
|
||||
* to process the ^-escaped special chars twice. In order to preserve the number of carets, which would be
|
||||
* halved after the second expansion otherwise, it duplicates all the carets behind the scenes beforehand:
|
||||
*
|
||||
* [^] # original value
|
||||
* -> [^^] # escaping
|
||||
* -> [^^^^] # under the hood: CALL caret doubling
|
||||
* -> [^^] # under the hood: CALL second expansion
|
||||
* -> [^] # execution
|
||||
*
|
||||
* Unfortunately it blindly doubles all carets, with no regard to the quote flag. But a quoted caret is not
|
||||
* treated as an escape, hence it's not consumed. These carets appear duplicated to a process being called:
|
||||
*
|
||||
* [^] # original value
|
||||
* -> ["^"] # escaping
|
||||
* -> ["^^"] # under the hood: CALL caret doubling
|
||||
* -> ["^^"] # under the hood: CALL second expansion
|
||||
* -> [^^] # execution (oops...)
|
||||
*
|
||||
*
|
||||
* *** [CMD] The rest special chars ([&<>()@|] except the caret) are quoted instead (i.e. emitted with the quote
|
||||
* flag ON) instead of being ^-escaped (with the only exception of ECHO command, see below):
|
||||
*
|
||||
* [&] # original value
|
||||
* -> ["&"] # escaping
|
||||
* -> [&] # execution
|
||||
*
|
||||
* Why not use ^-escaping, just like for the caret itself? Again, because of the CALL caret doubling:
|
||||
*
|
||||
* [&] # original value
|
||||
* -> [^&] # escaping
|
||||
* -> [^^&] # under the hood: CALL caret doubling
|
||||
* -> [^] # under the hood: CALL second expansion (stray [&] may lead to errors)
|
||||
* -> [] # execution (oops...)
|
||||
*
|
||||
* *** [CMD] [ECHO] The ECHO command doesn't use CRT for parsing the ARGV, so only the common rules for CMD parameters
|
||||
* apply. That is, we ^-escape all the special chars [&<>()@|^].
|
||||
*
|
||||
*
|
||||
* Useful links:
|
||||
*
|
||||
* * https://ss64.com/nt/syntax-esc.html Syntax : Escape Characters, Delimiters and Quotes
|
||||
* * http://stackoverflow.com/a/4095133/545027 How does the Windows Command Interpreter (CMD.EXE) parse scripts?
|
||||
*/
|
||||
|
||||
private static void addToWindowsCommandLine(@NotNull List<String> parameters,
|
||||
@NotNull List<String> commandLine) {
|
||||
String command = commandLine.get(0);
|
||||
|
||||
boolean isCmdParam = isWinShell(command);
|
||||
int cmdInvocationDepth = isWinShellScript(command) ? 2 : isCmdParam ? 1 : 0;
|
||||
|
||||
QuoteFlag quoteFlag = new QuoteFlag(command);
|
||||
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
String parameter = parameters.get(i);
|
||||
|
||||
parameter = unquoteString(parameter, INESCAPABLE_QUOTE);
|
||||
boolean inescapableQuoting = !parameter.equals(parameters.get(i));
|
||||
|
||||
if (parameter.isEmpty()) {
|
||||
commandLine.add(QQ);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCmdParam && parameter.startsWith("/") && parameter.length() == 2) {
|
||||
commandLine.add(parameter);
|
||||
continue;
|
||||
}
|
||||
|
||||
String parameterPrefix = "";
|
||||
if (isCmdParam) {
|
||||
Matcher m = WIN_QUIET_COMMAND.matcher(parameter);
|
||||
if (m.matches()) {
|
||||
parameterPrefix = m.group(1); // @...
|
||||
parameter = m.group(2);
|
||||
}
|
||||
|
||||
if (parameter.equalsIgnoreCase("echo")) {
|
||||
// no further quoting, only ^-escape and wrap the whole "echo ..." into double quotes
|
||||
String parametersJoin = join(ContainerUtil.subList(parameters, i), " ");
|
||||
parameter = escapeParameter(parametersJoin, quoteFlag.toggle(), cmdInvocationDepth, false);
|
||||
commandLine.add(parameter); // prefix is already included
|
||||
break;
|
||||
}
|
||||
|
||||
if (!parameter.equalsIgnoreCase("call")) {
|
||||
isCmdParam = isWinShell(parameter);
|
||||
if (isCmdParam || isWinShellScript(parameter)) {
|
||||
cmdInvocationDepth++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdInvocationDepth > 0 && !isCmdParam || inescapableQuoting) {
|
||||
parameter = escapeParameter(parameter, quoteFlag, cmdInvocationDepth, !inescapableQuoting);
|
||||
}
|
||||
else {
|
||||
parameter = backslashEscapeQuotes(parameter);
|
||||
}
|
||||
|
||||
commandLine.add(parameterPrefix + parameter);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Contract(pure = true)
|
||||
public static String escapeParameterOnWindows(@NotNull String s, boolean isWinShell) {
|
||||
boolean hadLineBreaks = !s.equals(s = convertLineSeparators(s, ""));
|
||||
if (s.isEmpty()) return QQ;
|
||||
String ret = isWinShell ? escapeParameter(s, new QuoteFlag(hadLineBreaks), 1, true) : backslashEscapeQuotes(s);
|
||||
return hadLineBreaks ? quote(ret, Q) : ret;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Contract(pure = true)
|
||||
private static String escapeParameter(@NotNull String s,
|
||||
@NotNull QuoteFlag quoteFlag,
|
||||
int cmdInvocationDepth,
|
||||
boolean escapeQuotingInside) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final String escapingCarets = repeatSymbol('^', (1 << cmdInvocationDepth) - 1);
|
||||
|
||||
return (escapeQuotingInside ? quoteEscape(sb, s, quoteFlag, escapingCarets)
|
||||
: caretEscape(sb, s, quoteFlag, escapingCarets)).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a parameter for passing it to CMD.EXE, that is only ^-escape and wrap it with double quotes,
|
||||
* but do not touch any double quotes or backslashes inside.
|
||||
*
|
||||
* @param escapingCarets 2^n-1 carets for escaping the special chars
|
||||
* @return sb
|
||||
*/
|
||||
@NotNull
|
||||
private static StringBuilder caretEscape(@NotNull StringBuilder sb,
|
||||
@NotNull String s,
|
||||
@NotNull QuoteFlag quoteFlag,
|
||||
@NotNull String escapingCarets) {
|
||||
quoteFlag.toggle();
|
||||
sb.append(Q);
|
||||
|
||||
int lastPos = 0;
|
||||
final Matcher m = WIN_CARET_SPECIAL.matcher(s);
|
||||
while (m.find()) {
|
||||
quoteFlag.consume(s, lastPos, m.start());
|
||||
sb.append(s, lastPos, m.start());
|
||||
|
||||
if (!quoteFlag.isEnabled()) sb.append(escapingCarets);
|
||||
sb.append(m.group());
|
||||
|
||||
lastPos = m.end();
|
||||
}
|
||||
quoteFlag.consume(s, lastPos, s.length());
|
||||
sb.append(s, lastPos, s.length());
|
||||
|
||||
quoteFlag.toggle();
|
||||
return sb.append(Q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a parameter for passing it to Windows application through CMD.EXE, that is ^-escape + quote.
|
||||
*
|
||||
* @param escapingCarets 2^n-1 carets for escaping the special chars
|
||||
* @return sb
|
||||
*/
|
||||
@NotNull
|
||||
private static StringBuilder quoteEscape(@NotNull StringBuilder sb,
|
||||
@NotNull String s,
|
||||
@NotNull QuoteFlag quoteFlag,
|
||||
@NotNull String escapingCarets) {
|
||||
int lastPos = 0;
|
||||
final Matcher m = WIN_CARET_SPECIAL.matcher(s);
|
||||
while (m.find()) {
|
||||
quoteFlag.consume(s, lastPos, m.start());
|
||||
appendQuoted(sb, s.substring(lastPos, m.start()));
|
||||
|
||||
String specialText = m.group();
|
||||
boolean isCaret = specialText.equals("^");
|
||||
if (isCaret) specialText = escapingCarets + specialText; // only a caret is escaped using carets
|
||||
if (isCaret == quoteFlag.isEnabled()) {
|
||||
// a caret must be always outside quotes: close a quote temporarily, put a caret, reopen a quote
|
||||
// rest special chars are always inside: quote and append them as usual
|
||||
appendQuoted(sb, specialText); // works for both cases
|
||||
}
|
||||
else {
|
||||
sb.append(specialText);
|
||||
}
|
||||
|
||||
lastPos = m.end();
|
||||
}
|
||||
quoteFlag.consume(s, lastPos, s.length());
|
||||
appendQuoted(sb, s.substring(lastPos));
|
||||
|
||||
// JDK ProcessBuilder implementation on Windows checks each argument to contain whitespaces and encloses it in
|
||||
// double quotes unless it's already quoted. Since our escaping logic is more complicated, we may end up with
|
||||
// something like [^^"foo bar"], which is not a quoted string, strictly speaking; it would require additional
|
||||
// quotes from the JDK's point of view, which in turn ruins the quoting and caret state inside the string.
|
||||
//
|
||||
// Here, we prepend and/or append a [""] token (2 quotes to preserve the parity), if necessary, to make the result
|
||||
// look like a properly quoted string:
|
||||
//
|
||||
// [^^"foo bar"] -> [""^^"foo bar"] # starts and ends with quotes
|
||||
//
|
||||
if (!isQuoted(sb, Q) && indexOfAny(sb, " \t") >= 0) {
|
||||
if (sb.charAt(0) != Q) sb.insert(0, QQ);
|
||||
if (sb.charAt(sb.length() - 1) != Q) sb.append(QQ);
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the string to the buffer, quoting the former if necessary.
|
||||
*
|
||||
* @return sb
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
private static StringBuilder appendQuoted(@NotNull StringBuilder sb, @NotNull String s) {
|
||||
if (s.isEmpty()) return sb;
|
||||
|
||||
s = backslashEscapeQuotes(s);
|
||||
if (WIN_CARET_SPECIAL.matcher(s).find()) s = quote(s, Q);
|
||||
|
||||
// Can't just concatenate two quoted strings, like ["foo"] and ["bar"],
|
||||
// the two quotes inside would be treated as ["] upon unescaping, that is:
|
||||
//
|
||||
// ["foo""bar"] -> [foo"bar] # [""""] -> ["] (oops...)
|
||||
//
|
||||
int numTrailingBackslashes = removeClosingQuote(sb);
|
||||
if (numTrailingBackslashes < 0) {
|
||||
// sb was not quoted at its end
|
||||
return sb.append(s);
|
||||
}
|
||||
|
||||
s = unquoteString(s, Q);
|
||||
|
||||
if (WIN_BACKSLASHES_PRECEDING_QUOTE.matcher(s).matches()) {
|
||||
// those trailing backslashes left in the buffer (if any) are going to precede a quote, double them
|
||||
repeatSymbol(sb, '\\', numTrailingBackslashes);
|
||||
}
|
||||
return sb.append(s)
|
||||
.append(Q);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Contract(pure = true)
|
||||
private static String backslashEscapeQuotes(@NotNull String s) {
|
||||
assert !s.isEmpty();
|
||||
|
||||
String result = WIN_BACKSLASHES_PRECEDING_QUOTE.matcher(s)
|
||||
.replaceAll("$1$1") // duplicate trailing backslashes and those preceding a double quote
|
||||
.replace("\"", "\\\""); // backslash-escape all double quotes
|
||||
|
||||
if (!result.equals(s) || WIN_QUOTE_SPECIAL.matcher(s).find()) {
|
||||
result = quote(result, Q);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the buffer ends with a double-quoted token, "reopen" it by deleting the closing quote.
|
||||
*
|
||||
* @param sb the buffer being modified
|
||||
* @return -1 if the buffer doesn't end with a quotation (in this case it's left unmodified),
|
||||
* otherwise a number of trailing backslashes left in the buffer, if any
|
||||
*/
|
||||
private static int removeClosingQuote(@NotNull StringBuilder sb) {
|
||||
if (sb.length() < 2 || sb.charAt(sb.length() - 1) != Q) return -1;
|
||||
|
||||
// Strip the closing quote and halve the number of trailing backslashes (if any):
|
||||
// they no more precede a double quote, hence lose their special treatment during unescaping.
|
||||
sb.setLength(sb.length() - 1);
|
||||
int numTrailingBackslashes = sb.length() - trimTrailing(sb, '\\').length();
|
||||
repeatSymbol(sb, '\\', numTrailingBackslashes / 2);
|
||||
|
||||
if (numTrailingBackslashes % 2 == 1) {
|
||||
// retreat, it was a backslash-escaped quote, restore it
|
||||
repeatSymbol(sb, '\\', numTrailingBackslashes / 2 + 1);
|
||||
sb.append(Q);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return numTrailingBackslashes / 2;
|
||||
}
|
||||
|
||||
|
||||
private static boolean isWinShell(@NotNull String command) {
|
||||
return endsWithIgnoreCase(command, ".cmd") || endsWithIgnoreCase(command, ".bat") ||
|
||||
"cmd".equalsIgnoreCase(command) || "cmd.exe".equalsIgnoreCase(command);
|
||||
return "cmd".equalsIgnoreCase(command) || "cmd.exe".equalsIgnoreCase(command);
|
||||
}
|
||||
|
||||
private static boolean isWinShellScript(@NotNull String command) {
|
||||
return endsWithIgnoreCase(command, ".cmd") || endsWithIgnoreCase(command, ".bat");
|
||||
}
|
||||
|
||||
private static boolean endsWithIgnoreCase(@NotNull String str, @NotNull String suffix) {
|
||||
return str.regionMatches(true, str.length() - suffix.length(), suffix, 0, suffix.length());
|
||||
}
|
||||
|
||||
private static String quote(String s, char ch) {
|
||||
@NotNull
|
||||
private static String quote(@NotNull String s, char ch) {
|
||||
return !isQuoted(s, ch) ? ch + s + ch : s;
|
||||
}
|
||||
|
||||
private static boolean isQuoted(String s, char ch) {
|
||||
private static boolean isQuoted(@NotNull CharSequence s, char ch) {
|
||||
return s.length() >= 2 && s.charAt(0) == ch && s.charAt(s.length() - 1) == ch;
|
||||
}
|
||||
|
||||
@@ -100,7 +463,7 @@ public class CommandLineUtil {
|
||||
public static String extractPresentableName(@NotNull String commandLine) {
|
||||
String executable = commandLine.trim();
|
||||
|
||||
List<String> words = StringUtil.splitHonorQuotes(executable, ' ');
|
||||
List<String> words = splitHonorQuotes(executable, ' ');
|
||||
String execName;
|
||||
List<String> args;
|
||||
if (words.isEmpty()) {
|
||||
@@ -113,13 +476,49 @@ public class CommandLineUtil {
|
||||
}
|
||||
|
||||
if (VERBOSE_COMMAND_LINE_MODE) {
|
||||
return StringUtil.shortenPathWithEllipsis(execName + " " + StringUtil.join(args, " "), 250);
|
||||
return shortenPathWithEllipsis(execName + " " + join(args, " "), 250);
|
||||
}
|
||||
|
||||
return new File(StringUtil.unquoteString(execName)).getName();
|
||||
return new File(unquoteString(execName)).getName();
|
||||
}
|
||||
|
||||
public static boolean hasWinShellSpecialChars(String parameter) {
|
||||
return StringUtil.containsAnyChar(parameter, WIN_SHELL_SPECIALS);
|
||||
return WIN_CARET_SPECIAL.matcher(parameter).find();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts quote parity needed to ^-escape special chars on Windows properly.
|
||||
*/
|
||||
private static class QuoteFlag {
|
||||
private boolean myValue;
|
||||
|
||||
public QuoteFlag(boolean value) {
|
||||
myValue = value;
|
||||
}
|
||||
|
||||
private QuoteFlag(@NotNull CharSequence s) {
|
||||
consume(s);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return myValue;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public QuoteFlag toggle() {
|
||||
myValue = !myValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public QuoteFlag consume(@NotNull CharSequence s) {
|
||||
return consume(s, 0, s.length());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public QuoteFlag consume(@NotNull CharSequence s, int start, int end) {
|
||||
myValue ^= countChars(s, Q, start, end, false) % 2 != 0; // count all, no matter whether backslash-escaped or not
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ public enum GitVersionSpecialty {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated on Windows, quotes are now added automatically whenever necessary on the GeneralCommandLine level
|
||||
*/
|
||||
@Deprecated
|
||||
NEEDS_QUOTES_IN_STASH_NAME {
|
||||
@Override
|
||||
public boolean existsIn(@NotNull GitVersion version) {
|
||||
|
||||
@@ -17,7 +17,6 @@ package git4idea.ui;
|
||||
|
||||
import com.intellij.CommonBundle;
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.execution.configurations.GeneralCommandLine;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationListener;
|
||||
import com.intellij.openapi.application.AccessToken;
|
||||
@@ -43,10 +42,8 @@ import com.intellij.ui.DocumentAdapter;
|
||||
import com.intellij.util.Consumer;
|
||||
import git4idea.GitRevisionNumber;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.commands.*;
|
||||
import git4idea.config.GitVersionSpecialty;
|
||||
import git4idea.i18n.GitBundle;
|
||||
import git4idea.merge.GitConflictResolver;
|
||||
import git4idea.repo.GitRepository;
|
||||
@@ -92,14 +89,12 @@ public class GitUnstashDialog extends DialogWrapper {
|
||||
private final HashSet<String> myBranches = new HashSet<>();
|
||||
|
||||
private final Project myProject;
|
||||
private GitVcs myVcs;
|
||||
private static final Logger LOG = Logger.getInstance(GitUnstashDialog.class);
|
||||
|
||||
public GitUnstashDialog(final Project project, final List<VirtualFile> roots, final VirtualFile defaultRoot) {
|
||||
super(project, true);
|
||||
setModal(false);
|
||||
myProject = project;
|
||||
myVcs = GitVcs.getInstance(project);
|
||||
setTitle(GitBundle.getString("unstash.title"));
|
||||
setOKButtonText(GitBundle.getString("unstash.button.apply"));
|
||||
setCancelButtonText(CommonBundle.getCloseButtonText());
|
||||
@@ -172,8 +167,7 @@ public class GitUnstashDialog extends DialogWrapper {
|
||||
|
||||
private GitSimpleHandler dropHandler(String stash) {
|
||||
GitSimpleHandler h = new GitSimpleHandler(myProject, getGitRoot(), GitCommand.STASH);
|
||||
h.addParameters("drop");
|
||||
addStashParameter(h, stash);
|
||||
h.addParameters("drop", stash);
|
||||
return h;
|
||||
}
|
||||
});
|
||||
@@ -185,8 +179,7 @@ public class GitUnstashDialog extends DialogWrapper {
|
||||
try {
|
||||
GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.REV_LIST);
|
||||
h.setSilent(true);
|
||||
h.addParameters("--timestamp", "--max-count=1");
|
||||
addStashParameter(h, selectedStash);
|
||||
h.addParameters("--timestamp", "--max-count=1", selectedStash);
|
||||
h.endOptions();
|
||||
final String output = h.run();
|
||||
resolvedStash = GitRevisionNumber.parseRevlistOutputAsRevisionNumber(h, output).asString();
|
||||
@@ -202,18 +195,6 @@ public class GitUnstashDialog extends DialogWrapper {
|
||||
updateDialogState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@code stash@{x}} parameter to the handler, quotes it if needed.
|
||||
*/
|
||||
private void addStashParameter(@NotNull GitHandler handler, @NotNull String stash) {
|
||||
if (GitVersionSpecialty.NEEDS_QUOTES_IN_STASH_NAME.existsIn(myVcs.getVersion())) {
|
||||
handler.addParameters(GeneralCommandLine.inescapableQuote(stash));
|
||||
}
|
||||
else {
|
||||
handler.addParameters(stash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state dialog depending on the current state of the fields
|
||||
*/
|
||||
@@ -312,7 +293,7 @@ public class GitUnstashDialog extends DialogWrapper {
|
||||
h.addParameters("branch", branch);
|
||||
}
|
||||
String selectedStash = getSelectedStash().getStash();
|
||||
addStashParameter(h, selectedStash);
|
||||
h.addParameters(selectedStash);
|
||||
return h;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user