001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 */ 018 019 package org.apache.commons.exec; 020 021 import org.apache.commons.exec.util.StringUtils; 022 023 import java.io.File; 024 import java.util.HashMap; 025 import java.util.Iterator; 026 import java.util.StringTokenizer; 027 import java.util.Vector; 028 import java.util.Map; 029 030 /** 031 * CommandLine objects help handling command lines specifying processes to 032 * execute. The class can be used to a command line by an application. 033 */ 034 public class CommandLine { 035 036 /** 037 * The arguments of the command. 038 */ 039 private final Vector arguments = new Vector(); 040 041 /** 042 * The program to execute. 043 */ 044 private final String executable; 045 046 /** 047 * A map of name value pairs used to expand command line arguments 048 */ 049 private Map substitutionMap; 050 051 /** 052 * Was a file being used to set the executable? 053 */ 054 private final boolean isFile; 055 056 /** 057 * Create a command line from a string. 058 * 059 * @param line the first element becomes the executable, the rest the arguments 060 * @return the parsed command line 061 * @throws IllegalArgumentException If line is null or all whitespace 062 */ 063 public static CommandLine parse(final String line) { 064 return parse(line, null); 065 } 066 067 /** 068 * Create a command line from a string. 069 * 070 * @param line the first element becomes the executable, the rest the arguments 071 * @param substitutionMap the name/value pairs used for substitution 072 * @return the parsed command line 073 * @throws IllegalArgumentException If line is null or all whitespace 074 */ 075 public static CommandLine parse(final String line, Map substitutionMap) { 076 077 if (line == null) { 078 throw new IllegalArgumentException("Command line can not be null"); 079 } else if (line.trim().length() == 0) { 080 throw new IllegalArgumentException("Command line can not be empty"); 081 } else { 082 String[] tmp = translateCommandline(line); 083 084 CommandLine cl = new CommandLine(tmp[0]); 085 cl.setSubstitutionMap(substitutionMap); 086 for (int i = 1; i < tmp.length; i++) { 087 cl.addArgument(tmp[i]); 088 } 089 090 return cl; 091 } 092 } 093 094 /** 095 * Create a command line without any arguments. 096 * 097 * @param executable the executable 098 */ 099 public CommandLine(String executable) { 100 this.isFile=false; 101 this.executable=getExecutable(executable); 102 } 103 104 /** 105 * Create a command line without any arguments. 106 * 107 * @param executable the executable file 108 */ 109 public CommandLine(File executable) { 110 this.isFile=true; 111 this.executable=getExecutable(executable.getAbsolutePath()); 112 } 113 114 /** 115 * Copy constructor. 116 * 117 * @param other the instance to copy 118 */ 119 public CommandLine(CommandLine other) 120 { 121 this.executable = other.getExecutable(); 122 this.isFile = other.isFile(); 123 this.arguments.addAll(other.arguments); 124 125 if(other.getSubstitutionMap() != null) 126 { 127 this.substitutionMap = new HashMap(); 128 Iterator iterator = other.substitutionMap.keySet().iterator(); 129 while(iterator.hasNext()) 130 { 131 Object key = iterator.next(); 132 this.substitutionMap.put(key, other.getSubstitutionMap().get(key)); 133 } 134 } 135 } 136 137 /** 138 * Returns the executable. 139 * 140 * @return The executable 141 */ 142 public String getExecutable() { 143 // Expand the executable and replace '/' and '\\' with the platform 144 // specific file separator char. This is safe here since we know 145 // that this is a platform specific command. 146 return StringUtils.fixFileSeparatorChar(expandArgument(executable)); 147 } 148 149 /** 150 * Was a file being used to set the executable? 151 * 152 * @return true if a file was used for setting the executable 153 */ 154 public boolean isFile(){ 155 return isFile; 156 } 157 158 /** 159 * Add multiple arguments. Handles parsing of quotes and whitespace. 160 * 161 * @param arguments An array of arguments 162 * @return The command line itself 163 */ 164 public CommandLine addArguments(final String[] arguments) { 165 return this.addArguments(arguments, true); 166 } 167 168 /** 169 * Add multiple arguments. 170 * 171 * @param arguments An array of arguments 172 * @param handleQuoting Add the argument with/without handling quoting 173 * @return The command line itself 174 */ 175 public CommandLine addArguments(final String[] arguments, boolean handleQuoting) { 176 if (arguments != null) { 177 for (int i = 0; i < arguments.length; i++) { 178 addArgument(arguments[i], handleQuoting); 179 } 180 } 181 182 return this; 183 } 184 185 /** 186 * Add multiple arguments. Handles parsing of quotes and whitespace. 187 * Please note that the parsing can have undesired side-effects therefore 188 * it is recommended to build the command line incrementally. 189 * 190 * @param arguments An string containing multiple arguments. 191 * @return The command line itself 192 */ 193 public CommandLine addArguments(final String arguments) { 194 return this.addArguments(arguments, true); 195 } 196 197 /** 198 * Add multiple arguments. Handles parsing of quotes and whitespace. 199 * Please note that the parsing can have undesired side-effects therefore 200 * it is recommended to build the command line incrementally. 201 * 202 * @param arguments An string containing multiple arguments. 203 * @param handleQuoting Add the argument with/without handling quoting 204 * @return The command line itself 205 */ 206 public CommandLine addArguments(final String arguments, boolean handleQuoting) { 207 if (arguments != null) { 208 String[] argumentsArray = translateCommandline(arguments); 209 addArguments(argumentsArray, handleQuoting); 210 } 211 212 return this; 213 } 214 215 /** 216 * Add a single argument. Handles quoting. 217 * 218 * @param argument The argument to add 219 * @return The command line itself 220 * @throws IllegalArgumentException If argument contains both single and double quotes 221 */ 222 public CommandLine addArgument(final String argument) { 223 return this.addArgument(argument, true); 224 } 225 226 /** 227 * Add a single argument. 228 * 229 * @param argument The argument to add 230 * @param handleQuoting Add the argument with/without handling quoting 231 * @return The command line itself 232 */ 233 public CommandLine addArgument(final String argument, boolean handleQuoting) { 234 235 if (argument == null) 236 { 237 return this; 238 } 239 240 // check if we can really quote the argument - if not throw an 241 // IllegalArgumentException 242 if (handleQuoting) 243 { 244 StringUtils.quoteArgument(argument); 245 } 246 247 arguments.add(new Argument(argument, handleQuoting)); 248 return this; 249 } 250 251 /** 252 * Returns the expanded and quoted command line arguments. 253 * 254 * @return The quoted arguments 255 */ 256 public String[] getArguments() { 257 258 Argument currArgument; 259 String expandedArgument; 260 String[] result = new String[arguments.size()]; 261 262 for(int i=0; i<result.length; i++) { 263 currArgument = (Argument) arguments.get(i); 264 expandedArgument = expandArgument(currArgument.getValue()); 265 result[i] = (currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument); 266 } 267 268 return result; 269 } 270 271 /** 272 * @return the substitution map 273 */ 274 public Map getSubstitutionMap() { 275 return substitutionMap; 276 } 277 278 /** 279 * Set the substitutionMap to expand variables in the 280 * command line. 281 * 282 * @param substitutionMap the map 283 */ 284 public void setSubstitutionMap(Map substitutionMap) { 285 this.substitutionMap = substitutionMap; 286 } 287 288 /** 289 * Returns the command line as an array of strings. 290 * 291 * @return The command line as an string array 292 */ 293 public String[] toStrings() { 294 final String[] result = new String[arguments.size() + 1]; 295 result[0] = this.getExecutable(); 296 System.arraycopy(getArguments(), 0, result, 1, result.length-1); 297 return result; 298 } 299 300 /** 301 * Stringify operator returns the command line as a string. 302 * Parameters are correctly quoted when containing a space or 303 * left untouched if the are already quoted. 304 * 305 * @return the command line as single string 306 */ 307 public String toString() { 308 return StringUtils.toString(toStrings(), " "); 309 } 310 311 // --- Implementation --------------------------------------------------- 312 313 /** 314 * Expand variables in a command line argument. 315 * 316 * @param argument the argument 317 * @return the expanded string 318 */ 319 private String expandArgument(final String argument) { 320 StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, this.getSubstitutionMap(), true); 321 return stringBuffer.toString(); 322 } 323 324 /** 325 * Crack a command line. 326 * 327 * @param toProcess 328 * the command line to process 329 * @return the command line broken into strings. An empty or null toProcess 330 * parameter results in a zero sized array 331 */ 332 private static String[] translateCommandline(final String toProcess) { 333 if (toProcess == null || toProcess.length() == 0) { 334 // no command? no string 335 return new String[0]; 336 } 337 338 // parse with a simple finite state machine 339 340 final int normal = 0; 341 final int inQuote = 1; 342 final int inDoubleQuote = 2; 343 int state = normal; 344 StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); 345 Vector v = new Vector(); 346 StringBuffer current = new StringBuffer(); 347 boolean lastTokenHasBeenQuoted = false; 348 349 while (tok.hasMoreTokens()) { 350 String nextTok = tok.nextToken(); 351 switch (state) { 352 case inQuote: 353 if ("\'".equals(nextTok)) { 354 lastTokenHasBeenQuoted = true; 355 state = normal; 356 } else { 357 current.append(nextTok); 358 } 359 break; 360 case inDoubleQuote: 361 if ("\"".equals(nextTok)) { 362 lastTokenHasBeenQuoted = true; 363 state = normal; 364 } else { 365 current.append(nextTok); 366 } 367 break; 368 default: 369 if ("\'".equals(nextTok)) { 370 state = inQuote; 371 } else if ("\"".equals(nextTok)) { 372 state = inDoubleQuote; 373 } else if (" ".equals(nextTok)) { 374 if (lastTokenHasBeenQuoted || current.length() != 0) { 375 v.addElement(current.toString()); 376 current = new StringBuffer(); 377 } 378 } else { 379 current.append(nextTok); 380 } 381 lastTokenHasBeenQuoted = false; 382 break; 383 } 384 } 385 386 if (lastTokenHasBeenQuoted || current.length() != 0) { 387 v.addElement(current.toString()); 388 } 389 390 if (state == inQuote || state == inDoubleQuote) { 391 throw new IllegalArgumentException("Unbalanced quotes in " 392 + toProcess); 393 } 394 395 String[] args = new String[v.size()]; 396 v.copyInto(args); 397 return args; 398 } 399 400 /** 401 * Get the executable - the argument is trimmed and '/' and '\\' are 402 * replaced with the platform specific file separator char 403 * 404 * @param executable the executable 405 * @return the platform-specific executable string 406 */ 407 private String getExecutable(final String executable) { 408 if (executable == null) { 409 throw new IllegalArgumentException("Executable can not be null"); 410 } else if(executable.trim().length() == 0) { 411 throw new IllegalArgumentException("Executable can not be empty"); 412 } else { 413 return StringUtils.fixFileSeparatorChar(executable); 414 } 415 } 416 417 /** 418 * Encapsulates a command line argument. 419 */ 420 class Argument { 421 422 private final String value; 423 private final boolean handleQuoting; 424 425 private Argument(String value, boolean handleQuoting) 426 { 427 this.value = value.trim(); 428 this.handleQuoting = handleQuoting; 429 } 430 431 private String getValue() 432 { 433 return value; 434 } 435 436 private boolean isHandleQuoting() 437 { 438 return handleQuoting; 439 } 440 } 441 }