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 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
358 * will be used if available in the current running jvm.</b>
359 */
360 public static Properties getSystemEnvVars()
361 {
362 return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
363 }
364
365 /**
366 * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar keys will all be
367 * upper-case.
368 *
369 * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
370 * @return Properties object of (possibly modified) envar keys mapped to their values.
371 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
372 * will be used if available in the current running jvm.</b>
373 */
374 public static Properties getSystemEnvVars( boolean caseSensitive )
375 {
376 Properties envVars = new Properties();
377 Map<String, String> envs = System.getenv();
378 for ( String key : envs.keySet() )
379 {
380 String value = envs.get( key );
381 if ( !caseSensitive )
382 {
383 key = key.toUpperCase( Locale.ENGLISH );
384 }
385 envVars.put( key, value );
386 }
387 return envVars;
388 }
389
390 public static boolean isAlive( Process p )
391 {
392 if ( p == null )
393 {
394 return false;
395 }
396
397 try
398 {
399 p.exitValue();
400 return false;
401 }
402 catch ( IllegalThreadStateException e )
403 {
404 return true;
405 }
406 }
407
408 public static String[] translateCommandline( String toProcess )
409 throws Exception
410 {
411 if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
412 {
413 return new String[0];
414 }
415
416 // parse with a simple finite state machine
417
418 final int normal = 0;
419 final int inQuote = 1;
420 final int inDoubleQuote = 2;
421 int state = normal;
422 StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
423 Vector<String> v = new Vector<String>();
424 StringBuilder current = new StringBuilder();
425
426 while ( tok.hasMoreTokens() )
427 {
428 String nextTok = tok.nextToken();
429 switch ( state )
430 {
431 case inQuote:
432 if ( "\'".equals( nextTok ) )
433 {
434 state = normal;
435 }
436 else
437 {
438 current.append( nextTok );
439 }
440 break;
441 case inDoubleQuote:
442 if ( "\"".equals( nextTok ) )
443 {
444 state = normal;
445 }
446 else
447 {
448 current.append( nextTok );
449 }
450 break;
451 default:
452 if ( "\'".equals( nextTok ) )
453 {
454 state = inQuote;
455 }
456 else if ( "\"".equals( nextTok ) )
457 {
458 state = inDoubleQuote;
459 }
460 else if ( " ".equals( nextTok ) )
461 {
462 if ( current.length() != 0 )
463 {
464 v.addElement( current.toString() );
465 current.setLength( 0 );
466 }
467 }
468 else
469 {
470 current.append( nextTok );
471 }
472 break;
473 }
474 }
475
476 if ( current.length() != 0 )
477 {
478 v.addElement( current.toString() );
479 }
480
481 if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
482 {
483 throw new CommandLineException( "unbalanced quotes in " + toProcess );
484 }
485
486 String[] args = new String[v.size()];
487 v.copyInto( args );
488 return args;
489 }
490
491 /**
492 * <p>
493 * Put quotes around the given String if necessary.
494 * </p>
495 * <p>
496 * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
497 * quotes - else surround the argument by double quotes.
498 * </p>
499 * @param argument the argument
500 * @return the transformed command line
501 * @throws CommandLineException if the argument contains both, single and double quotes.
502 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
503 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
504 * {@link StringUtils#quoteAndEscape(String, char)} instead.
505 */
506 @Deprecated
507 @SuppressWarnings( { "JavaDoc", "deprecation" } )
508 public static String quote( String argument )
509 throws CommandLineException
510 {
511 return quote( argument, false, false, true );
512 }
513
514 /**
515 * <p>
516 * Put quotes around the given String if necessary.
517 * </p>
518 * <p>
519 * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
520 * quotes - else surround the argument by double quotes.
521 * </p>
522 * @param argument see name
523 * @param wrapExistingQuotes see name
524 * @return the transformed command line
525 * @throws CommandLineException if the argument contains both, single and double quotes.
526 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
527 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
528 * {@link StringUtils#quoteAndEscape(String, char)} instead.
529 */
530 @Deprecated
531 @SuppressWarnings( { "JavaDoc", "UnusedDeclaration", "deprecation" } )
532 public static String quote( String argument, boolean wrapExistingQuotes )
533 throws CommandLineException
534 {
535 return quote( argument, false, false, wrapExistingQuotes );
536 }
537
538 /**
539 * @param argument the argument
540 * @param escapeSingleQuotes see name
541 * @param escapeDoubleQuotes see name
542 * @param wrapExistingQuotes see name
543 * @return the transformed command line
544 * @throws CommandLineException some trouble
545 * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
546 * {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
547 * {@link StringUtils#quoteAndEscape(String, char)} instead.
548 */
549 @Deprecated
550 @SuppressWarnings( { "JavaDoc" } )
551 public static String quote( String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes,
552 boolean wrapExistingQuotes )
553 throws CommandLineException
554 {
555 if ( argument.contains( "\"" ) )
556 {
557 if ( argument.contains( "\'" ) )
558 {
559 throw new CommandLineException( "Can't handle single and double quotes in same argument" );
560 }
561 else
562 {
563 if ( escapeSingleQuotes )
564 {
565 return "\\\'" + argument + "\\\'";
566 }
567 else if ( wrapExistingQuotes )
568 {
569 return '\'' + argument + '\'';
570 }
571 }
572 }
573 else if ( argument.contains( "\'" ) )
574 {
575 if ( escapeDoubleQuotes )
576 {
577 return "\\\"" + argument + "\\\"";
578 }
579 else if ( wrapExistingQuotes )
580 {
581 return '\"' + argument + '\"';
582 }
583 }
584 else if ( argument.contains( " " ) )
585 {
586 if ( escapeDoubleQuotes )
587 {
588 return "\\\"" + argument + "\\\"";
589 }
590 else
591 {
592 return '\"' + argument + '\"';
593 }
594 }
595
596 return argument;
597 }
598
599 public static String toString( String[] line )
600 {
601 // empty path return empty string
602 if ( ( line == null ) || ( line.length == 0 ) )
603 {
604 return "";
605 }
606
607 // path containing one or more elements
608 final StringBuilder result = new StringBuilder();
609 for ( int i = 0; i < line.length; i++ )
610 {
611 if ( i > 0 )
612 {
613 result.append( ' ' );
614 }
615 try
616 {
617 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
618 }
619 catch ( Exception e )
620 {
621 System.err.println( "Error quoting argument: " + e.getMessage() );
622 }
623 }
624 return result.toString();
625 }
626
627 }