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