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