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 }