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