/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.exec;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringTokenizer;
import org.apache.commons.exec.util.StringUtils;
/**
* CommandLine objects help handling command lines specifying processes to execute. The class can be used to a command line by an application.
*/
public class CommandLine {
/**
* Encapsulates a command line argument.
*/
static final class Argument {
/**
* Argument value.
*/
private final String value;
/**
* Whether to quote arguments.
*/
private final boolean handleQuoting;
private Argument(final String value, final boolean handleQuoting) { this.value = value.trim(); this.handleQuoting = handleQuoting; }
private String getValue() {
return value;
}
private boolean isHandleQuoting() {
return handleQuoting;
}
}
/**
* Create a command line from a string.
*
* @param line the first element becomes the executable, the rest the arguments.
* @return the parsed command line.
* @throws IllegalArgumentException If line is null or all whitespace.
*/
public static CommandLine parse(final String line) {
return parse(line, null);
}
/**
* Create a command line from a string.
*
* @param line the first element becomes the executable, the rest the arguments.
* @param substitutionMap the name/value pairs used for substitution.
* @return the parsed command line.
* @throws IllegalArgumentException If line is null or all whitespace.
*/
public static CommandLine parse(final String line, final Map<String, ?> substitutionMap) {
if (line == null) { throw new IllegalArgumentException("Command line cannot be null");
}
if (line.trim().isEmpty()) { throw new IllegalArgumentException("Command line cannot be empty");
}
final String[] tmp = translateCommandline(line); final CommandLine cl = new CommandLine(tmp[0]); cl.setSubstitutionMap(substitutionMap); for (int i = 1; i < tmp.length; i++) { cl.addArgument(tmp[i]);
}
return cl;
}
/**
* Crack a command line.
*
* @param toProcess the command line to process.
* @return the command line broken into strings. An empty or null toProcess parameter results in a zero sized array.
*/
private static String[] translateCommandline(final String toProcess) {
if (toProcess == null || toProcess.trim().isEmpty()) {
// no command? no string
return new String[0];
}
// parse with a simple finite state machine.
final int normal = 0; final int inQuote = 1; final int inDoubleQuote = 2; int state = normal; final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); final ArrayList<String> list = new ArrayList<>(); StringBuilder current = new StringBuilder(); boolean lastTokenHasBeenQuoted = false; while (tok.hasMoreTokens()) { final String nextTok = tok.nextToken(); switch (state) {
case inQuote:
if ("\'".equals(nextTok)) { lastTokenHasBeenQuoted = true; state = normal;
} else {
current.append(nextTok);
}
break;
case inDoubleQuote:
if ("\"".equals(nextTok)) { lastTokenHasBeenQuoted = true; state = normal;
} else {
current.append(nextTok);
}
break;
default:
switch (nextTok) {
case "\'":
state = inQuote; break;
case "\"":
state = inDoubleQuote; break;
case " ":
if (lastTokenHasBeenQuoted || current.length() != 0) { list.add(current.toString()); current = new StringBuilder();
}
break;
default:
current.append(nextTok);
break;
}
lastTokenHasBeenQuoted = false;
break;
}
} if (lastTokenHasBeenQuoted || current.length() != 0) { list.add(current.toString());
}
if (state == inQuote || state == inDoubleQuote) { throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
}
final String[] args = new String[list.size()]; return list.toArray(args);
}
/**
* The arguments of the command.
*/
private final List<Argument> arguments = new ArrayList<>();
/**
* The program to execute.
*/
private final String executable;
/**
* A map of name value pairs used to expand command line arguments.
*/
private Map<String, ?> substitutionMap; // This can contain values other than Strings.
/**
* Tests whether a file was used to set the executable.
*/
private final boolean isFile;
/**
* Copy constructor.
*
* @param other the instance to copy.
*/
public CommandLine(final CommandLine other) { this.executable = other.getExecutable(); this.isFile = other.isFile(); this.arguments.addAll(other.arguments); if (other.getSubstitutionMap() != null) { this.substitutionMap = new HashMap<>(other.getSubstitutionMap());
}
}
/**
* Constructs a command line without any arguments.
*
* @param executable the executable file.
*/
public CommandLine(final File executable) { this.isFile = true; this.executable = toCleanExecutable(executable.getAbsolutePath()); }
/**
* Constructs a command line without any arguments.
*
* @param executable the executable file.
* @since 1.5.0
*/
public CommandLine(final Path executable) { this.isFile = true; this.executable = toCleanExecutable(executable.toAbsolutePath().toString()); }
/**
* Constructs a command line without any arguments.
*
* @param executable the executable.
* @throws NullPointerException on null input.
* @throws IllegalArgumentException on empty input.
*/
public CommandLine(final String executable) { this.isFile = false; this.executable = toCleanExecutable(executable); }
/**
* Add a single argument. Handles quoting.
*
* @param argument The argument to add.
* @return The command line itself.
* @throws IllegalArgumentException If argument contains both single and double quotes.
*/
public CommandLine addArgument(final String argument) {
return addArgument(argument, true);
}
/**
* Add a single argument.
*
* @param argument The argument to add.
* @param handleQuoting Add the argument with/without handling quoting.
* @return The command line itself.
*/
public CommandLine addArgument(final String argument, final boolean handleQuoting) {
if (argument == null) { return this;
}
// check if we can really quote the argument - if not throw an
// IllegalArgumentException
if (handleQuoting) { StringUtils.quoteArgument(argument);
}
arguments.add(new Argument(argument, handleQuoting)); return this;
}
/**
* Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
* recommended to build the command line incrementally.
*
* @param addArguments A string containing multiple arguments.
* @return The command line itself.
*/
public CommandLine addArguments(final String addArguments) {
return addArguments(addArguments, true);
}
/**
* Add multiple arguments. Handles parsing of quotes and whitespace. Please note that the parsing can have undesired side-effects therefore it is
* recommended to build the command line incrementally.
*
* @param addArguments A string containing multiple arguments.
* @param handleQuoting Add the argument with/without handling quoting.
* @return The command line itself.
*/
public CommandLine addArguments(final String addArguments, final boolean handleQuoting) {
if (addArguments != null) { final String[] argumentsArray = translateCommandline(addArguments); addArguments(argumentsArray, handleQuoting);
}
return this;
}
/**
* Add multiple arguments. Handles parsing of quotes and whitespace.
*
* @param addArguments An array of arguments.
* @return The command line itself.
*/
public CommandLine addArguments(final String[] addArguments) {
return addArguments(addArguments, true);
}
/**
* Add multiple arguments.
*
* @param addArguments An array of arguments.
* @param handleQuoting Add the argument with/without handling quoting.
* @return The command line itself.
*/
public CommandLine addArguments(final String[] addArguments, final boolean handleQuoting) {
if (addArguments != null) { for (final String addArgument : addArguments) { addArgument(addArgument, handleQuoting);
}
}
return this;
}
/**
* Expand variables in a command line argument.
*
* @param argument the argument.
* @return the expanded string.
*/
private String expandArgument(final String argument) {
final StringBuffer stringBuffer = StringUtils.stringSubstitution(argument, getSubstitutionMap(), true); return stringBuffer.toString();
}
/**
* Gets the expanded and quoted command line arguments.
*
* @return The quoted arguments.
*/
public String[] getArguments() {
Argument currArgument;
String expandedArgument;
final String[] result = new String[arguments.size()]; for (int i = 0; i < result.length; i++) { currArgument = arguments.get(i); expandedArgument = expandArgument(currArgument.getValue()); result[i] = currArgument.isHandleQuoting() ? StringUtils.quoteArgument(expandedArgument) : expandedArgument;
}
return result;
}
/**
* Gets the executable.
*
* @return The executable.
*/
public String getExecutable() {
// Expand the executable and replace '/' and '\\' with the platform
// specific file separator char. This is safe here since we know
// that this is a platform specific command.
return StringUtils.fixFileSeparatorChar(expandArgument(executable));
}
/**
* Gets the substitution map.
*
* @return the substitution map.
*/
public Map<String, ?> getSubstitutionMap() {
return substitutionMap;
}
/**
* Tests whether a file was used to set the executable.
*
* @return true whether a file was used for setting the executable.
*/
public boolean isFile() {
return isFile;
}
/**
* Sets the substitutionMap to expand variables in the command line.
*
* @param substitutionMap the map
*/
public void setSubstitutionMap(final Map<String, ?> substitutionMap) {
this.substitutionMap = substitutionMap; }
/**
* Cleans the executable string. The argument is trimmed and '/' and '\\' are replaced with the platform specific file separator char
*
* @param dirtyExecutable the executable.
* @return the platform-specific executable string.
* @throws NullPointerException on null input.
* @throws IllegalArgumentException on empty input.
*/
private String toCleanExecutable(final String dirtyExecutable) {
Objects.requireNonNull(dirtyExecutable, "dirtyExecutable"); if (dirtyExecutable.trim().isEmpty()) { throw new IllegalArgumentException("Executable cannot be empty");
}
return StringUtils.fixFileSeparatorChar(dirtyExecutable);
}
/**
* Stringify operator returns the command line as a string.
* Parameters are correctly quoted when containing a space or left untouched if they are already
* quoted.
*
* @return the command line as single string.
*/
@Override
public String toString() {
return "[" + String.join(", ", toStrings()) + "]";
}
/**
* Converts the command line as an array of strings.
*
* @return The command line as a string array.
*/
public String[] toStrings() {
final String[] result = new String[arguments.size() + 1]; result[0] = getExecutable(); System.arraycopy(getArguments(), 0, result, 1, result.length - 1); return result;
}
}