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 }