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  
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  
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          public void consumeLine( String line )
51          {
52              string.append( line ).append( ls );
53          }
54  
55          public String getOutput()
56          {
57              return string.toString();
58          }
59  
60      }
61  
62      /**
63       * Number of milliseconds per second.
64       */
65      private static final long MILLIS_PER_SECOND = 1000L;
66  
67      /**
68       * Number of nanoseconds per second.
69       */
70      private static final long NANOS_PER_SECOND = 1000000000L;
71  
72      public static int executeCommandLine( Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr )
73          throws CommandLineException
74      {
75          return executeCommandLine( cl, null, systemOut, systemErr, 0 );
76      }
77  
78      public static int executeCommandLine( Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr,
79                                            int timeoutInSeconds )
80          throws CommandLineException
81      {
82          return executeCommandLine( cl, null, systemOut, systemErr, timeoutInSeconds );
83      }
84  
85      public static int executeCommandLine( Commandline cl, InputStream systemIn, StreamConsumer systemOut,
86                                            StreamConsumer systemErr )
87          throws CommandLineException
88      {
89          return executeCommandLine( cl, systemIn, systemOut, systemErr, 0 );
90      }
91  
92      /**
93       * @param cl The command line to execute
94       * @param systemIn The input to read from, must be thread safe
95       * @param systemOut A consumer that receives output, must be thread safe
96       * @param systemErr A consumer that receives system error stream output, must be thread safe
97       * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
98       * @return A return value, see {@link Process#exitValue()}
99       * @throws CommandLineException or CommandLineTimeOutException if time out occurs
100      * @noinspection ThrowableResultOfMethodCallIgnored
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      * @noinspection ThrowableResultOfMethodCallIgnored
124      */
125     public static CommandLineCallable executeCommandLineAsCallable( final Commandline cl, final InputStream systemIn,
126                                                                     final StreamConsumer systemOut,
127                                                                     final StreamConsumer systemErr,
128                                                                     final int timeoutInSeconds )
129         throws CommandLineException
130     {
131         if ( cl == null )
132         {
133             throw new IllegalArgumentException( "cl cannot be null." );
134         }
135 
136         final Process p = cl.execute();
137 
138         final Thread processHook = new Thread()
139         {
140 
141             {
142                 this.setName( "CommandLineUtils process shutdown hook" );
143                 this.setContextClassLoader( null );
144             }
145 
146             @Override
147             public void run()
148             {
149                 p.destroy();
150             }
151 
152         };
153 
154         ShutdownHookUtils.addShutDownHook( processHook );
155 
156         return new CommandLineCallable()
157         {
158 
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      *
504      * @throws CommandLineException if the argument contains both, single and double quotes.
505      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
506      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
507      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
508      */
509     @SuppressWarnings( { "JavaDoc", "deprecation" } )
510     public static String quote( String argument )
511         throws CommandLineException
512     {
513         return quote( argument, false, false, true );
514     }
515 
516     /**
517      * <p>
518      * Put quotes around the given String if necessary.
519      * </p>
520      * <p>
521      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
522      * quotes - else surround the argument by double quotes.
523      * </p>
524      *
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     @SuppressWarnings( { "JavaDoc", "UnusedDeclaration", "deprecation" } )
531     public static String quote( String argument, boolean wrapExistingQuotes )
532         throws CommandLineException
533     {
534         return quote( argument, false, false, wrapExistingQuotes );
535     }
536 
537     /**
538      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
539      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
540      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
541      */
542     @SuppressWarnings( { "JavaDoc" } )
543     public static String quote( String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes,
544                                 boolean wrapExistingQuotes )
545         throws CommandLineException
546     {
547         if ( argument.contains( "\"" ) )
548         {
549             if ( argument.contains( "\'" ) )
550             {
551                 throw new CommandLineException( "Can't handle single and double quotes in same argument" );
552             }
553             else
554             {
555                 if ( escapeSingleQuotes )
556                 {
557                     return "\\\'" + argument + "\\\'";
558                 }
559                 else if ( wrapExistingQuotes )
560                 {
561                     return '\'' + argument + '\'';
562                 }
563             }
564         }
565         else if ( argument.contains( "\'" ) )
566         {
567             if ( escapeDoubleQuotes )
568             {
569                 return "\\\"" + argument + "\\\"";
570             }
571             else if ( wrapExistingQuotes )
572             {
573                 return '\"' + argument + '\"';
574             }
575         }
576         else if ( argument.contains( " " ) )
577         {
578             if ( escapeDoubleQuotes )
579             {
580                 return "\\\"" + argument + "\\\"";
581             }
582             else
583             {
584                 return '\"' + argument + '\"';
585             }
586         }
587 
588         return argument;
589     }
590 
591     public static String toString( String[] line )
592     {
593         // empty path return empty string
594         if ( ( line == null ) || ( line.length == 0 ) )
595         {
596             return "";
597         }
598 
599         // path containing one or more elements
600         final StringBuilder result = new StringBuilder();
601         for ( int i = 0; i < line.length; i++ )
602         {
603             if ( i > 0 )
604             {
605                 result.append( ' ' );
606             }
607             try
608             {
609                 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
610             }
611             catch ( Exception e )
612             {
613                 System.err.println( "Error quoting argument: " + e.getMessage() );
614             }
615         }
616         return result.toString();
617     }
618 
619 }