a42a1bfab18a75c0604e3133ce0842b10fe26e9c
[idea/community.git] / platform / platform-api / src / com / intellij / execution / configurations / GeneralCommandLine.java
1 /*
2  * Copyright 2000-2013 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.execution.configurations;
17
18 import com.intellij.execution.CommandLineUtil;
19 import com.intellij.execution.ExecutionException;
20 import com.intellij.execution.Platform;
21 import com.intellij.execution.process.ProcessNotCreatedException;
22 import com.intellij.ide.IdeBundle;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.util.Key;
25 import com.intellij.openapi.util.SystemInfo;
26 import com.intellij.openapi.util.UserDataHolder;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.CharsetToolkit;
29 import com.intellij.util.EnvironmentUtil;
30 import com.intellij.util.PlatformUtils;
31 import com.intellij.util.containers.ContainerUtil;
32 import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
33 import gnu.trove.THashMap;
34 import org.jetbrains.annotations.NonNls;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import java.io.File;
39 import java.io.IOException;
40 import java.nio.charset.Charset;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
44 import java.util.Map;
45
46 /**
47  * OS-independent way of executing external processes with complex parameters.
48  * <p/>
49  * Main idea of the class is to accept parameters "as-is", just as they should look to an external process, and quote/escape them
50  * as required by the underlying platform.
51  *
52  * @see com.intellij.execution.process.OSProcessHandler
53  */
54 public class GeneralCommandLine implements UserDataHolder {
55   private static final Logger LOG = Logger.getInstance("#com.intellij.execution.configurations.GeneralCommandLine");
56
57   private String myExePath = null;
58   private File myWorkDirectory = null;
59   private final Map<String, String> myEnvParams = new MyTHashMap();
60   private boolean myPassParentEnvironment = true;
61   private final ParametersList myProgramParams = new ParametersList();
62   private Charset myCharset = CharsetToolkit.getDefaultSystemCharset();
63   private boolean myRedirectErrorStream = false;
64   private Map<Object, Object> myUserData = null;
65
66   public GeneralCommandLine() { }
67
68   public GeneralCommandLine(@NotNull String... command) {
69     this(Arrays.asList(command));
70   }
71
72   public GeneralCommandLine(@NotNull List<String> command) {
73     int size = command.size();
74     if (size > 0) {
75       setExePath(command.get(0));
76       if (size > 1) {
77         addParameters(command.subList(1, size));
78       }
79     }
80   }
81
82   public String getExePath() {
83     return myExePath;
84   }
85
86   public void setExePath(@NotNull @NonNls final String exePath) {
87     myExePath = exePath.trim();
88   }
89
90   public File getWorkDirectory() {
91     return myWorkDirectory;
92   }
93
94   public void setWorkDirectory(@Nullable @NonNls final String path) {
95     setWorkDirectory(path != null ? new File(path) : null);
96   }
97
98   public void setWorkDirectory(@Nullable final File workDirectory) {
99     myWorkDirectory = workDirectory;
100   }
101
102   /**
103    * Note: the map returned is forgiving to passing null values into putAll().
104    */
105   @NotNull
106   public Map<String, String> getEnvironment() {
107     return myEnvParams;
108   }
109
110   /**
111    * @deprecated use {@link #getEnvironment()} (to remove in IDEA 14)
112    */
113   @SuppressWarnings("unused")
114   public Map<String, String> getEnvParams() {
115     return getEnvironment();
116   }
117
118   /**
119    * @deprecated use {@link #getEnvironment()} (to remove in IDEA 14)
120    */
121   @SuppressWarnings("unused")
122   public void setEnvParams(@Nullable Map<String, String> envParams) {
123     myEnvParams.clear();
124     if (envParams != null) {
125       myEnvParams.putAll(envParams);
126     }
127   }
128
129   public void setPassParentEnvironment(boolean passParentEnvironment) {
130     myPassParentEnvironment = passParentEnvironment;
131   }
132
133   /**
134    * @deprecated use {@link #setPassParentEnvironment(boolean)} (to remove in IDEA 14)
135    */
136   @SuppressWarnings({"unused", "SpellCheckingInspection"})
137   public void setPassParentEnvs(boolean passParentEnvironment) {
138     setPassParentEnvironment(passParentEnvironment);
139   }
140
141   public boolean isPassParentEnvironment() {
142     return myPassParentEnvironment;
143   }
144
145   public void addParameters(final String... parameters) {
146     for (String parameter : parameters) {
147       addParameter(parameter);
148     }
149   }
150
151   public void addParameters(@NotNull final List<String> parameters) {
152     for (final String parameter : parameters) {
153       addParameter(parameter);
154     }
155   }
156
157   public void addParameter(@NotNull @NonNls final String parameter) {
158     myProgramParams.add(parameter);
159   }
160
161   public ParametersList getParametersList() {
162     return myProgramParams;
163   }
164
165   @NotNull
166   public Charset getCharset() {
167     return myCharset;
168   }
169
170   public void setCharset(@NotNull final Charset charset) {
171     myCharset = charset;
172   }
173
174   public boolean isRedirectErrorStream() {
175     return myRedirectErrorStream;
176   }
177
178   public void setRedirectErrorStream(final boolean redirectErrorStream) {
179     myRedirectErrorStream = redirectErrorStream;
180   }
181
182   /**
183    * Returns string representation of this command line.<br/>
184    * Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
185    *
186    * @return single-string representation of this command line.
187    */
188   public String getCommandLineString() {
189     return getCommandLineString(null);
190   }
191
192   /**
193    * Returns string representation of this command line.<br/>
194    * Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
195    *
196    * @param exeName use this executable name instead of given by {@link #setExePath(String)}
197    * @return single-string representation of this command line.
198    */
199   public String getCommandLineString(@Nullable final String exeName) {
200     final List<String> commands = new ArrayList<String>();
201     if (exeName != null) {
202       commands.add(exeName);
203     }
204     else if (myExePath != null) {
205       commands.add(myExePath);
206     }
207     else {
208       commands.add("<null>");
209     }
210     commands.addAll(myProgramParams.getList());
211     return ParametersList.join(commands);
212   }
213
214   /**
215    * Prepares command (quotes and escapes all arguments) and returns it as a newline-separated list
216    * (suitable e.g. for passing in an environment variable).
217    *
218    * @param platform a target platform
219    * @return command as a newline-separated list.
220    */
221   @NotNull
222   public String getPreparedCommandLine(@NotNull Platform platform) {
223     String exePath = myExePath != null ? myExePath : "";
224     return StringUtil.join(CommandLineUtil.toCommandLine(exePath, myProgramParams.getList(), platform), "\n");
225   }
226
227   public Process createProcess() throws ExecutionException {
228     if (LOG.isDebugEnabled()) {
229       LOG.debug("Executing [" + getCommandLineString() + "]");
230     }
231
232     List<String> commands;
233     try {
234       checkWorkingDirectory();
235
236       if (StringUtil.isEmptyOrSpaces(myExePath)) {
237         throw new ExecutionException(IdeBundle.message("run.configuration.error.executable.not.specified"));
238       }
239
240       commands = CommandLineUtil.toCommandLine(myExePath, myProgramParams.getList());
241     }
242     catch (ExecutionException e) {
243       LOG.warn(e);
244       throw e;
245     }
246
247     try {
248       return startProcess(commands);
249     }
250     catch (IOException e) {
251       LOG.warn(e);
252       throw new ProcessNotCreatedException(e.getMessage(), e, this);
253     }
254   }
255
256   protected Process startProcess(@NotNull List<String> commands) throws IOException {
257     ProcessBuilder builder = new ProcessBuilder(commands);
258     setupEnvironment(builder.environment());
259     builder.directory(myWorkDirectory);
260     builder.redirectErrorStream(myRedirectErrorStream);
261     return builder.start();
262   }
263
264   private void checkWorkingDirectory() throws ExecutionException {
265     if (myWorkDirectory == null) {
266       return;
267     }
268     if (!myWorkDirectory.exists()) {
269       throw new ExecutionException(
270         IdeBundle.message("run.configuration.error.working.directory.does.not.exist", myWorkDirectory.getAbsolutePath()));
271     }
272     if (!myWorkDirectory.isDirectory()) {
273       throw new ExecutionException(IdeBundle.message("run.configuration.error.working.directory.not.directory"));
274     }
275   }
276
277   protected void setupEnvironment(@NotNull Map<String, String> environment) {
278     environment.clear();
279
280     if (myPassParentEnvironment) {
281       environment.putAll(PlatformUtils.isAppCode() ? System.getenv() // Temporarily fix for OC-8606
282                                                    : EnvironmentUtil.getEnvironmentMap());
283     }
284
285     if (!myEnvParams.isEmpty()) {
286       if (SystemInfo.isWindows) {
287         THashMap<String, String> envVars = new THashMap<String, String>(CaseInsensitiveStringHashingStrategy.INSTANCE);
288         envVars.putAll(environment);
289         envVars.putAll(myEnvParams);
290         environment.clear();
291         environment.putAll(envVars);
292       }
293       else {
294         environment.putAll(myEnvParams);
295       }
296     }
297   }
298
299   /**
300    * Normally, double quotes in parameters are escaped so they arrive to a called program as-is.
301    * But some commands (e.g. {@code 'cmd /c start "title" ...'}) should get they quotes non-escaped.
302    * Wrapping a parameter by this method (instead of using quotes) will do exactly this.
303    *
304    * @see com.intellij.execution.util.ExecUtil#getTerminalCommand(String, String)
305    */
306   @NotNull
307   public static String inescapableQuote(@NotNull String parameter) {
308     return CommandLineUtil.specialQuote(parameter);
309   }
310
311   @Override
312   public String toString() {
313     return myExePath + " " + myProgramParams;
314   }
315
316   @Override
317   public <T> T getUserData(@NotNull final Key<T> key) {
318     if (myUserData != null) {
319       @SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"}) final T t = (T)myUserData.get(key);
320       return t;
321     }
322     return null;
323   }
324
325   @Override
326   public <T> void putUserData(@NotNull final Key<T> key, @Nullable final T value) {
327     if (myUserData == null) {
328       myUserData = ContainerUtil.newHashMap();
329     }
330     myUserData.put(key, value);
331   }
332
333   private static class MyTHashMap extends THashMap<String, String> {
334     @Override
335     public void putAll(Map<? extends String, ? extends String> map) {
336       if (map != null) {
337         super.putAll(map);
338       }
339     }
340   }
341 }