1 package org.codehaus.plexus.util.cli;
2
3 /*
4 * Copyright The Codehaus Foundation.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import java.io.InputStream;
20 import java.util.Locale;
21 import java.util.Map;
22 import java.util.Properties;
23 import java.util.StringTokenizer;
24 import java.util.Vector;
25
26 import org.codehaus.plexus.util.Os;
27 import org.codehaus.plexus.util.StringUtils;
28
29 /**
30 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl </a>
31 *
32 */
33 public abstract class CommandLineUtils {
34
35 /**
36 * A {@code StreamConsumer} providing consumed lines as a {@code String}.
37 *
38 * @see #getOutput()
39 */
40 public static class StringStreamConsumer implements StreamConsumer {
41
42 private StringBuffer string = new StringBuffer();
43
44 private String ls = System.getProperty("line.separator");
45
46 @Override
47 public void consumeLine(String line) {
48 string.append(line).append(ls);
49 }
50
51 public String getOutput() {
52 return string.toString();
53 }
54 }
55
56 /**
57 * Number of milliseconds per second.
58 */
59 private static final long MILLIS_PER_SECOND = 1000L;
60
61 /**
62 * Number of nanoseconds per second.
63 */
64 private static final long NANOS_PER_SECOND = 1000000000L;
65
66 public static int executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr)
67 throws CommandLineException {
68 return executeCommandLine(cl, null, systemOut, systemErr, 0);
69 }
70
71 public static int executeCommandLine(
72 Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds)
73 throws CommandLineException {
74 return executeCommandLine(cl, null, systemOut, systemErr, timeoutInSeconds);
75 }
76
77 public static int executeCommandLine(
78 Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr)
79 throws CommandLineException {
80 return executeCommandLine(cl, systemIn, systemOut, systemErr, 0);
81 }
82
83 /**
84 * @param cl The command line to execute
85 * @param systemIn The input to read from, must be thread safe
86 * @param systemOut A consumer that receives output, must be thread safe
87 * @param systemErr A consumer that receives system error stream output, must be thread safe
88 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
89 * @return A return value, see {@link Process#exitValue()}
90 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
91 */
92 public static int executeCommandLine(
93 Commandline cl,
94 InputStream systemIn,
95 StreamConsumer systemOut,
96 StreamConsumer systemErr,
97 int timeoutInSeconds)
98 throws CommandLineException {
99 final CommandLineCallable future =
100 executeCommandLineAsCallable(cl, systemIn, systemOut, systemErr, timeoutInSeconds);
101 return future.call();
102 }
103
104 /**
105 * Immediately forks a process, returns a callable that will block until process is complete.
106 *
107 * @param cl The command line to execute
108 * @param systemIn The input to read from, must be thread safe
109 * @param systemOut A consumer that receives output, must be thread safe
110 * @param systemErr A consumer that receives system error stream output, must be thread safe
111 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
112 * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
113 * must be called on this to be sure the forked process has terminated, no guarantees is made about any
114 * internal state before after the completion of the call statements
115 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
116 */
117 public static CommandLineCallable executeCommandLineAsCallable(
118 final Commandline cl,
119 final InputStream systemIn,
120 final StreamConsumer systemOut,
121 final StreamConsumer systemErr,
122 final int timeoutInSeconds)
123 throws CommandLineException {
124 if (cl == null) {
125 throw new IllegalArgumentException("cl cannot be null.");
126 }
127
128 final Process p = cl.execute();
129
130 final Thread processHook = new Thread() {
131
132 {
133 this.setName("CommandLineUtils process shutdown hook");
134 this.setContextClassLoader(null);
135 }
136
137 @Override
138 public void run() {
139 p.destroy();
140 }
141 };
142
143 ShutdownHookUtils.addShutDownHook(processHook);
144
145 return new CommandLineCallable() {
146
147 @Override
148 public Integer call() throws CommandLineException {
149 StreamFeeder inputFeeder = null;
150 StreamPumper outputPumper = null;
151 StreamPumper errorPumper = null;
152 boolean success = false;
153 try {
154 if (systemIn != null) {
155 inputFeeder = new StreamFeeder(systemIn, p.getOutputStream());
156 inputFeeder.start();
157 }
158
159 outputPumper = new StreamPumper(p.getInputStream(), systemOut);
160 outputPumper.start();
161
162 errorPumper = new StreamPumper(p.getErrorStream(), systemErr);
163 errorPumper.start();
164
165 int returnValue;
166 if (timeoutInSeconds <= 0) {
167 returnValue = p.waitFor();
168 } else {
169 final long now = System.nanoTime();
170 final long timeout = now + NANOS_PER_SECOND * timeoutInSeconds;
171
172 while (isAlive(p) && (System.nanoTime() < timeout)) {
173 // The timeout is specified in seconds. Therefore we must not sleep longer than one second
174 // but we should sleep as long as possible to reduce the number of iterations performed.
175 Thread.sleep(MILLIS_PER_SECOND - 1L);
176 }
177
178 if (isAlive(p)) {
179 throw new InterruptedException(
180 String.format("Process timed out after %d seconds.", timeoutInSeconds));
181 }
182
183 returnValue = p.exitValue();
184 }
185
186 // TODO Find out if waitUntilDone needs to be called using a try-finally construct. The method may
187 // throw an
188 // InterruptedException so that calls to waitUntilDone may be skipped.
189 // try
190 // {
191 // if ( inputFeeder != null )
192 // {
193 // inputFeeder.waitUntilDone();
194 // }
195 // }
196 // finally
197 // {
198 // try
199 // {
200 // outputPumper.waitUntilDone();
201 // }
202 // finally
203 // {
204 // errorPumper.waitUntilDone();
205 // }
206 // }
207 if (inputFeeder != null) {
208 inputFeeder.waitUntilDone();
209 }
210
211 outputPumper.waitUntilDone();
212 errorPumper.waitUntilDone();
213
214 if (inputFeeder != null) {
215 inputFeeder.close();
216 handleException(inputFeeder, "stdin");
217 }
218
219 outputPumper.close();
220 handleException(outputPumper, "stdout");
221
222 errorPumper.close();
223 handleException(errorPumper, "stderr");
224
225 success = true;
226 return returnValue;
227 } catch (InterruptedException ex) {
228 throw new CommandLineTimeOutException(
229 "Error while executing external command, process killed.", ex);
230
231 } finally {
232 if (inputFeeder != null) {
233 inputFeeder.disable();
234 }
235 if (outputPumper != null) {
236 outputPumper.disable();
237 }
238 if (errorPumper != null) {
239 errorPumper.disable();
240 }
241
242 try {
243 ShutdownHookUtils.removeShutdownHook(processHook);
244 processHook.run();
245 } finally {
246 try {
247 if (inputFeeder != null) {
248 inputFeeder.close();
249
250 if (success) {
251 success = false;
252 handleException(inputFeeder, "stdin");
253 success = true; // Only reached when no exception has been thrown.
254 }
255 }
256 } finally {
257 try {
258 if (outputPumper != null) {
259 outputPumper.close();
260
261 if (success) {
262 success = false;
263 handleException(outputPumper, "stdout");
264 success = true; // Only reached when no exception has been thrown.
265 }
266 }
267 } finally {
268 if (errorPumper != null) {
269 errorPumper.close();
270
271 if (success) {
272 handleException(errorPumper, "stderr");
273 }
274 }
275 }
276 }
277 }
278 }
279 }
280 };
281 }
282
283 private static void handleException(final StreamPumper streamPumper, final String streamName)
284 throws CommandLineException {
285 if (streamPumper.getException() != null) {
286 throw new CommandLineException(
287 String.format("Failure processing %s.", streamName), streamPumper.getException());
288 }
289 }
290
291 private static void handleException(final StreamFeeder streamFeeder, final String streamName)
292 throws CommandLineException {
293 if (streamFeeder.getException() != null) {
294 throw new CommandLineException(
295 String.format("Failure processing %s.", streamName), streamFeeder.getException());
296 }
297 }
298
299 /**
300 * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
301 * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
302 * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
303 * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
304 *
305 * @return The shell environment variables, can be empty but never <code>null</code>.
306 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
307 * will be used if available in the current running jvm.</b>
308 */
309 public static Properties getSystemEnvVars() {
310 return getSystemEnvVars(!Os.isFamily(Os.FAMILY_WINDOWS));
311 }
312
313 /**
314 * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar keys will all be
315 * upper-case.
316 *
317 * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
318 * @return Properties object of (possibly modified) envar keys mapped to their values.
319 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
320 * will be used if available in the current running jvm.</b>
321 */
322 public static Properties getSystemEnvVars(boolean caseSensitive) {
323 Properties envVars = new Properties();
324 Map<String, String> envs = System.getenv();
325 for (String key : envs.keySet()) {
326 String value = envs.get(key);
327 if (!caseSensitive) {
328 key = key.toUpperCase(Locale.ENGLISH);
329 }
330 envVars.put(key, value);
331 }
332 return envVars;
333 }
334
335 public static boolean isAlive(Process p) {
336 if (p == null) {
337 return false;
338 }
339
340 try {
341 p.exitValue();
342 return false;
343 } catch (IllegalThreadStateException e) {
344 return true;
345 }
346 }
347
348 public static String[] translateCommandline(String toProcess) throws Exception {
349 if ((toProcess == null) || (toProcess.length() == 0)) {
350 return new String[0];
351 }
352
353 // parse with a simple finite state machine
354
355 final int normal = 0;
356 final int inQuote = 1;
357 final int inDoubleQuote = 2;
358 int state = normal;
359 StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
360 Vector<String> v = new Vector<String>();
361 StringBuilder current = new StringBuilder();
362
363 while (tok.hasMoreTokens()) {
364 String nextTok = tok.nextToken();
365 switch (state) {
366 case inQuote:
367 if ("\'".equals(nextTok)) {
368 state = normal;
369 } else {
370 current.append(nextTok);
371 }
372 break;
373 case inDoubleQuote:
374 if ("\"".equals(nextTok)) {
375 state = normal;
376 } else {
377 current.append(nextTok);
378 }
379 break;
380 default:
381 if ("\'".equals(nextTok)) {
382 state = inQuote;
383 } else if ("\"".equals(nextTok)) {
384 state = inDoubleQuote;
385 } else if (" ".equals(nextTok)) {
386 if (current.length() != 0) {
387 v.addElement(current.toString());
388 current.setLength(0);
389 }
390 } else {
391 current.append(nextTok);
392 }
393 break;
394 }
395 }
396
397 if (current.length() != 0) {
398 v.addElement(current.toString());
399 }
400
401 if ((state == inQuote) || (state == inDoubleQuote)) {
402 throw new CommandLineException("unbalanced quotes in " + toProcess);
403 }
404
405 String[] args = new String[v.size()];
406 v.copyInto(args);
407 return args;
408 }
409
410 /**
411 * <p>
412 * Put quotes around the given String if necessary.
413 * </p>
414 * <p>
415 * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
416 * quotes - else surround the argument by double quotes.
417 * </p>
418 * @param argument the argument
419 * @return the transformed command line
420 * @throws CommandLineException if the argument contains both, single and double quotes.
421 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
422 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
423 * {@link StringUtils#quoteAndEscape(String, char)} instead.
424 */
425 @Deprecated
426 @SuppressWarnings({"JavaDoc", "deprecation"})
427 public static String quote(String argument) throws CommandLineException {
428 return quote(argument, false, false, true);
429 }
430
431 /**
432 * <p>
433 * Put quotes around the given String if necessary.
434 * </p>
435 * <p>
436 * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
437 * quotes - else surround the argument by double quotes.
438 * </p>
439 * @param argument see name
440 * @param wrapExistingQuotes see name
441 * @return the transformed command line
442 * @throws CommandLineException if the argument contains both, single and double quotes.
443 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
444 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
445 * {@link StringUtils#quoteAndEscape(String, char)} instead.
446 */
447 @Deprecated
448 @SuppressWarnings({"JavaDoc", "UnusedDeclaration", "deprecation"})
449 public static String quote(String argument, boolean wrapExistingQuotes) throws CommandLineException {
450 return quote(argument, false, false, wrapExistingQuotes);
451 }
452
453 /**
454 * @param argument the argument
455 * @param escapeSingleQuotes see name
456 * @param escapeDoubleQuotes see name
457 * @param wrapExistingQuotes see name
458 * @return the transformed command line
459 * @throws CommandLineException some trouble
460 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
461 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
462 * {@link StringUtils#quoteAndEscape(String, char)} instead.
463 */
464 @Deprecated
465 @SuppressWarnings({"JavaDoc"})
466 public static String quote(
467 String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes, boolean wrapExistingQuotes)
468 throws CommandLineException {
469 if (argument.contains("\"")) {
470 if (argument.contains("\'")) {
471 throw new CommandLineException("Can't handle single and double quotes in same argument");
472 } else {
473 if (escapeSingleQuotes) {
474 return "\\\'" + argument + "\\\'";
475 } else if (wrapExistingQuotes) {
476 return '\'' + argument + '\'';
477 }
478 }
479 } else if (argument.contains("\'")) {
480 if (escapeDoubleQuotes) {
481 return "\\\"" + argument + "\\\"";
482 } else if (wrapExistingQuotes) {
483 return '\"' + argument + '\"';
484 }
485 } else if (argument.contains(" ")) {
486 if (escapeDoubleQuotes) {
487 return "\\\"" + argument + "\\\"";
488 } else {
489 return '\"' + argument + '\"';
490 }
491 }
492
493 return argument;
494 }
495
496 public static String toString(String[] line) {
497 // empty path return empty string
498 if ((line == null) || (line.length == 0)) {
499 return "";
500 }
501
502 // path containing one or more elements
503 final StringBuilder result = new StringBuilder();
504 for (int i = 0; i < line.length; i++) {
505 if (i > 0) {
506 result.append(' ');
507 }
508 try {
509 result.append(StringUtils.quoteAndEscape(line[i], '\"'));
510 } catch (Exception e) {
511 System.err.println("Error quoting argument: " + e.getMessage());
512 }
513 }
514 return result.toString();
515 }
516 }