001    package org.codehaus.plexus.util.cli;
002    
003    /*
004     * Copyright The Codehaus Foundation.
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    import java.io.IOException;
020    import java.io.InputStream;
021    import java.util.Locale;
022    import java.util.Map;
023    import java.util.Properties;
024    import java.util.StringTokenizer;
025    import java.util.Vector;
026    
027    import org.codehaus.plexus.util.Os;
028    import org.codehaus.plexus.util.StringUtils;
029    import org.kuali.common.util.Assert;
030    
031    public abstract class CommandLineUtils {
032    
033            public static int executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr) throws CommandLineException {
034                    return executeCommandLine(cl, null, systemOut, systemErr, 0);
035            }
036    
037            public static int executeCommandLine(Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds) throws CommandLineException {
038                    return executeCommandLine(cl, null, systemOut, systemErr, timeoutInSeconds);
039            }
040    
041            public static int executeCommandLine(Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr) throws CommandLineException {
042                    return executeCommandLine(cl, systemIn, systemOut, systemErr, 0);
043            }
044    
045            /**
046             * @param cl
047             *            The command line to execute
048             * @param systemIn
049             *            The input to read from, must be thread safe
050             * @param systemOut
051             *            A consumer that receives output, must be thread safe
052             * @param systemErr
053             *            A consumer that receives system error stream output, must be thread safe
054             * @param timeoutInSeconds
055             *            Positive integer to specify timeout, zero and negative integers for no timeout.
056             * @return A return value, see {@link Process#exitValue()}
057             * @throws CommandLineException
058             *             or CommandLineTimeOutException if time out occurs
059             * @noinspection ThrowableResultOfMethodCallIgnored
060             */
061            public static int executeCommandLine(Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds)
062                            throws CommandLineException {
063                    final CommandLineCallable future = executeCommandLineAsCallable(cl, systemIn, systemOut, systemErr, timeoutInSeconds);
064                    return future.call();
065            }
066    
067            /**
068             * Immediately forks a process, returns a callable that will block until process is complete.
069             * 
070             * @param cl
071             *            The command line to execute
072             * @param systemIn
073             *            The input to read from, must be thread safe
074             * @param systemOut
075             *            A consumer that receives output, must be thread safe
076             * @param systemErr
077             *            A consumer that receives system error stream output, must be thread safe
078             * @param timeoutInSeconds
079             *            Positive integer to specify timeout, zero and negative integers for no timeout.
080             * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call" must be called on this to be sure the forked process has
081             *         terminated, no guarantees is made about any internal state before the completion of the call statement
082             * @throws CommandLineException
083             *             or CommandLineTimeOutException if time out occurs
084             * @noinspection ThrowableResultOfMethodCallIgnored
085             */
086            public static CommandLineCallable executeCommandLineAsCallable(Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds)
087                            throws CommandLineException {
088    
089                    Assert.noNulls(cl);
090    
091                    final Process p = cl.execute();
092                    final StreamFeeder inputFeeder = systemIn != null ? new StreamFeeder(systemIn, p.getOutputStream()) : null;
093                    final StreamPumper outputPumper = new StreamPumper(p.getInputStream(), systemOut);
094                    final StreamPumper errorPumper = new StreamPumper(p.getErrorStream(), systemErr);
095    
096                    if (inputFeeder != null) {
097                            inputFeeder.start();
098                    }
099    
100                    outputPumper.start();
101                    errorPumper.start();
102    
103                    final ProcessHook processHook = new ProcessHook(p);
104    
105                    ShutdownHookUtils.addShutDownHook(processHook);
106    
107                    return new CLICallable(timeoutInSeconds, inputFeeder, outputPumper, errorPumper, p, processHook);
108    
109            }
110    
111            public static void waitForAllPumpers(StreamFeeder inputFeeder, StreamPumper outputPumper, StreamPumper errorPumper) throws InterruptedException {
112                    if (inputFeeder != null) {
113                            inputFeeder.waitUntilDone();
114                    }
115    
116                    outputPumper.waitUntilDone();
117                    errorPumper.waitUntilDone();
118            }
119    
120            /**
121             * Gets the shell environment variables for this process. Note that the returned mapping from variable names to values will always be case-sensitive regardless of the platform,
122             * i.e. <code>getSystemEnvVars().get("path")</code> and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms with
123             * case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
124             * 
125             * @return The shell environment variables, can be empty but never <code>null</code>.
126             * @throws IOException
127             *             If the environment variables could not be queried from the shell.
128             * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
129             */
130            public static Properties getSystemEnvVars() throws IOException {
131                    return getSystemEnvVars(!Os.isFamily(Os.FAMILY_WINDOWS));
132            }
133    
134            /**
135             * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar keys will all be upper-case.
136             * 
137             * @param caseSensitive
138             *            Whether environment variable keys should be treated case-sensitively.
139             * @return Properties object of (possibly modified) envar keys mapped to their values.
140             * @throws IOException .
141             * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
142             */
143            public static Properties getSystemEnvVars(boolean caseSensitive) throws IOException {
144                    Properties envVars = new Properties();
145                    Map<String, String> envs = System.getenv();
146                    for (String key : envs.keySet()) {
147                            String value = envs.get(key);
148                            if (!caseSensitive) {
149                                    key = key.toUpperCase(Locale.ENGLISH);
150                            }
151                            envVars.put(key, value);
152                    }
153                    return envVars;
154            }
155    
156            public static boolean isAlive(Process p) {
157                    if (p == null) {
158                            return false;
159                    }
160    
161                    try {
162                            p.exitValue();
163                            return false;
164                    } catch (IllegalThreadStateException e) {
165                            return true;
166                    }
167            }
168    
169            public static String[] translateCommandline(String toProcess) throws Exception {
170                    if ((toProcess == null) || (toProcess.length() == 0)) {
171                            return new String[0];
172                    }
173    
174                    // parse with a simple finite state machine
175    
176                    final int normal = 0;
177                    final int inQuote = 1;
178                    final int inDoubleQuote = 2;
179                    int state = normal;
180                    StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
181                    Vector<String> v = new Vector<String>();
182                    StringBuilder current = new StringBuilder();
183    
184                    while (tok.hasMoreTokens()) {
185                            String nextTok = tok.nextToken();
186                            switch (state) {
187                            case inQuote:
188                                    if ("\'".equals(nextTok)) {
189                                            state = normal;
190                                    } else {
191                                            current.append(nextTok);
192                                    }
193                                    break;
194                            case inDoubleQuote:
195                                    if ("\"".equals(nextTok)) {
196                                            state = normal;
197                                    } else {
198                                            current.append(nextTok);
199                                    }
200                                    break;
201                            default:
202                                    if ("\'".equals(nextTok)) {
203                                            state = inQuote;
204                                    } else if ("\"".equals(nextTok)) {
205                                            state = inDoubleQuote;
206                                    } else if (" ".equals(nextTok)) {
207                                            if (current.length() != 0) {
208                                                    v.addElement(current.toString());
209                                                    current.setLength(0);
210                                            }
211                                    } else {
212                                            current.append(nextTok);
213                                    }
214                                    break;
215                            }
216                    }
217    
218                    if (current.length() != 0) {
219                            v.addElement(current.toString());
220                    }
221    
222                    if ((state == inQuote) || (state == inDoubleQuote)) {
223                            throw new CommandLineException("unbalanced quotes in " + toProcess);
224                    }
225    
226                    String[] args = new String[v.size()];
227                    v.copyInto(args);
228                    return args;
229            }
230    
231            /**
232             * <p>
233             * Put quotes around the given String if necessary.
234             * </p>
235             * <p>
236             * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single quotes - else surround the argument by double quotes.
237             * </p>
238             * 
239             * @throws CommandLineException
240             *             if the argument contains both, single and double quotes.
241             * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)}, {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
242             *             {@link StringUtils#quoteAndEscape(String, char)} instead.
243             */
244            @Deprecated
245            public static String quote(String argument) throws CommandLineException {
246                    return quote(argument, false, false, true);
247            }
248    
249            /**
250             * <p>
251             * Put quotes around the given String if necessary.
252             * </p>
253             * <p>
254             * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single quotes - else surround the argument by double quotes.
255             * </p>
256             * 
257             * @throws CommandLineException
258             *             if the argument contains both, single and double quotes.
259             * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)}, {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
260             *             {@link StringUtils#quoteAndEscape(String, char)} instead.
261             */
262            @Deprecated
263            public static String quote(String argument, boolean wrapExistingQuotes) throws CommandLineException {
264                    return quote(argument, false, false, wrapExistingQuotes);
265            }
266    
267            /**
268             * @deprecated Use {@link StringUtils#quoteAndEscape(String, char, char[], char[], char, boolean)}, {@link StringUtils#quoteAndEscape(String, char, char[], char, boolean)}, or
269             *             {@link StringUtils#quoteAndEscape(String, char)} instead.
270             */
271            @Deprecated
272            public static String quote(String argument, boolean escapeSingleQuotes, boolean escapeDoubleQuotes, boolean wrapExistingQuotes) throws CommandLineException {
273                    if (argument.contains("\"")) {
274                            if (argument.contains("\'")) {
275                                    throw new CommandLineException("Can't handle single and double quotes in same argument");
276                            } else {
277                                    if (escapeSingleQuotes) {
278                                            return "\\\'" + argument + "\\\'";
279                                    } else if (wrapExistingQuotes) {
280                                            return '\'' + argument + '\'';
281                                    }
282                            }
283                    } else if (argument.contains("\'")) {
284                            if (escapeDoubleQuotes) {
285                                    return "\\\"" + argument + "\\\"";
286                            } else if (wrapExistingQuotes) {
287                                    return '\"' + argument + '\"';
288                            }
289                    } else if (argument.contains(" ")) {
290                            if (escapeDoubleQuotes) {
291                                    return "\\\"" + argument + "\\\"";
292                            } else {
293                                    return '\"' + argument + '\"';
294                            }
295                    }
296    
297                    return argument;
298            }
299    
300            public static String toString(String[] line) {
301                    // empty path return empty string
302                    if ((line == null) || (line.length == 0)) {
303                            return "";
304                    }
305    
306                    // path containing one or more elements
307                    final StringBuilder result = new StringBuilder();
308                    for (int i = 0; i < line.length; i++) {
309                            if (i > 0) {
310                                    result.append(' ');
311                            }
312                            try {
313                                    result.append(StringUtils.quoteAndEscape(line[i], '\"'));
314                            } catch (Exception e) {
315                                    System.err.println("Error quoting argument: " + e.getMessage());
316                            }
317                    }
318                    return result.toString();
319            }
320    
321    }