case-insensitive environment map for Windows
[idea/community.git] / platform / platform-api / src / com / intellij / execution / configurations / GeneralCommandLine.java
1 /*
2  * Copyright 2000-2015 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.NotNull;
35 import org.jetbrains.annotations.Nullable;
36
37 import java.io.File;
38 import java.io.IOException;
39 import java.nio.charset.Charset;
40 import java.util.*;
41
42 /**
43  * OS-independent way of executing external processes with complex parameters.
44  * <p/>
45  * Main idea of the class is to accept parameters "as-is", just as they should look to an external process, and quote/escape them
46  * as required by the underlying platform.
47  *
48  * @see com.intellij.execution.process.OSProcessHandler
49  */
50 public class GeneralCommandLine implements UserDataHolder {
51   private static final Logger LOG = Logger.getInstance("#com.intellij.execution.configurations.GeneralCommandLine");
52
53   private String myExePath = null;
54   private File myWorkDirectory = null;
55   private final Map<String, String> myEnvParams = new MyTHashMap();
56   private boolean myPassParentEnvironment = true;
57   private final ParametersList myProgramParams = new ParametersList();
58   private Charset myCharset = CharsetToolkit.getDefaultSystemCharset();
59   private boolean myRedirectErrorStream = false;
60   private Map<Object, Object> myUserData = null;
61
62   public GeneralCommandLine() { }
63
64   public GeneralCommandLine(@NotNull String... command) {
65     this(Arrays.asList(command));
66   }
67
68   public GeneralCommandLine(@NotNull List<String> command) {
69     int size = command.size();
70     if (size > 0) {
71       setExePath(command.get(0));
72       if (size > 1) {
73         addParameters(command.subList(1, size));
74       }
75     }
76   }
77
78   public String getExePath() {
79     return myExePath;
80   }
81
82   @NotNull
83   public GeneralCommandLine withExePath(@NotNull String exePath) {
84     myExePath = exePath.trim();
85     return this;
86   }
87
88   public void setExePath(@NotNull String exePath) {
89     withExePath(exePath);
90   }
91
92   public File getWorkDirectory() {
93     return myWorkDirectory;
94   }
95
96   @NotNull
97   public GeneralCommandLine withWorkDirectory(@Nullable String path) {
98     return withWorkDirectory(path != null ? new File(path) : null);
99   }
100
101   @NotNull
102   public GeneralCommandLine withWorkDirectory(@Nullable File workDirectory) {
103     myWorkDirectory = workDirectory;
104     return this;
105   }
106
107   public void setWorkDirectory(@Nullable String path) {
108     withWorkDirectory(path);
109   }
110
111   public void setWorkDirectory(@Nullable File workDirectory) {
112     withWorkDirectory(workDirectory);
113   }
114
115   /**
116    * Note: the map returned is forgiving to passing null values into putAll().
117    */
118   @NotNull
119   public Map<String, String> getEnvironment() {
120     return myEnvParams;
121   }
122
123   @NotNull
124   public GeneralCommandLine withEnvironment(@Nullable Map<String, String> environment) {
125     if (environment != null) {
126       getEnvironment().putAll(environment);
127     }
128     return this;
129   }
130
131   public boolean isPassParentEnvironment() {
132     return myPassParentEnvironment;
133   }
134
135   @NotNull
136   public GeneralCommandLine withPassParentEnvironment(boolean passParentEnvironment) {
137     myPassParentEnvironment = passParentEnvironment;
138     return this;
139   }
140
141   public void setPassParentEnvironment(boolean passParentEnvironment) {
142     withPassParentEnvironment(passParentEnvironment);
143   }
144
145   /**
146    * @return unmodifiable map of the parent environment, that will be passed to the process if isPassParentEnvironment() == true
147    */
148   @NotNull
149   public Map<String, String> getParentEnvironment() {
150     return PlatformUtils.isAppCode() ? System.getenv() // Temporarily fix for OC-8606
151                                      : EnvironmentUtil.getEnvironmentMap();
152   }
153
154   public void addParameters(String... parameters) {
155     for (String parameter : parameters) {
156       addParameter(parameter);
157     }
158   }
159
160   public void addParameters(@NotNull List<String> parameters) {
161     for (String parameter : parameters) {
162       addParameter(parameter);
163     }
164   }
165
166   public void addParameter(@NotNull String parameter) {
167     myProgramParams.add(parameter);
168   }
169
170   public ParametersList getParametersList() {
171     return myProgramParams;
172   }
173
174   @NotNull
175   public Charset getCharset() {
176     return myCharset;
177   }
178
179   @NotNull
180   public GeneralCommandLine withCharset(@NotNull Charset charset) {
181     myCharset = charset;
182     return this;
183   }
184
185   public void setCharset(@NotNull Charset charset) {
186     withCharset(charset);
187   }
188
189   public boolean isRedirectErrorStream() {
190     return myRedirectErrorStream;
191   }
192
193   @NotNull
194   public GeneralCommandLine withRedirectErrorStream(boolean redirectErrorStream) {
195     myRedirectErrorStream = redirectErrorStream;
196     return this;
197   }
198
199   public void setRedirectErrorStream(boolean redirectErrorStream) {
200     withRedirectErrorStream(redirectErrorStream);
201   }
202
203   /**
204    * Returns string representation of this command line.<br/>
205    * Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
206    *
207    * @return single-string representation of this command line.
208    */
209   public String getCommandLineString() {
210     return getCommandLineString(null);
211   }
212
213   /**
214    * Returns string representation of this command line.<br/>
215    * Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
216    *
217    * @param exeName use this executable name instead of given by {@link #setExePath(String)}
218    * @return single-string representation of this command line.
219    */
220   public String getCommandLineString(@Nullable String exeName) {
221     return ParametersList.join(getCommandLineList(exeName));
222   }
223
224   public List<String> getCommandLineList(@Nullable String exeName) {
225     List<String> commands = new ArrayList<String>();
226     if (exeName != null) {
227       commands.add(exeName);
228     }
229     else if (myExePath != null) {
230       commands.add(myExePath);
231     }
232     else {
233       commands.add("<null>");
234     }
235     commands.addAll(myProgramParams.getList());
236     return commands;
237   }
238
239   /**
240    * Prepares command (quotes and escapes all arguments) and returns it as a newline-separated list
241    * (suitable e.g. for passing in an environment variable).
242    *
243    * @param platform a target platform
244    * @return command as a newline-separated list.
245    */
246   @NotNull
247   public String getPreparedCommandLine(@NotNull Platform platform) {
248     String exePath = myExePath != null ? myExePath : "";
249     return StringUtil.join(CommandLineUtil.toCommandLine(exePath, myProgramParams.getList(), platform), "\n");
250   }
251
252   @NotNull
253   public Process createProcess() throws ExecutionException {
254     if (LOG.isDebugEnabled()) {
255       LOG.debug("Executing [" + getCommandLineString() + "]");
256     }
257
258     List<String> commands;
259     try {
260       checkWorkingDirectory();
261
262       if (StringUtil.isEmptyOrSpaces(myExePath)) {
263         throw new ExecutionException(IdeBundle.message("run.configuration.error.executable.not.specified"));
264       }
265
266       commands = CommandLineUtil.toCommandLine(myExePath, myProgramParams.getList());
267     }
268     catch (ExecutionException e) {
269       LOG.info(e);
270       throw e;
271     }
272
273     try {
274       return startProcess(commands);
275     }
276     catch (IOException e) {
277       LOG.info(e);
278       throw new ProcessNotCreatedException(e.getMessage(), e, this);
279     }
280   }
281
282   @NotNull
283   protected Process startProcess(@NotNull List<String> commands) throws IOException {
284     ProcessBuilder builder = new ProcessBuilder(commands);
285     setupEnvironment(builder.environment());
286     builder.directory(myWorkDirectory);
287     builder.redirectErrorStream(myRedirectErrorStream);
288     return builder.start();
289   }
290
291   private void checkWorkingDirectory() throws ExecutionException {
292     if (myWorkDirectory == null) {
293       return;
294     }
295     if (!myWorkDirectory.exists()) {
296       throw new ExecutionException(
297         IdeBundle.message("run.configuration.error.working.directory.does.not.exist", myWorkDirectory.getAbsolutePath()));
298     }
299     if (!myWorkDirectory.isDirectory()) {
300       throw new ExecutionException(IdeBundle.message("run.configuration.error.working.directory.not.directory"));
301     }
302   }
303
304   protected void setupEnvironment(@NotNull Map<String, String> environment) {
305     environment.clear();
306
307     if (myPassParentEnvironment) {
308       environment.putAll(getParentEnvironment());
309     }
310     
311     if (!myEnvParams.isEmpty()) {
312       if (SystemInfo.isWindows) {
313         THashMap<String, String> envVars = new THashMap<String, String>(CaseInsensitiveStringHashingStrategy.INSTANCE);
314         envVars.putAll(environment);
315         envVars.putAll(myEnvParams);
316         environment.clear();
317         environment.putAll(envVars);
318       }
319       else {
320         environment.putAll(myEnvParams);
321       }
322     }
323   }
324
325   /**
326    * Normally, double quotes in parameters are escaped so they arrive to a called program as-is.
327    * But some commands (e.g. {@code 'cmd /c start "title" ...'}) should get they quotes non-escaped.
328    * Wrapping a parameter by this method (instead of using quotes) will do exactly this.
329    *
330    * @see com.intellij.execution.util.ExecUtil#getTerminalCommand(String, String)
331    */
332   @NotNull
333   public static String inescapableQuote(@NotNull String parameter) {
334     return CommandLineUtil.specialQuote(parameter);
335   }
336
337   @Override
338   public String toString() {
339     return myExePath + " " + myProgramParams;
340   }
341
342   @Override
343   public <T> T getUserData(@NotNull Key<T> key) {
344     if (myUserData != null) {
345       @SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"}) T t = (T)myUserData.get(key);
346       return t;
347     }
348     return null;
349   }
350
351   @Override
352   public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
353     if (myUserData == null) {
354       myUserData = ContainerUtil.newHashMap();
355     }
356     myUserData.put(key, value);
357   }
358
359   private static class MyTHashMap extends THashMap<String, String> {
360     public MyTHashMap() {
361       super(SystemInfo.isWindows ? CaseInsensitiveStringHashingStrategy.INSTANCE : ContainerUtil.<String>canonicalStrategy());
362     }
363
364     @Override
365     public String put(String key, String value) {
366       if (key == null || value == null) {
367         LOG.error(new Exception("Nulls are not allowed"));
368         return null;
369       }
370       if (key.isEmpty()) {
371         // Windows: passing an environment variable with empty name causes "CreateProcess error=87, The parameter is incorrect"
372         LOG.warn("Skipping environment variable with empty name, value: " + value);
373         return null;
374       }
375       return super.put(key, value);
376     }
377
378     @Override
379     public void putAll(Map<? extends String, ? extends String> map) {
380       if (map != null) {
381         super.putAll(map);
382       }
383     }
384   }
385 }