ProcessBuilders.java
// Generated by delombok at Mon Nov 18 07:27:48 UTC 2024
package de.larssh.utils.io;
import static java.util.stream.Collectors.joining;
import java.io.File;
import java.lang.ProcessBuilder.Redirect;
import java.lang.ProcessBuilder.Redirect.Type;
import java.nio.file.Paths;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.larssh.utils.Nullables;
import de.larssh.utils.SystemUtils;
import de.larssh.utils.annotations.PackagePrivate;
import de.larssh.utils.text.Strings;
/**
* This class contains helper methods for {@link ProcessBuilder}.
*/
public final class ProcessBuilders {
/**
* Pattern describing all single characters retaining the special meaning of a
* backslash when the backslash is followed by this characters inside a Unix
* command line arguments.
*/
private static final Pattern UNIX_BACKSLASH_SPECIAL_MEANING_CHARACTER_PATTERN = Pattern.compile("[\r\n!\"$\\\\`]");
/**
* Pattern describing Unix command line arguments, which can be safely used
* without quoting and escaping.
*/
private static final Pattern UNIX_SAFE_ARGUMENT_PATTERN = Pattern.compile("[-.0-9A-Z_a-z]+");
/**
* Pattern describing a single double quote
*/
private static final Pattern WINDOWS_DOUBLE_QUOTE_PATTERN = Pattern.compile("\"");
/**
* Pattern describing all single space characters inside Windows command line
* arguments, requiring an argument to be escaped.
*/
private static final Pattern WINDOWS_SPACE_CHARACTER_PATTERN = Pattern.compile("[ \t\n\013]");
/**
* Appends the redirect {@code operation} to {@code file} to {@code builder}.
* {@code file} is converted into an absolute path and normalized.
*
* @param builder the builder to append to
* @param operation the redirect operation
* @param file the path of the redirection
*/
private static void appendRedirect(final StringBuilder builder, final String operation, final File file) {
final String normalizedPath = file.toPath().toAbsolutePath().normalize().toString();
builder.append(' ').append(operation).append(' ').append(SystemUtils.isWindows() ? escapeArgumentOnWindows(normalizedPath) : escapeArgumentOnUnix(normalizedPath));
}
/**
* Appends redirect operations of {@code processBuilder} to {@code builder}.
*
* @param builder the builder to append to
* @param processBuilder the process builder, which information to handle
*/
@SuppressWarnings("PMD.CyclomaticComplexity")
private static void appendRedirects(final StringBuilder builder, final ProcessBuilder processBuilder) {
// Standard Input
final Redirect input = processBuilder.redirectInput();
if (input.type() == Type.READ && input.file() != null) {
appendRedirect(builder, "<", input.file());
}
// Standard Output
final Redirect output = processBuilder.redirectOutput();
if (output.type() == Type.APPEND && output.file() != null) {
appendRedirect(builder, ">>", output.file());
} else if (output.type() == Type.WRITE && output.file() != null) {
appendRedirect(builder, ">", output.file());
}
// Standard Error
if (processBuilder.redirectErrorStream()) {
builder.append(" 2>&1");
} else {
final Redirect error = processBuilder.redirectError();
if (error.type() == Type.APPEND && error.file() != null) {
appendRedirect(builder, "2>>", error.file());
} else if (error.type() == Type.WRITE && error.file() != null) {
appendRedirect(builder, "2>", error.file());
}
}
}
/**
* Escapes {@code argument} as command line argument for Unix.
*
* <p>
* based on <a href=
* "https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes">"3.1.2.3
* Double Quotes" of the Bash Reference Manual</a>
*
* @param argument the argument to escape
* @return the escaped argument
*/
@PackagePrivate
static String escapeArgumentOnUnix(final String argument) {
// No need to quote and escape in case of clearly safe characters
if (Strings.matches(argument, UNIX_SAFE_ARGUMENT_PATTERN)) {
return argument;
}
// Wrap in double quotes and escape characters with special meanings
return '\"' + Strings.replaceAll(argument, UNIX_BACKSLASH_SPECIAL_MEANING_CHARACTER_PATTERN, "\\\\$0") + '\"';
}
/**
* Escapes {@code argument} as command line argument for Windows.
*
* @param argument the argument to escape
* @return the escaped argument
*/
@PackagePrivate
@SuppressWarnings({"checkstyle:IllegalInstantiation", "checkstyle:XIllegalTypeCustom"})
static String escapeArgumentOnWindows(final String argument) {
final StringBuffer buffer = new StringBuffer();
// The argument needs to be wrapped in double quotes in case of space characters
final boolean quote = argument.isEmpty() || Strings.find(argument, WINDOWS_SPACE_CHARACTER_PATTERN);
if (quote) {
buffer.append('\"');
}
final Matcher matcher = WINDOWS_DOUBLE_QUOTE_PATTERN.matcher(argument);
while (matcher.find()) {
matcher.appendReplacement(buffer, "");
// In case of a double quote trailing backslashes need to be duplicated and the
// found double quote needs to be escaped and appended.
for (int index = buffer.length() - 1; index > -1 && buffer.charAt(index) == '\\'; index -= 1) {
buffer.append('\\');
}
buffer.append("\\\"");
}
matcher.appendTail(buffer);
// In case of quoting trailing backslashes need to be duplicated and the
// trailing double quote needs to be appended.
if (quote) {
for (int index = buffer.length() - 1; index > -1 && buffer.charAt(index) == '\\'; index -= 1) {
buffer.append('\\');
}
buffer.append('\"');
}
return buffer.toString();
}
/**
* Returns a command to launch {@code processBuilder} via command line.
*
* <p>
* This method is most likely used for logging and user output or debugging.
*
* <p>
* The results of this method depend on the Operating System as command line
* arguments on Unix and Windows are quoted and escaped differently.
*
* @param processBuilder the process definition
* @param prependWorkingDirectory if {@code true} the working directory of
* {@code processBuilder} is prepended, else the
* returned value consists of the command line
* only.
* @return the escaped command
*/
public static String toCommandLine(final ProcessBuilder processBuilder, final boolean prependWorkingDirectory) {
final StringBuilder builder = new StringBuilder();
// Working Directory
if (prependWorkingDirectory) {
builder.append(Nullables.orElseGet(processBuilder.directory(), () -> Paths.get(".").toAbsolutePath().normalize().toString())).append("> ");
}
// Command and Arguments
final UnaryOperator<String> escapeArgument = SystemUtils.isWindows() ? ProcessBuilders::escapeArgumentOnWindows : ProcessBuilders::escapeArgumentOnUnix;
builder.append(processBuilder.command().stream().map(escapeArgument).collect(joining(" ")));
// Redirects (Standard Input, Output, Error)
appendRedirects(builder, processBuilder);
return builder.toString();
}
@java.lang.SuppressWarnings("all")
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
@lombok.Generated
private ProcessBuilders() {
throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}