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