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