001package 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
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Properties;
024import java.util.StringTokenizer;
025import java.util.Vector;
026
027import org.codehaus.plexus.util.Os;
028import org.codehaus.plexus.util.StringUtils;
029import org.kuali.common.util.Assert;
030
031public 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}