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.InputStream;
20  import java.util.Locale;
21  import java.util.Map;
22  import java.util.Properties;
23  import java.util.StringTokenizer;
24  import java.util.Vector;
25  
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   *
32   */
33  public abstract class CommandLineUtils {
34  
35      /**
36       * A {@code StreamConsumer} providing consumed lines as a {@code String}.
37       *
38       * @see #getOutput()
39       */
40      public static class StringStreamConsumer implements StreamConsumer {
41  
42          private StringBuffer string = new StringBuffer();
43  
44          private String ls = System.getProperty("line.separator");
45  
46          @Override
47          public void consumeLine(String line) {
48              string.append(line).append(ls);
49          }
50  
51          public String getOutput() {
52              return string.toString();
53          }
54      }
55  
56      /**
57       * Number of milliseconds per second.
58       */
59      private static final long MILLIS_PER_SECOND = 1000L;
60  
61      /**
62       * Number of nanoseconds per second.
63       */
64      private static final long NANOS_PER_SECOND = 1000000000L;
65  
66      public static int executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr)
67              throws CommandLineException {
68          return executeCommandLine(cl, null, systemOut, systemErr, 0);
69      }
70  
71      public static int executeCommandLine(
72              Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds)
73              throws CommandLineException {
74          return executeCommandLine(cl, null, systemOut, systemErr, timeoutInSeconds);
75      }
76  
77      public static int executeCommandLine(
78              Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr)
79              throws CommandLineException {
80          return executeCommandLine(cl, systemIn, systemOut, systemErr, 0);
81      }
82  
83      /**
84       * @param cl The command line to execute
85       * @param systemIn The input to read from, must be thread safe
86       * @param systemOut A consumer that receives output, must be thread safe
87       * @param systemErr A consumer that receives system error stream output, must be thread safe
88       * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
89       * @return A return value, see {@link Process#exitValue()}
90       * @throws CommandLineException or CommandLineTimeOutException if time out occurs
91       */
92      public static int executeCommandLine(
93              Commandline cl,
94              InputStream systemIn,
95              StreamConsumer systemOut,
96              StreamConsumer systemErr,
97              int timeoutInSeconds)
98              throws CommandLineException {
99          final CommandLineCallable future =
100                 executeCommandLineAsCallable(cl, systemIn, systemOut, systemErr, timeoutInSeconds);
101         return future.call();
102     }
103 
104     /**
105      * Immediately forks a process, returns a callable that will block until process is complete.
106      *
107      * @param cl The command line to execute
108      * @param systemIn The input to read from, must be thread safe
109      * @param systemOut A consumer that receives output, must be thread safe
110      * @param systemErr A consumer that receives system error stream output, must be thread safe
111      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
112      * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
113      *         must be called on this to be sure the forked process has terminated, no guarantees is made about any
114      *         internal state before after the completion of the call statements
115      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
116      */
117     public static CommandLineCallable executeCommandLineAsCallable(
118             final Commandline cl,
119             final InputStream systemIn,
120             final StreamConsumer systemOut,
121             final StreamConsumer systemErr,
122             final int timeoutInSeconds)
123             throws CommandLineException {
124         if (cl == null) {
125             throw new IllegalArgumentException("cl cannot be null.");
126         }
127 
128         final Process p = cl.execute();
129 
130         final Thread processHook = new Thread() {
131 
132             {
133                 this.setName("CommandLineUtils process shutdown hook");
134                 this.setContextClassLoader(null);
135             }
136 
137             @Override
138             public void run() {
139                 p.destroy();
140             }
141         };
142 
143         ShutdownHookUtils.addShutDownHook(processHook);
144 
145         return new CommandLineCallable() {
146 
147             @Override
148             public Integer call() throws CommandLineException {
149                 StreamFeeder inputFeeder = null;
150                 StreamPumper outputPumper = null;
151                 StreamPumper errorPumper = null;
152                 boolean success = false;
153                 try {
154                     if (systemIn != null) {
155                         inputFeeder = new StreamFeeder(systemIn, p.getOutputStream());
156                         inputFeeder.start();
157                     }
158 
159                     outputPumper = new StreamPumper(p.getInputStream(), systemOut);
160                     outputPumper.start();
161 
162                     errorPumper = new StreamPumper(p.getErrorStream(), systemErr);
163                     errorPumper.start();
164 
165                     int returnValue;
166                     if (timeoutInSeconds <= 0) {
167                         returnValue = p.waitFor();
168                     } else {
169                         final long now = System.nanoTime();
170                         final long timeout = now + NANOS_PER_SECOND * timeoutInSeconds;
171 
172                         while (isAlive(p) && (System.nanoTime() < timeout)) {
173                             // The timeout is specified in seconds. Therefore we must not sleep longer than one second
174                             // but we should sleep as long as possible to reduce the number of iterations performed.
175                             Thread.sleep(MILLIS_PER_SECOND - 1L);
176                         }
177 
178                         if (isAlive(p)) {
179                             throw new InterruptedException(
180                                     String.format("Process timed out after %d seconds.", timeoutInSeconds));
181                         }
182 
183                         returnValue = p.exitValue();
184                     }
185 
186                     // TODO Find out if waitUntilDone needs to be called using a try-finally construct. The method may
187                     // throw an
188                     // InterruptedException so that calls to waitUntilDone may be skipped.
189                     // try
190                     // {
191                     // if ( inputFeeder != null )
192                     // {
193                     // inputFeeder.waitUntilDone();
194                     // }
195                     // }
196                     // finally
197                     // {
198                     // try
199                     // {
200                     // outputPumper.waitUntilDone();
201                     // }
202                     // finally
203                     // {
204                     // errorPumper.waitUntilDone();
205                     // }
206                     // }
207                     if (inputFeeder != null) {
208                         inputFeeder.waitUntilDone();
209                     }
210 
211                     outputPumper.waitUntilDone();
212                     errorPumper.waitUntilDone();
213 
214                     if (inputFeeder != null) {
215                         inputFeeder.close();
216                         handleException(inputFeeder, "stdin");
217                     }
218 
219                     outputPumper.close();
220                     handleException(outputPumper, "stdout");
221 
222                     errorPumper.close();
223                     handleException(errorPumper, "stderr");
224 
225                     success = true;
226                     return returnValue;
227                 } catch (InterruptedException ex) {
228                     throw new CommandLineTimeOutException(
229                             "Error while executing external command, process killed.", ex);
230 
231                 } finally {
232                     if (inputFeeder != null) {
233                         inputFeeder.disable();
234                     }
235                     if (outputPumper != null) {
236                         outputPumper.disable();
237                     }
238                     if (errorPumper != null) {
239                         errorPumper.disable();
240                     }
241 
242                     try {
243                         ShutdownHookUtils.removeShutdownHook(processHook);
244                         processHook.run();
245                     } finally {
246                         try {
247                             if (inputFeeder != null) {
248                                 inputFeeder.close();
249 
250                                 if (success) {
251                                     success = false;
252                                     handleException(inputFeeder, "stdin");
253                                     success = true; // Only reached when no exception has been thrown.
254                                 }
255                             }
256                         } finally {
257                             try {
258                                 if (outputPumper != null) {
259                                     outputPumper.close();
260 
261                                     if (success) {
262                                         success = false;
263                                         handleException(outputPumper, "stdout");
264                                         success = true; // Only reached when no exception has been thrown.
265                                     }
266                                 }
267                             } finally {
268                                 if (errorPumper != null) {
269                                     errorPumper.close();
270 
271                                     if (success) {
272                                         handleException(errorPumper, "stderr");
273                                     }
274                                 }
275                             }
276                         }
277                     }
278                 }
279             }
280         };
281     }
282 
283     private static void handleException(final StreamPumper streamPumper, final String streamName)
284             throws CommandLineException {
285         if (streamPumper.getException() != null) {
286             throw new CommandLineException(
287                     String.format("Failure processing %s.", streamName), streamPumper.getException());
288         }
289     }
290 
291     private static void handleException(final StreamFeeder streamFeeder, final String streamName)
292             throws CommandLineException {
293         if (streamFeeder.getException() != null) {
294             throw new CommandLineException(
295                     String.format("Failure processing %s.", streamName), streamFeeder.getException());
296         }
297     }
298 
299     /**
300      * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
301      * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
302      * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
303      * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
304      *
305      * @return The shell environment variables, can be empty but never <code>null</code>.
306      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
307      *      will be used if available in the current running jvm.</b>
308      */
309     public static Properties getSystemEnvVars() {
310         return getSystemEnvVars(!Os.isFamily(Os.FAMILY_WINDOWS));
311     }
312 
313     /**
314      * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar keys will all be
315      * upper-case.
316      *
317      * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
318      * @return Properties object of (possibly modified) envar keys mapped to their values.
319      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv()
320      *      will be used if available in the current running jvm.</b>
321      */
322     public static Properties getSystemEnvVars(boolean caseSensitive) {
323         Properties envVars = new Properties();
324         Map<String, String> envs = System.getenv();
325         for (String key : envs.keySet()) {
326             String value = envs.get(key);
327             if (!caseSensitive) {
328                 key = key.toUpperCase(Locale.ENGLISH);
329             }
330             envVars.put(key, value);
331         }
332         return envVars;
333     }
334 
335     public static boolean isAlive(Process p) {
336         if (p == null) {
337             return false;
338         }
339 
340         try {
341             p.exitValue();
342             return false;
343         } catch (IllegalThreadStateException e) {
344             return true;
345         }
346     }
347 
348     public static String[] translateCommandline(String toProcess) throws Exception {
349         if ((toProcess == null) || (toProcess.length() == 0)) {
350             return new String[0];
351         }
352 
353         // parse with a simple finite state machine
354 
355         final int normal = 0;
356         final int inQuote = 1;
357         final int inDoubleQuote = 2;
358         int state = normal;
359         StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
360         Vector<String> v = new Vector<String>();
361         StringBuilder current = new StringBuilder();
362 
363         while (tok.hasMoreTokens()) {
364             String nextTok = tok.nextToken();
365             switch (state) {
366                 case inQuote:
367                     if ("\'".equals(nextTok)) {
368                         state = normal;
369                     } else {
370                         current.append(nextTok);
371                     }
372                     break;
373                 case inDoubleQuote:
374                     if ("\"".equals(nextTok)) {
375                         state = normal;
376                     } else {
377                         current.append(nextTok);
378                     }
379                     break;
380                 default:
381                     if ("\'".equals(nextTok)) {
382                         state = inQuote;
383                     } else if ("\"".equals(nextTok)) {
384                         state = inDoubleQuote;
385                     } else if (" ".equals(nextTok)) {
386                         if (current.length() != 0) {
387                             v.addElement(current.toString());
388                             current.setLength(0);
389                         }
390                     } else {
391                         current.append(nextTok);
392                     }
393                     break;
394             }
395         }
396 
397         if (current.length() != 0) {
398             v.addElement(current.toString());
399         }
400 
401         if ((state == inQuote) || (state == inDoubleQuote)) {
402             throw new CommandLineException("unbalanced quotes in " + toProcess);
403         }
404 
405         String[] args = new String[v.size()];
406         v.copyInto(args);
407         return args;
408     }
409 
410     /**
411      * <p>
412      * Put quotes around the given String if necessary.
413      * </p>
414      * <p>
415      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
416      * quotes - else surround the argument by double quotes.
417      * </p>
418      * @param argument the argument
419      * @return the transformed command line
420      * @throws CommandLineException if the argument contains both, single and double quotes.
421      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
422      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
423      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
424      */
425     @Deprecated
426     @SuppressWarnings({"JavaDoc", "deprecation"})
427     public static String quote(String argument) throws CommandLineException {
428         return quote(argument, false, false, true);
429     }
430 
431     /**
432      * <p>
433      * Put quotes around the given String if necessary.
434      * </p>
435      * <p>
436      * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single
437      * quotes - else surround the argument by double quotes.
438      * </p>
439      * @param argument see name
440      * @param wrapExistingQuotes see name
441      * @return the transformed command line
442      * @throws CommandLineException if the argument contains both, single and double quotes.
443      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
444      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
445      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
446      */
447     @Deprecated
448     @SuppressWarnings({"JavaDoc", "UnusedDeclaration", "deprecation"})
449     public static String quote(String argument, boolean wrapExistingQuotes) throws CommandLineException {
450         return quote(argument, false, false, wrapExistingQuotes);
451     }
452 
453     /**
454      * @param argument the argument
455      * @param escapeSingleQuotes see name
456      * @param escapeDoubleQuotes see name
457      * @param wrapExistingQuotes see name
458      * @return the transformed command line
459      * @throws CommandLineException some trouble
460      * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)},
461      *             {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
462      *             {@link StringUtils#quoteAndEscape(String, char)} instead.
463      */
464     @Deprecated
465     @SuppressWarnings({"JavaDoc"})
466     public static String quote(
467             String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes, boolean wrapExistingQuotes)
468             throws CommandLineException {
469         if (argument.contains("\"")) {
470             if (argument.contains("\'")) {
471                 throw new CommandLineException("Can't handle single and double quotes in same argument");
472             } else {
473                 if (escapeSingleQuotes) {
474                     return "\\\'" + argument + "\\\'";
475                 } else if (wrapExistingQuotes) {
476                     return '\'' + argument + '\'';
477                 }
478             }
479         } else if (argument.contains("\'")) {
480             if (escapeDoubleQuotes) {
481                 return "\\\"" + argument + "\\\"";
482             } else if (wrapExistingQuotes) {
483                 return '\"' + argument + '\"';
484             }
485         } else if (argument.contains(" ")) {
486             if (escapeDoubleQuotes) {
487                 return "\\\"" + argument + "\\\"";
488             } else {
489                 return '\"' + argument + '\"';
490             }
491         }
492 
493         return argument;
494     }
495 
496     public static String toString(String[] line) {
497         // empty path return empty string
498         if ((line == null) || (line.length == 0)) {
499             return "";
500         }
501 
502         // path containing one or more elements
503         final StringBuilder result = new StringBuilder();
504         for (int i = 0; i < line.length; i++) {
505             if (i > 0) {
506                 result.append(' ');
507             }
508             try {
509                 result.append(StringUtils.quoteAndEscape(line[i], '\"'));
510             } catch (Exception e) {
511                 System.err.println("Error quoting argument: " + e.getMessage());
512             }
513         }
514         return result.toString();
515     }
516 }