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   *
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      * @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      * @param argument the argument
504      * @return the transformed command line
505      * @throws CommandLineException if the argument contains both, single and double quotes.
506      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
507      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
508      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
509      */
510     @Deprecated
511     @SuppressWarnings( { "JavaDoc", "deprecation" } )
512     public static String quote( String argument )
513         throws CommandLineException
514     {
515         return quote( argument, false, false, true );
516     }
517 
518     /**
519      * <p>
520      * Put quotes around the given String if necessary.
521      * </p>
522      * <p>
523      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
524      * quotes - else surround the argument by double quotes.
525      * </p>
526      * @param argument see name
527      * @param wrapExistingQuotes see name
528      * @return the transformed command line
529      * @throws CommandLineException if the argument contains both, single and double quotes.
530      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
531      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
532      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
533      */
534     @Deprecated
535     @SuppressWarnings( { "JavaDoc", "UnusedDeclaration", "deprecation" } )
536     public static String quote( String argument, boolean wrapExistingQuotes )
537         throws CommandLineException
538     {
539         return quote( argument, false, false, wrapExistingQuotes );
540     }
541 
542     /**
543      * @param argument the argument
544      * @param escapeSingleQuotes see name
545      * @param escapeDoubleQuotes see name
546      * @param wrapExistingQuotes see name
547      * @return the transformed command line
548      * @throws CommandLineException some trouble
549      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
550      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
551      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
552      */
553     @Deprecated
554     @SuppressWarnings( { "JavaDoc" } )
555     public static String quote( String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes,
556                                 boolean wrapExistingQuotes )
557         throws CommandLineException
558     {
559         if ( argument.contains( "\"" ) )
560         {
561             if ( argument.contains( "\'" ) )
562             {
563                 throw new CommandLineException( "Can't handle single and double quotes in same argument" );
564             }
565             else
566             {
567                 if ( escapeSingleQuotes )
568                 {
569                     return "\\\'" + argument + "\\\'";
570                 }
571                 else if ( wrapExistingQuotes )
572                 {
573                     return '\'' + argument + '\'';
574                 }
575             }
576         }
577         else if ( argument.contains( "\'" ) )
578         {
579             if ( escapeDoubleQuotes )
580             {
581                 return "\\\"" + argument + "\\\"";
582             }
583             else if ( wrapExistingQuotes )
584             {
585                 return '\"' + argument + '\"';
586             }
587         }
588         else if ( argument.contains( " " ) )
589         {
590             if ( escapeDoubleQuotes )
591             {
592                 return "\\\"" + argument + "\\\"";
593             }
594             else
595             {
596                 return '\"' + argument + '\"';
597             }
598         }
599 
600         return argument;
601     }
602 
603     public static String toString( String[] line )
604     {
605         // empty path return empty string
606         if ( ( line == null ) || ( line.length == 0 ) )
607         {
608             return "";
609         }
610 
611         // path containing one or more elements
612         final StringBuilder result = new StringBuilder();
613         for ( int i = 0; i < line.length; i++ )
614         {
615             if ( i > 0 )
616             {
617                 result.append( ' ' );
618             }
619             try
620             {
621                 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
622             }
623             catch ( Exception e )
624             {
625                 System.err.println( "Error quoting argument: " + e.getMessage() );
626             }
627         }
628         return result.toString();
629     }
630 
631 }