View Javadoc
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&oslash;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&lt;=0</code> 
153      *    thrown by {@link Process#waitFor()}. 
154      *    If <code>timeoutInSeconds &gt; 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 }