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