com.urbancode.air.CommandHelper.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of groovy-plugin-utils Show documentation
Show all versions of groovy-plugin-utils Show documentation
A set of utility scripts than can be used in community plugins on JazzHub
The newest version!
package com.urbancode.air
import java.util.regex.Matcher
public class CommandHelper {
//**************************************************************************
// CLASS
//**************************************************************************
/**
* This is the set of characters which would represent special processing to a shell interpreter either breaking
* a word or being evaluated as non-literal text within a word.
*
*
* metacharacter
* A character that, when unquoted, separates words. One of the following:
* | & ; ( ) < > space tab
*
* QUOTING
*
* Quoting is used to remove the special meaning of certain characters or words to the shell. Quoting
* can be used to disable special treatment for special characters, to prevent reserved words from being
* recognized as such, and to prevent parameter expansion.
*
* Each of the metacharacters listed above has special meaning to the shell and must
* be quoted if it is to represent itself.
*
* When the command history expansion facilities are being used (see HISTORY EXPANSION below), the his-
* tory expansion character, usually !, must be quoted to prevent history expansion.
*
* There are three quoting mechanisms: the escape character, single quotes, and double quotes.
*
* A non-quoted backslash (\) is the escape character. It preserves the literal value of the next char-
* acter that follows, with the exception of <newline>. If a \<newline> pair appears, and the backslash
* is not itself quoted, the \<newline> is treated as a line continuation (that is, it is removed from
* the input stream and effectively ignored).
*
* Enclosing characters in single quotes preserves the literal value of each character within the
* quotes. A single quote may not occur between single quotes, even when preceded by a backslash.
*
* Enclosing characters in double quotes preserves the literal value of all characters within the
* quotes, with the exception of $, `, \, and, when history expansion is enabled, !. The characters $
* and ` retain their special meaning within double quotes. The backslash retains its special meaning
* only when followed by one of the following characters: $, `, ", \, or <newline>. A double quote may
* be quoted within double quotes by preceding it with a backslash. If enabled, history expansion will
* be performed unless an ! appearing in double quotes is escaped using a backslash. The backslash
* preceding the ! is not removed.
*
* The special parameters * and @ have special meaning when in double quotes (see PARAMETERS below).
*
* Words of the form $'string' are treated specially. The word expands to string, with backslash-
* escaped characters replaced as specified by the ANSI C standard. Backslash escape sequences, if
* present, are decoded as follows:
* \a alert (bell)
* \b backspace
* \e an escape character
* \f form feed
* \n new line
* \r carriage return
* \t horizontal tab
* \v vertical tab
* \\ backslash
* \' single quote
* \nnn the eight-bit character whose value is the octal value nnn (one to three digits)
* \xHH the eight-bit character whose value is the hexadecimal value HH (one or two hex digits)
* \cx a control-x character
*
* The expanded result is single-quoted, as if the dollar sign had not been present.
*
* A double-quoted string preceded by a dollar sign ($) will cause the string to be translated according
* to the current locale. If the current locale is C or POSIX, the dollar sign is ignored. If the
* string is translated and replaced, the replacement is double-quoted.
*
*/
static private final Collection specialChars;
static {
Set chars = new LinkedHashSet();
Collections.addAll(chars, "|&;()<> \t\n".split("")); // word breaking chars, see meta-character section
Collections.addAll(chars, "{}" .split("")); // compound command chars
Collections.addAll(chars, "'\"" .split("")); // quoting chars
Collections.addAll(chars, "\$[]*!" .split("")); // expanding chars
Collections.addAll(chars, "`" .split("")); // sub-command chars
chars.remove(""); // ensure empty-string is not present
specialChars = Collections.unmodifiableCollection(chars);
}
//**************************************************************************
// INSTANCE
//**************************************************************************
private final def pb
private def out = System.out
boolean ignoreExitValue = false
public CommandHelper(dir) {
pb = new ProcessBuilder(['echo'] as String[]).directory(dir)
}
/**
* A convenience method for running commands and optionally parsing the stdout of that command.
* The process' stdOut and stdErr are forwarded to this scripts 'stdOut' and stdIn is untouched.
*
* @param message an optional message to print prior to the commandline
* @param command the command to be used as a String[]
* @see #runCommand(def,def,Closure)
*/
public int runCommand(def message, def command) {
return runCommand(message, command, null, null)
}
/**
* A convenience method for running commands and optionally parsing the stdout of that command.
* An input String can be provided and will be written to the process OutputStream
* The process' stdOut and stdErr are forwarded to this scripts 'stdOut' and stdIn is untouched.
*
* @param message an optional message to print prior to the commandline
* @param command the command to be used as a String[]
* @param input String that will be written to the process OutputStream
* @see #runCommand(def,def,Closure)
*/
public int runCommand(def message, def command, String input) {
return runCommand(message, command, null, input)
}
/**
* A convenience method for running commands and optionally parsing the stdout of that command.
* If closure is non-null, the closure will be passed the resultant {@link Process} and is expected to deal with all IO.
* The process' stdOut and stdErr are forwarded to this scripts 'stdOut' and stdIn is untouched.
*
* @param message an optional message to print prior to the commandline
* @param command the command to be used as a String[]
* @param closure an optional closure to deal with Process IO
* @see #runCommand(def,def,Closure)
*/
public int runCommand(def message, def command, Closure closure) {
return runCommand(message, command, closure, null)
}
/**
* A convenience method for running commands and optionally parsing the stdout of that command.
* If closure is non-null, the closure will be passed the resultant {@link Process} and is expected to deal with all IO.
* An input String can be provided and will be written to the process OutputStream
* Otherwise, the process' stdOut and stdErr are forwarded to this scripts 'stdOut' and stdIn is untouched.
*
* @param message an optional message to print prior to the commandline
* @param command the command to be used as a String[]
* @param closure an optional closure to deal with Process IO
* @param input String that will be written to the process OutputStream
*/
public int runCommand(def message, def command, Closure closure, String input) {
command[0] = sanitizeExecutable(command[0])
pb.command(command as String[])
println()
if (message) {
println(message)
}
println("command: ${pb.command().collect{addDisplayQuotes(it)}.join(' ')}")
def proc = pb.start()
if (input != null && input.length() > 0) {
proc.getOutputStream().write(input.getBytes());
}
def hook = {
proc.destroy();
}
addShutdownHook(hook);
if (closure) {
closure(proc)
}
else {
proc.out.close() // close stdin
def out = new PrintStream(this.out, true)
try {
proc.waitForProcessOutput(out, out) // forward stdout and stderr to autoflushing output stream
}
finally {
out.flush();
}
}
proc.waitFor()
removeShutdownHook(hook);
if (!ignoreExitValue && proc.exitValue()) {
throw new ExitCodeException("Command failed with exit code: " + proc.exitValue())
}
return proc.exitValue();
}
public def getProcessBuilder() {
return pb;
}
private void addShutdownHook(def hook) {
Runtime.getRuntime().addShutdownHook(hook as Thread);
}
private void removeShutdownHook(def hook) {
Runtime.getRuntime().removeShutdownHook(hook as Thread);
}
/**
* If the given value contains characters which would be interpreted specially by a shell,
* applies quoting to the value. Otherwise simply returns the value as is.
*
*/
private String addDisplayQuotes(String value) {
if (requiresDisplayQuotes(value)) {
// this generates an ugly result for leading/trailing ' characters in original value, could be improved
return "'"+(value.replaceAll("'", "'\\''"))+"'";
}
else {
return value;
}
}
/**
* Check if a given argument contains any special shell character which would
*
* - not render (empty argument)
* - contains any characters which would break word boundaries
* - contains any expanding/special characters
*
*/
private boolean requiresDisplayQuotes(String value) {
if (value == null) {
return false
}
// empty string requires quoting to be an explicit argument
if (value == "" || specialChars.find{value.contains(it)}) {
return true;
}
else {
return false;
}
}
public void ignoreExitValue(boolean ignore) {
this.ignoreExitValue = ignore;
}
public void addEnvironmentVariable(String key, String value) {
if (pb != null) {
Map environmentVariables = pb.environment();
environmentVariables.put(key, value);
}
}
public void removeEnvironmentVariable(String key) {
if (pb != null) {
Map environmentVariables = pb.environment();
environmentVariables.remove(key);
}
}
public void printEnvironmentVariables() {
if (pb != null) {
Map environmentVariables = pb.environment();
environmentVariables.each { key, value ->
println "$key=$value"
}
}
}
private String sanitizeExecutable(String path) {
String sanitizedPath = path
File exe = new File(path)
if (exe.isAbsolute()) {
sanitizedPath = path.replaceAll("[\\\\/]", Matcher.quoteReplacement(File.separator))
}
return sanitizedPath
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy