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