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.IOException;
20 import java.io.InputStream;
21 import java.lang.reflect.Method;
22 import java.util.Locale;
23 import java.util.Map;
24 import java.util.Properties;
25 import java.util.StringTokenizer;
26 import java.util.Vector;
27 import org.codehaus.plexus.util.Os;
28 import org.codehaus.plexus.util.StringUtils;
29
30 /**
31 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl </a>
32 * @version $Id$
33 */
34 public abstract class CommandLineUtils
35 {
36 public static class StringStreamConsumer
37 implements StreamConsumer
38 {
39 private StringBuffer string = new StringBuffer();
40
41 private String ls = System.getProperty( "line.separator" );
42
43 public void consumeLine( String line )
44 {
45 string.append( line ).append( ls );
46 }
47
48 public String getOutput()
49 {
50 return string.toString();
51 }
52 }
53
54 private static class ProcessHook extends Thread {
55 private final Process process;
56
57 private ProcessHook( Process process )
58 {
59 super("CommandlineUtils process shutdown hook");
60 this.process = process;
61 this.setContextClassLoader( null );
62 }
63
64 public void run()
65 {
66 process.destroy();
67 }
68 }
69
70 /**
71 * @throws CommandLineException
72 * (without {@link CommandLineTimeOutException}) if
73 * <ul>
74 * <li><!-- see Commandline.execute() -->
75 * the file expected to be the working directory
76 * does not exist or is not a directory.
77 * <li><!-- see Commandline.execute() -->
78 * {@link Runtime#exec(String, String[], File)} fails
79 * throwing an {@link IOException}.
80 * <li> <!-- see CommandLineCallable.call() -->
81 * an error inside systemOut parser occurs
82 * <li> <!-- see CommandLineCallable.call() -->
83 * an error inside systemErr parser occurs
84 * </ul>
85 * @throws CommandLineTimeOutException
86 * Wrapping an {@link InterruptedException}
87 * on the process to be executed thrown by {@link Process#waitFor()}.
88 */
89 // used in org.m2latex.core.CommandExecutorImpl
90 public static int executeCommandLine(Commandline cl,
91 StreamConsumer systemOut,
92 StreamConsumer systemErr)
93 throws CommandLineException
94 {
95 // may throw CommandLineException
96 return executeCommandLine(cl, null, systemOut, systemErr, 0);
97 }
98
99 // public static int executeCommandLine(Commandline cl,
100 // StreamConsumer systemOut,
101 // StreamConsumer systemErr,
102 // int timeoutInSeconds)
103 // throws CommandLineException
104 // {
105 // // may throw CommandLineException
106 // return executeCommandLine
107 // (cl, null, systemOut, systemErr, timeoutInSeconds);
108 // }
109
110 // public static int executeCommandLine(Commandline cl,
111 // InputStream systemIn,
112 // StreamConsumer systemOut,
113 // StreamConsumer systemErr)
114 // throws CommandLineException
115 // {
116 // // may throw CommandLineException
117 // return executeCommandLine(cl, systemIn, systemOut, systemErr, 0);
118 // }
119
120 /**
121 * @param cl
122 * The command line to execute
123 * @param systemIn
124 * The input to read from, must be thread safe
125 * @param systemOut
126 * A consumer that receives output, must be thread safe
127 * @param systemErr
128 * A consumer that receives system error stream output,
129 * must be thread safe
130 * @param timeoutInSeconds
131 * Positive integer to specify timeout,
132 * zero and negative integers for no timeout.
133 * @return
134 * A return value, see {@link Process#exitValue()}
135 * @throws CommandLineException
136 * (without {@link CommandLineTimeOutException}) if
137 * <ul>
138 * <li><!-- see Commandline.execute() -->
139 * the file expected to be the working directory
140 * does not exist or is not a directory.
141 * <li><!-- see Commandline.execute() -->
142 * {@link Runtime#exec(String, String[], File)} fails
143 * throwing an {@link IOException}.
144 * <li> <!-- see CommandLineCallable.call() -->
145 * an error inside systemOut parser occurs
146 * <li> <!-- see CommandLineCallable.call() -->
147 * an error inside systemErr parser occurs
148 * </ul>
149 * @throws CommandLineTimeOutException
150 * Wrapping an {@link InterruptedException}
151 * on the process to be executed:
152 * If <code>timeoutInSeconds<=0</code>
153 * thrown by {@link Process#waitFor()}.
154 * If <code>timeoutInSeconds > 0</code> thrown
155 * <ul>
156 * <li>by {@link Thread#sleep(long)}
157 * <li>if after sleep {@link #isAlive(Process)} holds.
158 * <li>by
159 * {@link #waitForAllPumpers(StreamFeeder, StreamPumper, StreamPumper)}
160 * </ul>
161 */
162 // used but invoked only with sytemIn==null and timeoutInSeconds==0
163 public static int executeCommandLine(Commandline cl,
164 InputStream systemIn,
165 StreamConsumer systemOut,
166 StreamConsumer systemErr,
167 int timeoutInSeconds)
168 throws CommandLineException {
169 // may throw CommandLineException
170 final CommandLineCallable future = executeCommandLineAsCallable
171 (cl, systemIn, systemOut, systemErr, timeoutInSeconds);
172 // may throw CommandLineException:
173 // see docs of executeCommandLineAsCallable:
174 // - error inside systemOut/systemErr parser
175 // - wrapping InterruptedException
176 return future.call();
177 }
178
179 /**
180 * Immediately forks a process,
181 * returns a callable that will block until process is complete.
182 * @param cl
183 * The command line to execute
184 * @param systemIn
185 * The input to read from, must be thread safe
186 * @param systemOut
187 * A consumer that receives output, must be thread safe
188 * @param systemErr
189 * A consumer that receives system error stream output,
190 * must be thread safe
191 * @param timeoutInSeconds
192 * Positive integer to specify timeout,
193 * zero and negative integers for no timeout.
194 * @return
195 * A CommandLineCallable that provides the process return value,
196 * see {@link Process#exitValue()}. "call" must be called on
197 * this to be sure the forked process has terminated,
198 * no guarantees is made about any internal state
199 * before after the completion of the call statements
200 * <p>
201 * It is documented in which cases the return value throws exceptions
202 * if {@link CommandLineCallable#call()} is executed.
203 * @throws CommandLineException
204 * or CommandLineTimeOutException:
205 * see {@link Commandline#execute()}.
206 */
207 // used but invoked only with sytemIn==null and timeoutInSeconds==0
208 public static CommandLineCallable
209 executeCommandLineAsCallable(final Commandline cl,
210 final InputStream systemIn,
211 final StreamConsumer systemOut,
212 final StreamConsumer systemErr,
213 final int timeoutInSeconds)
214 throws CommandLineException {
215 if (cl == null) {
216 throw new IllegalArgumentException("Commandline cannot be null." );
217 }
218 // may throw CommandLineException (sole)
219 final Process proc = cl.execute();
220
221 final StreamFeeder inputFeeder = systemIn != null
222 ? new StreamFeeder(systemIn, proc.getOutputStream()) : null;
223
224 final StreamPumpermper.html#StreamPumper">StreamPumper outputPumper = new StreamPumper
225 (proc.getInputStream(), systemOut);
226
227 final StreamPumperumper.html#StreamPumper">StreamPumper errorPumper = new StreamPumper
228 (proc.getErrorStream(), systemErr);
229
230 if ( inputFeeder != null )
231 {
232 inputFeeder.start();
233 }
234
235 outputPumper.start();
236
237 errorPumper.start();
238
239 final ProcessHook processHook = new ProcessHook(proc);
240
241 ShutdownHookUtils.addShutDownHook( processHook );
242
243 /**
244 * @throws CommandLineException
245 * only explicitly (3 occurrences)
246 * <ul>
247 * <li> Error inside systemOut parser
248 * <li> Error inside systemErr parser
249 * <li> Even CommandLineTimeoutException
250 * Wrapping an {@link InterruptedException} on the process
251 * <code>proc</code> to be executed:
252 * <ul>
253 * <li>timeoutInSeconds <= 0 and {@link Process#waitFor()}
254 * <li>timeoutInSeconds > 0 and {@link Thread#sleep(int)}
255 * <li>timeoutInSeconds > 0 and {@link #isAlive(Process)}
256 * <li>timeoutInSeconds > 0 and
257 * {@link #waitForAllPumpers(StreamFeeder, StreamPumper, StreamPumper)}
258 * </ul>
259 * </ul>
260 */
261 return new CommandLineCallable() {
262 public Integer call() throws CommandLineException {
263 try {
264 int returnValue;
265 if (timeoutInSeconds <= 0) {
266 // may throw InterruptedException
267 returnValue = proc.waitFor();
268 } else {
269 long now = System.currentTimeMillis();
270 long timeoutInMillis = 1000L * timeoutInSeconds;
271 long finish = now + timeoutInMillis;
272 while (isAlive(proc) &&
273 System.currentTimeMillis() < finish) {
274 // may throw InterruptedException
275 Thread.sleep(10);
276 }
277 if (isAlive(proc)) {
278 // caught in catch block
279 throw new InterruptedException
280 ("Process timeout out after " +
281 timeoutInSeconds + " seconds" );
282 }
283 returnValue = proc.exitValue();
284 }
285 // may throw InterruptedException
286 waitForAllPumpers(inputFeeder, outputPumper, errorPumper);
287
288 if (outputPumper.getException() != null) {
289 throw new CommandLineException
290 ("Error inside systemOut parser",
291 outputPumper.getException());
292 }
293
294 if (errorPumper.getException() != null) {
295 throw new CommandLineException
296 ("Error inside systemErr parser",
297 errorPumper.getException() );
298 }
299
300 return returnValue;
301 } catch (InterruptedException ex) {
302 if (inputFeeder != null) {
303 inputFeeder.disable();
304 }
305 outputPumper.disable();
306 errorPumper .disable();
307 throw new CommandLineTimeOutException
308 ("Error while executing external command, " +
309 "process killed.", ex);
310 } finally {
311 ShutdownHookUtils.removeShutdownHook(processHook);
312 processHook.run();
313
314 if (inputFeeder != null) {
315 inputFeeder.close();
316 }
317
318 outputPumper.close();
319 errorPumper .close();
320 }
321 }
322 };
323 }
324
325 /**
326 *
327 * @throws InterruptedException
328 * if one of the parameters waitUntilDone throws such an exception.
329 */
330 private static void waitForAllPumpers(StreamFeeder inputFeeder,
331 StreamPumper outputPumper,
332 StreamPumper errorPumper)
333 throws InterruptedException
334 {
335 if ( inputFeeder != null )
336 {
337 inputFeeder.waitUntilDone();// may throw InterruptedException
338 }
339
340 outputPumper.waitUntilDone();// may throw InterruptedException
341 errorPumper .waitUntilDone();// may throw InterruptedException
342 }
343
344 /**
345 * Gets the shell environment variables for this process.
346 * Note that the returned mapping from variable names to
347 * values will always be case-sensitive regardless of the platform,
348 * i.e. <code>getSystemEnvVars().get("path")</code>
349 * and <code>getSystemEnvVars().get("PATH")</code>
350 * will in general return different values.
351 * However, on platforms
352 * with case-insensitive environment variables like Windows,
353 * all variable names will be normalized to upper case.
354 *
355 * @return
356 * The shell environment variables,
357 * can be empty but never <code>null</code>.
358 * @see System#getenv()
359 * System.getenv() API, new in JDK 5.0, to get the same result
360 * <b>since 2.0.2 System#getenv() will be used
361 * if available in the current running jvm.</b>
362 */
363 public static Properties getSystemEnvVars()
364 {
365 return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
366 }
367
368 /**
369 * Return the shell environment variables.
370 * If <code>caseSensitive == true</code>, then envar
371 * keys will all be upper-case.
372 *
373 * @param caseSensitive
374 * Whether environment variable keys should be treated case-sensitively.
375 * @return
376 * Properties object of (possibly modified) envar keys
377 * mapped to their values.
378 * @see System#getenv()
379 * System.getenv() API, new in JDK 5.0, to get the same result
380 * <b>since 2.0.2 System#getenv() will be used
381 * if available in the current running jvm.</b>
382 */
383 public static Properties getSystemEnvVars( boolean caseSensitive )
384 {
385 Properties envVars = new Properties();
386 Map<String, String> envs = System.getenv();
387 for ( String key : envs.keySet() )
388 {
389 String value = envs.get( key );
390 if ( !caseSensitive)
391 {
392 key = key.toUpperCase( Locale.ENGLISH );
393 }
394 envVars.put( key, value );
395 }
396 return envVars;
397 }
398
399 public static boolean isAlive( Process p )
400 {
401 if ( p == null )
402 {
403 return false;
404 }
405
406 try
407 {
408 p.exitValue();
409 return false;
410 }
411 catch ( IllegalThreadStateException e )
412 {
413 return true;
414 }
415 }
416
417 /**
418 *
419 * @throws CommandLineException
420 * If the quotes are not ballanced
421 */
422 public static String[] translateCommandline(String toProcess)
423 throws CommandLineException// thrown only once: explicitly
424 {
425 if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
426 {
427 return new String[0];
428 }
429
430 // parse with a simple finite state machine
431
432 final int normal = 0;
433 final int inQuote = 1;
434 final int inDoubleQuote = 2;
435 int state = normal;
436 StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
437 Vector<String> v = new Vector<String>();
438 StringBuilder current = new StringBuilder();
439
440 while ( tok.hasMoreTokens() )
441 {
442 String nextTok = tok.nextToken();
443 switch ( state )
444 {
445 case inQuote:
446 if ( "\'".equals( nextTok ) )
447 {
448 state = normal;
449 }
450 else
451 {
452 current.append( nextTok );
453 }
454 break;
455 case inDoubleQuote:
456 if ( "\"".equals( nextTok ) )
457 {
458 state = normal;
459 }
460 else
461 {
462 current.append( nextTok );
463 }
464 break;
465 default:
466 if ( "\'".equals( nextTok ) )
467 {
468 state = inQuote;
469 }
470 else if ( "\"".equals( nextTok ) )
471 {
472 state = inDoubleQuote;
473 }
474 else if ( " ".equals( nextTok ) )
475 {
476 if ( current.length() != 0 )
477 {
478 v.addElement( current.toString() );
479 current.setLength( 0 );
480 }
481 }
482 else
483 {
484 current.append( nextTok );
485 }
486 break;
487 }
488 }
489
490 if ( current.length() != 0 )
491 {
492 v.addElement( current.toString() );
493 }
494
495 if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
496 {
497 throw new CommandLineException
498 ("Unbalanced quotes in " + toProcess);
499 }
500
501 String[] args = new String[v.size()];
502 v.copyInto( args );
503 return args;
504 }
505
506 /**
507 * <p>Put quotes around the given String if necessary.</p>
508 * <p>If the argument doesn't include spaces or quotes, return it
509 * as is. If it contains double quotes, use single quotes - else
510 * surround the argument by double quotes.</p>
511 *
512 * @throws CommandLineException
513 * if the argument contains both, single and double quotes.
514 * @deprecated
515 * Use
516 * {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
517 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}
518 * or
519 * {@link StringUtils#quoteAndEscape(String, char)} instead.
520 */
521 @SuppressWarnings( { "JavaDoc", "deprecation" } )
522 public static String quote( String argument )
523 throws CommandLineException
524 {
525 // may throw CommandLineException
526 return quote(argument, false, false, true);
527 }
528
529 /**
530 * <p>Put quotes around the given String if necessary.</p>
531 * <p>If the argument doesn't include spaces or quotes, return it
532 * as is. If it contains double quotes, use single quotes - else
533 * surround the argument by double quotes.</p>
534 *
535 * @throws CommandLineException
536 * if the argument contains both, single and double quotes.
537 * @deprecated Use
538 * {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
539 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}
540 * or
541 * {@link StringUtils#quoteAndEscape(String, char)} instead.
542 */
543 @SuppressWarnings( { "JavaDoc", "UnusedDeclaration", "deprecation" } )
544 public static String quote( String argument, boolean wrapExistingQuotes )
545 throws CommandLineException
546 {
547 // may throw CommandLineException
548 return quote(argument, false, false, wrapExistingQuotes);
549 }
550
551 /**
552 * @deprecated Use
553 * {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
554 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}
555 * or
556 * {@link StringUtils#quoteAndEscape(String, char)} instead.
557 */
558 @SuppressWarnings( { "JavaDoc" } )
559 public static String quote(String argument,
560 boolean escapeSingleQuotes,
561 boolean escapeDoubleQuotes,
562 boolean wrapExistingQuotes)
563 throws CommandLineException// thrown once only: explicitly
564 {
565 if ( argument.contains( "\"" ) )
566 {
567 if ( argument.contains( "\'" ) )
568 {
569 throw new CommandLineException
570 ("Can't handle single and double quotes in same argument");
571 }
572 else
573 {
574 if ( escapeSingleQuotes )
575 {
576 return "\\\'" + argument + "\\\'";
577 }
578 else if ( wrapExistingQuotes )
579 {
580 return '\'' + argument + '\'';
581 }
582 }
583 }
584 else if ( argument.contains( "\'" ) )
585 {
586 if ( escapeDoubleQuotes )
587 {
588 return "\\\"" + argument + "\\\"";
589 }
590 else if ( wrapExistingQuotes )
591 {
592 return '\"' + argument + '\"';
593 }
594 }
595 else if ( argument.contains( " " ) )
596 {
597 if ( escapeDoubleQuotes )
598 {
599 return "\\\"" + argument + "\\\"";
600 }
601 else
602 {
603 return '\"' + argument + '\"';
604 }
605 }
606
607 return argument;
608 }
609
610 public static String toString( String[] line )
611 {
612 // empty path return empty string
613 if ( ( line == null ) || ( line.length == 0 ) )
614 {
615 return "";
616 }
617
618 // path containing one or more elements
619 final StringBuilder result = new StringBuilder();
620 for ( int i = 0; i < line.length; i++ )
621 {
622 if ( i > 0 )
623 {
624 result.append( ' ' );
625 }
626 try
627 {
628 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
629 }
630 catch ( Exception e )
631 {
632 System.err.println("Error quoting argument: " + e.getMessage());
633 }
634 }
635 return result.toString();
636 }
637
638 }