/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.execution.configurations;
import com.intellij.execution.CommandLineUtil;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Platform;
import com.intellij.execution.process.ProcessNotCreatedException;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.util.EnvironmentUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import consulo.annotations.DeprecationInfo;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
/**
* OS-independent way of executing external processes with complex parameters.
* <p>
* Main idea of the class is to accept parameters "as-is", just as they should look to an external process, and quote/escape them
* as required by the underlying platform.
*
* @see com.intellij.execution.process.OSProcessHandler
*/
public class GeneralCommandLine implements UserDataHolder {
private static final Logger LOG = Logger.getInstance("#com.intellij.execution.configurations.GeneralCommandLine");
/**
* Determines the scope of a parent environment passed to a child process.
* <p>
* {@code NONE} means a child process will receive an empty environment. <br/>
* {@code SYSTEM} will provide it with the same environment as an IDE. <br/>
* {@code CONSOLE} provides the child with a similar environment as if it was launched from, well, a console.
* On OS X, a console environment is simulated (see {@link EnvironmentUtil#getEnvironmentMap()} for reasons it's needed
* and details on how it works). On Windows and Unix hosts, this option is no different from {@code SYSTEM}
* since there is no drastic distinction in environment between GUI and console apps.
*/
public enum ParentEnvironmentType {
NONE, SYSTEM, CONSOLE
}
private String myExePath = null;
private File myWorkDirectory = null;
private final Map<String, String> myEnvParams = new MyTHashMap();
private ParentEnvironmentType myParentEnvironmentType = ParentEnvironmentType.CONSOLE;
private final ParametersList myProgramParams = new ParametersList();
private Charset myCharset = CharsetToolkit.getDefaultSystemCharset();
private boolean myRedirectErrorStream = false;
private Map<Object, Object> myUserData = null;
public GeneralCommandLine() {
}
public GeneralCommandLine(@NotNull String... command) {
this(Arrays.asList(command));
}
public GeneralCommandLine(@NotNull List<String> command) {
int size = command.size();
if (size > 0) {
setExePath(command.get(0));
if (size > 1) {
addParameters(command.subList(1, size));
}
}
}
public String getExePath() {
return myExePath;
}
@NotNull
public GeneralCommandLine withExePath(@NotNull String exePath) {
myExePath = exePath.trim();
return this;
}
public void setExePath(@NotNull String exePath) {
withExePath(exePath);
}
public File getWorkDirectory() {
return myWorkDirectory;
}
@NotNull
public GeneralCommandLine withWorkDirectory(@Nullable String path) {
return withWorkDirectory(path != null ? new File(path) : null);
}
@NotNull
public GeneralCommandLine withWorkDirectory(@Nullable File workDirectory) {
myWorkDirectory = workDirectory;
return this;
}
public void setWorkDirectory(@Nullable String path) {
withWorkDirectory(path);
}
public void setWorkDirectory(@Nullable File workDirectory) {
withWorkDirectory(workDirectory);
}
/**
* Note: the map returned is forgiving to passing null values into putAll().
*/
@NotNull
public Map<String, String> getEnvironment() {
return myEnvParams;
}
@NotNull
public GeneralCommandLine withEnvironment(@Nullable Map<String, String> environment) {
if (environment != null) {
getEnvironment().putAll(environment);
}
return this;
}
@Deprecated
@DeprecationInfo(value = "Use #getParentEnvironmentType()", until = "3.0")
public boolean isPassParentEnvironment() {
return myParentEnvironmentType != ParentEnvironmentType.NONE;
}
@NotNull
@Deprecated
@DeprecationInfo(value = "Use #withParentEnvironmentType(ParentEnvironmentType)", until = "3.0")
public GeneralCommandLine withPassParentEnvironment(boolean passParentEnvironment) {
withParentEnvironmentType(passParentEnvironment ? ParentEnvironmentType.CONSOLE : ParentEnvironmentType.NONE);
return this;
}
@NotNull
@Deprecated
@DeprecationInfo(value = "Use #withParentEnvironmentType(ParentEnvironmentType)", until = "3.0")
public void setPassParentEnvironment(boolean passParentEnvironment) {
withPassParentEnvironment(passParentEnvironment);
}
@NotNull
public ParentEnvironmentType getParentEnvironmentType() {
return myParentEnvironmentType;
}
@NotNull
public GeneralCommandLine withParentEnvironmentType(@NotNull ParentEnvironmentType type) {
myParentEnvironmentType = type;
return this;
}
/**
* @return unmodifiable map of the parent environment, that will be passed to the process if isPassParentEnvironment() == true
*/
/**
* Returns an environment that will be inherited by a child process.
* @see #getEffectiveEnvironment()
*/
@NotNull
public Map<String, String> getParentEnvironment() {
switch (myParentEnvironmentType) {
case SYSTEM:
return System.getenv();
case CONSOLE:
return EnvironmentUtil.getEnvironmentMap();
default:
return Collections.emptyMap();
}
}
/**
* Returns an environment as seen by a child process,
* that is the {@link #getEnvironment() environment} merged with the {@link #getParentEnvironment() parent} one.
*/
@NotNull
public Map<String, String> getEffectiveEnvironment() {
MyTHashMap env = new MyTHashMap();
setupEnvironment(env);
return env;
}
public void addParameters(String... parameters) {
for (String parameter : parameters) {
addParameter(parameter);
}
}
public void addParameters(@NotNull List<String> parameters) {
for (String parameter : parameters) {
addParameter(parameter);
}
}
public void addParameter(@NotNull String parameter) {
myProgramParams.add(parameter);
}
@NotNull
public GeneralCommandLine withParameters(@NotNull String... parameters) {
for (String parameter : parameters) addParameter(parameter);
return this;
}
@NotNull
public GeneralCommandLine withParameters(@NotNull List<String> parameters) {
for (String parameter : parameters) addParameter(parameter);
return this;
}
public ParametersList getParametersList() {
return myProgramParams;
}
@NotNull
public Charset getCharset() {
return myCharset;
}
@NotNull
public GeneralCommandLine withCharset(@NotNull Charset charset) {
myCharset = charset;
return this;
}
public void setCharset(@NotNull Charset charset) {
withCharset(charset);
}
public boolean isRedirectErrorStream() {
return myRedirectErrorStream;
}
@NotNull
public GeneralCommandLine withRedirectErrorStream(boolean redirectErrorStream) {
myRedirectErrorStream = redirectErrorStream;
return this;
}
public void setRedirectErrorStream(boolean redirectErrorStream) {
withRedirectErrorStream(redirectErrorStream);
}
/**
* Returns string representation of this command line.<br/>
* Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
*
* @return single-string representation of this command line.
*/
public String getCommandLineString() {
return getCommandLineString(null);
}
/**
* Returns string representation of this command line.<br/>
* Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
*
* @param exeName use this executable name instead of given by {@link #setExePath(String)}
* @return single-string representation of this command line.
*/
public String getCommandLineString(@Nullable String exeName) {
return ParametersList.join(getCommandLineList(exeName));
}
public List<String> getCommandLineList(@Nullable String exeName) {
List<String> commands = new ArrayList<String>();
if (exeName != null) {
commands.add(exeName);
}
else if (myExePath != null) {
commands.add(myExePath);
}
else {
commands.add("<null>");
}
commands.addAll(myProgramParams.getList());
return commands;
}
/**
* Prepares command (quotes and escapes all arguments) and returns it as a newline-separated list
* (suitable e.g. for passing in an environment variable).
*
* @param platform a target platform
* @return command as a newline-separated list.
*/
@NotNull
public String getPreparedCommandLine(@NotNull Platform platform) {
String exePath = myExePath != null ? myExePath : "";
return StringUtil.join(CommandLineUtil.toCommandLine(exePath, myProgramParams.getList(), platform), "\n");
}
@NotNull
public Process createProcess() throws ExecutionException {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing [" + getCommandLineString() + "]");
LOG.debug(" environment: " + myEnvParams + " (+" + myParentEnvironmentType + ")");
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;
}
try {
return startProcess(commands);
}
catch (IOException e) {
LOG.info(e);
throw new ProcessNotCreatedException(e.getMessage(), e, this);
}
}
@NotNull
protected Process startProcess(@NotNull List<String> commands) throws IOException {
ProcessBuilder builder = new ProcessBuilder(commands);
setupEnvironment(builder.environment());
builder.directory(myWorkDirectory);
builder.redirectErrorStream(myRedirectErrorStream);
return builder.start();
}
private void checkWorkingDirectory() throws ExecutionException {
if (myWorkDirectory == null) {
return;
}
if (!myWorkDirectory.exists()) {
throw new ExecutionException(IdeBundle.message("run.configuration.error.working.directory.does.not.exist", myWorkDirectory.getAbsolutePath()));
}
if (!myWorkDirectory.isDirectory()) {
throw new ExecutionException(IdeBundle.message("run.configuration.error.working.directory.not.directory"));
}
}
protected void setupEnvironment(@NotNull Map<String, String> environment) {
environment.clear();
if (myParentEnvironmentType != ParentEnvironmentType.NONE) {
environment.putAll(getParentEnvironment());
}
if (SystemInfo.isUnix) {
File workDirectory = getWorkDirectory();
if (workDirectory != null) {
environment.put("PWD", FileUtil.toSystemDependentName(workDirectory.getAbsolutePath()));
}
}
if (!myEnvParams.isEmpty()) {
if (SystemInfo.isWindows) {
THashMap<String, String> envVars = new THashMap<>(CaseInsensitiveStringHashingStrategy.INSTANCE);
envVars.putAll(environment);
envVars.putAll(myEnvParams);
environment.clear();
environment.putAll(envVars);
}
else {
environment.putAll(myEnvParams);
}
}
}
/**
* Normally, double quotes in parameters are escaped so they arrive to a called program as-is.
* But some commands (e.g. {@code 'cmd /c start "title" ...'}) should get they quotes non-escaped.
* Wrapping a parameter by this method (instead of using quotes) will do exactly this.
*
* @see com.intellij.execution.util.ExecUtil#getTerminalCommand(String, String)
*/
@NotNull
public static String inescapableQuote(@NotNull String parameter) {
return CommandLineUtil.specialQuote(parameter);
}
@Override
public String toString() {
return myExePath + " " + myProgramParams;
}
@Override
public <T> T getUserData(@NotNull Key<T> key) {
if (myUserData != null) {
@SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"}) T t = (T)myUserData.get(key);
return t;
}
return null;
}
@Override
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
if (myUserData == null) {
myUserData = ContainerUtil.newHashMap();
}
myUserData.put(key, value);
}
private static class MyTHashMap extends THashMap<String, String> {
public MyTHashMap() {
super(SystemInfo.isWindows ? CaseInsensitiveStringHashingStrategy.INSTANCE : ContainerUtil.<String>canonicalStrategy());
}
@Override
public String put(String key, String value) {
if (key == null || value == null) {
LOG.error(new Exception("Nulls are not allowed"));
return null;
}
if (key.isEmpty()) {
// Windows: passing an environment variable with empty name causes "CreateProcess error=87, The parameter is incorrect"
LOG.warn("Skipping environment variable with empty name, value: " + value);
return null;
}
return super.put(key, value);
}
@Override
public void putAll(Map<? extends String, ? extends String> map) {
if (map != null) {
super.putAll(map);
}
}
}
}