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      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
358      *      will be used if available in the current running jvm.</b>
359      */
360     public static Properties getSystemEnvVars()
361     {
362         return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
363     }
364 
365     /**
366      * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar keys will all be
367      * upper-case.
368      *
369      * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
370      * @return Properties object of (possibly modified) envar keys mapped to their values.
371      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
372      *      will be used if available in the current running jvm.</b>
373      */
374     public static Properties getSystemEnvVars( boolean caseSensitive )
375     {
376         Properties envVars = new Properties();
377         Map<String, String> envs = System.getenv();
378         for ( String key : envs.keySet() )
379         {
380             String value = envs.get( key );
381             if ( !caseSensitive )
382             {
383                 key = key.toUpperCase( Locale.ENGLISH );
384             }
385             envVars.put( key, value );
386         }
387         return envVars;
388     }
389 
390     public static boolean isAlive( Process p )
391     {
392         if ( p == null )
393         {
394             return false;
395         }
396 
397         try
398         {
399             p.exitValue();
400             return false;
401         }
402         catch ( IllegalThreadStateException e )
403         {
404             return true;
405         }
406     }
407 
408     public static String[] translateCommandline( String toProcess )
409         throws Exception
410     {
411         if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
412         {
413             return new String[0];
414         }
415 
416         // parse with a simple finite state machine
417 
418         final int normal = 0;
419         final int inQuote = 1;
420         final int inDoubleQuote = 2;
421         int state = normal;
422         StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
423         Vector<String> v = new Vector<String>();
424         StringBuilder current = new StringBuilder();
425 
426         while ( tok.hasMoreTokens() )
427         {
428             String nextTok = tok.nextToken();
429             switch ( state )
430             {
431                 case inQuote:
432                     if ( "\'".equals( nextTok ) )
433                     {
434                         state = normal;
435                     }
436                     else
437                     {
438                         current.append( nextTok );
439                     }
440                     break;
441                 case inDoubleQuote:
442                     if ( "\"".equals( nextTok ) )
443                     {
444                         state = normal;
445                     }
446                     else
447                     {
448                         current.append( nextTok );
449                     }
450                     break;
451                 default:
452                     if ( "\'".equals( nextTok ) )
453                     {
454                         state = inQuote;
455                     }
456                     else if ( "\"".equals( nextTok ) )
457                     {
458                         state = inDoubleQuote;
459                     }
460                     else if ( " ".equals( nextTok ) )
461                     {
462                         if ( current.length() != 0 )
463                         {
464                             v.addElement( current.toString() );
465                             current.setLength( 0 );
466                         }
467                     }
468                     else
469                     {
470                         current.append( nextTok );
471                     }
472                     break;
473             }
474         }
475 
476         if ( current.length() != 0 )
477         {
478             v.addElement( current.toString() );
479         }
480 
481         if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
482         {
483             throw new CommandLineException( "unbalanced quotes in " + toProcess );
484         }
485 
486         String[] args = new String[v.size()];
487         v.copyInto( args );
488         return args;
489     }
490 
491     /**
492      * <p>
493      * Put quotes around the given String if necessary.
494      * </p>
495      * <p>
496      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
497      * quotes - else surround the argument by double quotes.
498      * </p>
499      * @param argument the argument
500      * @return the transformed command line
501      * @throws CommandLineException if the argument contains both, single and double quotes.
502      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
503      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
504      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
505      */
506     @Deprecated
507     @SuppressWarnings( { "JavaDoc", "deprecation" } )
508     public static String quote( String argument )
509         throws CommandLineException
510     {
511         return quote( argument, false, false, true );
512     }
513 
514     /**
515      * <p>
516      * Put quotes around the given String if necessary.
517      * </p>
518      * <p>
519      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
520      * quotes - else surround the argument by double quotes.
521      * </p>
522      * @param argument see name
523      * @param wrapExistingQuotes see name
524      * @return the transformed command line
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     @Deprecated
531     @SuppressWarnings( { "JavaDoc", "UnusedDeclaration", "deprecation" } )
532     public static String quote( String argument, boolean wrapExistingQuotes )
533         throws CommandLineException
534     {
535         return quote( argument, false, false, wrapExistingQuotes );
536     }
537 
538     /**
539      * @param argument the argument
540      * @param escapeSingleQuotes see name
541      * @param escapeDoubleQuotes see name
542      * @param wrapExistingQuotes see name
543      * @return the transformed command line
544      * @throws CommandLineException some trouble
545      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
546      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
547      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
548      */
549     @Deprecated
550     @SuppressWarnings( { "JavaDoc" } )
551     public static String quote( String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes,
552                                 boolean wrapExistingQuotes )
553         throws CommandLineException
554     {
555         if ( argument.contains( "\"" ) )
556         {
557             if ( argument.contains( "\'" ) )
558             {
559                 throw new CommandLineException( "Can't handle single and double quotes in same argument" );
560             }
561             else
562             {
563                 if ( escapeSingleQuotes )
564                 {
565                     return "\\\'" + argument + "\\\'";
566                 }
567                 else if ( wrapExistingQuotes )
568                 {
569                     return '\'' + argument + '\'';
570                 }
571             }
572         }
573         else if ( argument.contains( "\'" ) )
574         {
575             if ( escapeDoubleQuotes )
576             {
577                 return "\\\"" + argument + "\\\"";
578             }
579             else if ( wrapExistingQuotes )
580             {
581                 return '\"' + argument + '\"';
582             }
583         }
584         else if ( argument.contains( " " ) )
585         {
586             if ( escapeDoubleQuotes )
587             {
588                 return "\\\"" + argument + "\\\"";
589             }
590             else
591             {
592                 return '\"' + argument + '\"';
593             }
594         }
595 
596         return argument;
597     }
598 
599     public static String toString( String[] line )
600     {
601         // empty path return empty string
602         if ( ( line == null ) || ( line.length == 0 ) )
603         {
604             return "";
605         }
606 
607         // path containing one or more elements
608         final StringBuilder result = new StringBuilder();
609         for ( int i = 0; i < line.length; i++ )
610         {
611             if ( i > 0 )
612             {
613                 result.append( ' ' );
614             }
615             try
616             {
617                 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
618             }
619             catch ( Exception e )
620             {
621                 System.err.println( "Error quoting argument: " + e.getMessage() );
622             }
623         }
624         return result.toString();
625     }
626 
627 }