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