From e5f130c9977c4438df5997cc9575fc84a7da4103 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 24 Jun 2017 16:40:55 +0200 Subject: [PATCH 01/12] Fixed whitespace issue on argument escaping. --- classes/phing/tasks/system/ExecTask.php | 5 ++- classes/phing/types/Commandline.php | 37 +++++++++++++------ .../phing/tasks/system/ExecTaskTest.php | 7 ++++ test/etc/tasks/system/ExecTest.xml | 6 +++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/classes/phing/tasks/system/ExecTask.php b/classes/phing/tasks/system/ExecTask.php index 89815a0381..816766da91 100644 --- a/classes/phing/tasks/system/ExecTask.php +++ b/classes/phing/tasks/system/ExecTask.php @@ -137,6 +137,7 @@ class ExecTask extends Task public function __construct() { $this->commandline = new Commandline(); + parent::__construct(); } /** @@ -226,14 +227,14 @@ protected function buildCommand() ); } else { if ($this->command === null) { - $this->realCommand = Commandline::toString($this->commandline->getCommandline(), $this->escape); + $this->realCommand = (string) $this->commandline; } else { if ($this->commandline->getExecutable() === null) { $this->realCommand = $this->command; //we need to escape the command only if it's specified directly // commandline takes care of "executable" already - if ($this->escape == true) { + if ($this->escape === true) { $this->realCommand = escapeshellcmd($this->realCommand); } } else { diff --git a/classes/phing/types/Commandline.php b/classes/phing/types/Commandline.php index 8c16b1bc8a..613cfd3de4 100644 --- a/classes/phing/types/Commandline.php +++ b/classes/phing/types/Commandline.php @@ -167,7 +167,13 @@ public function getArguments() */ public function __toString() { - return self::toString($this->getCommandline()); + try { + $cmd = self::toString($this->getCommandline()); + } catch (BuildException $be) { + $cmd = ''; + } + + return $cmd; } /** @@ -188,18 +194,25 @@ public static function quoteArgument($argument, $escape = false) { if ($escape) { return escapeshellarg($argument); - } elseif (strpos($argument, "\"") !== false && $argument != '""') { + } + + if (strpos($argument, "\"") !== false && $argument !== '""') { if (strpos($argument, "'") !== false) { throw new BuildException("Can't handle single and double quotes in same argument"); - } else { + } + + if ($escape) { return escapeshellarg($argument); } - } elseif (strpos($argument, "'") !== false || strpos($argument, " ") !== false) { + return $argument; + } + + if (strpos($argument, "'") !== false || strpos($argument, " ") !== false) { return escapeshellarg($argument); //return '\"' . $argument . '\"'; - } else { - return $argument; } + + return $argument; } /** @@ -257,7 +270,7 @@ public static function translateCommandline($to_process) while (($nextTok = array_shift($tokens)) !== null) { switch ($state) { case $inQuote: - if ("'" == $nextTok) { + if ("'" === $nextTok) { $lastTokenHasBeenQuoted = true; $state = $normal; } else { @@ -265,7 +278,7 @@ public static function translateCommandline($to_process) } break; case $inDoubleQuote: - if ("\"" == $nextTok) { + if ("\"" === $nextTok) { $lastTokenHasBeenQuoted = true; $state = $normal; } else { @@ -273,12 +286,12 @@ public static function translateCommandline($to_process) } break; default: - if ("'" == $nextTok) { + if ("'" === $nextTok) { $state = $inQuote; - } elseif ("\"" == $nextTok) { + } elseif ("\"" === $nextTok) { $state = $inDoubleQuote; - } elseif (" " == $nextTok) { - if ($lastTokenHasBeenQuoted || strlen($current) != 0) { + } elseif (" " === $nextTok) { + if ($lastTokenHasBeenQuoted || $current !== '') { $args[] = $current; $current = ""; } diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index cea215e9f0..d595141e64 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -363,4 +363,11 @@ public function testEscapedArg() $this->executeTarget(__FUNCTION__); $this->assertPropertyEquals('outval', 'abc$b3!SB'); } + + public function testEscapedArgWithoutWhitespace() + { + $this->executeTarget(__FUNCTION__); + $this->assertInLogs('echo "foo|bar" 2>&1'); + $this->assertNotInLogs('echo " foo|bar " 2>&1'); + } } diff --git a/test/etc/tasks/system/ExecTest.xml b/test/etc/tasks/system/ExecTest.xml index c312b4881d..32fdbddb7d 100644 --- a/test/etc/tasks/system/ExecTest.xml +++ b/test/etc/tasks/system/ExecTest.xml @@ -157,4 +157,10 @@ + + + + + + \ No newline at end of file From 331567834f35620799b1ba6a66c567d1630f8679 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 24 Jun 2017 16:47:56 +0200 Subject: [PATCH 02/12] Fixed test execution under linux. --- test/classes/phing/tasks/system/ExecTaskTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index d595141e64..782ab6a8ea 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -367,7 +367,7 @@ public function testEscapedArg() public function testEscapedArgWithoutWhitespace() { $this->executeTarget(__FUNCTION__); - $this->assertInLogs('echo "foo|bar" 2>&1'); - $this->assertNotInLogs('echo " foo|bar " 2>&1'); + $this->assertInLogs($this->windows ? 'echo "foo|bar" 2>&1' : 'echo \'foo|bar\' 2>&1'); + $this->assertNotInLogs($this->windows ? 'echo " foo|bar " 2>&1' : 'echo \' foo|bar \' 2>&1'); } } From c06bcc85c2de41fa73c7fa85a2a42a761c11cab0 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 24 Jun 2017 17:04:40 +0200 Subject: [PATCH 03/12] Fixed test execution under linux 2. --- classes/phing/types/Commandline.php | 10 +--------- test/classes/phing/tasks/system/ExecTaskTest.php | 1 + 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/classes/phing/types/Commandline.php b/classes/phing/types/Commandline.php index 613cfd3de4..e021bf2a1b 100644 --- a/classes/phing/types/Commandline.php +++ b/classes/phing/types/Commandline.php @@ -192,24 +192,16 @@ public function __toString() */ public static function quoteArgument($argument, $escape = false) { - if ($escape) { - return escapeshellarg($argument); - } - if (strpos($argument, "\"") !== false && $argument !== '""') { if (strpos($argument, "'") !== false) { throw new BuildException("Can't handle single and double quotes in same argument"); } - if ($escape) { - return escapeshellarg($argument); - } return $argument; } - if (strpos($argument, "'") !== false || strpos($argument, " ") !== false) { + if (strpos($argument, "'") !== false || strpos($argument, " ") !== false || $escape) { return escapeshellarg($argument); - //return '\"' . $argument . '\"'; } return $argument; diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index 782ab6a8ea..ad7f62eed0 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -366,6 +366,7 @@ public function testEscapedArg() public function testEscapedArgWithoutWhitespace() { + $arg = 'foo|bar'; $this->executeTarget(__FUNCTION__); $this->assertInLogs($this->windows ? 'echo "foo|bar" 2>&1' : 'echo \'foo|bar\' 2>&1'); $this->assertNotInLogs($this->windows ? 'echo " foo|bar " 2>&1' : 'echo \' foo|bar \' 2>&1'); From d5242893768e0ec33d1e9ab61acf18fc76c15560 Mon Sep 17 00:00:00 2001 From: siad007 Date: Fri, 16 Feb 2018 19:17:32 +0100 Subject: [PATCH 04/12] ExecTaskTests working again --- classes/phing/tasks/system/ApplyTask.php | 2 +- classes/phing/tasks/system/ExecTask.php | 237 ++++++++++++------ .../tasks/system/condition/OsCondition.php | 14 +- classes/phing/types/Commandline.php | 128 +++++----- classes/phing/types/CommandlineArgument.php | 13 +- .../phing/tasks/system/ExecTaskTest.php | 34 +-- test/etc/tasks/system/ExecTest.xml | 22 +- 7 files changed, 274 insertions(+), 176 deletions(-) diff --git a/classes/phing/tasks/system/ApplyTask.php b/classes/phing/tasks/system/ApplyTask.php index cea9f518d3..be128833ba 100644 --- a/classes/phing/tasks/system/ApplyTask.php +++ b/classes/phing/tasks/system/ApplyTask.php @@ -586,7 +586,7 @@ private function buildCommand() $this->log('Command building started ', $this->loglevel); // Building the executable - $this->realCommand = Commandline::toString($this->commandline->getCommandline(), $this->escape); + $this->realCommand = (string) $this->commandline; // Adding the source filename at the end of command, validating the existing // sourcefile position explicit mentioning diff --git a/classes/phing/tasks/system/ExecTask.php b/classes/phing/tasks/system/ExecTask.php index 6ab853c6fc..c2d9e7257c 100644 --- a/classes/phing/tasks/system/ExecTask.php +++ b/classes/phing/tasks/system/ExecTask.php @@ -38,12 +38,6 @@ class ExecTask extends Task */ protected $realCommand; - /** - * Given command - * @var string - */ - protected $command; - /** * Commandline managing object * @@ -128,21 +122,24 @@ class ExecTask extends Task protected $checkreturn = false; private $osFamily; + private $executable; + private $resolveExecutable = false; + private $searchPath = false; /** - * + * @throws \BuildException */ public function __construct() { parent::__construct(); $this->commandline = new Commandline(); - parent::__construct(); } /** * Main method: wraps execute() command. * * @return void + * @throws \BuildException */ public function main() { @@ -150,9 +147,15 @@ public function main() return; } + try { + $this->commandline->setExecutable($this->resolveExecutable($this->executable, $this->searchPath)); + } catch (IOException | NullPointerException $e) { + throw new BuildException($e); + } + $this->prepare(); $this->buildCommand(); - list($return, $output) = $this->executeCommand(); + [$return, $output] = $this->executeCommand(); $this->cleanup($return, $output); } @@ -166,14 +169,25 @@ public function main() protected function prepare() { if ($this->dir === null) { - return; + $this->dir = $this->getProject()->getBasedir(); + } + + if ($this->commandline->getExecutable() === null) { + throw new BuildException( + 'ExecTask: Please provide "executable"' + ); } // expand any symbolic links first try { + if (!$this->dir->getCanonicalFile()->exists()) { + throw new BuildException( + "The directory '" . (string) $this->dir . "' does not exist" + ); + } if (!$this->dir->getCanonicalFile()->isDirectory()) { throw new BuildException( - "'" . (string) $this->dir . "' is not a valid directory" + "'" . (string) $this->dir . "' is not a directory" ); } } catch (IOException $e) { @@ -183,6 +197,8 @@ protected function prepare() } $this->currdir = getcwd(); @chdir($this->dir->getPath()); + + $this->commandline->setEscape($this->escape); } /** @@ -194,34 +210,10 @@ protected function prepare() */ protected function buildCommand() { - if ($this->command === null && $this->commandline->getExecutable() === null) { - throw new BuildException( - 'ExecTask: Please provide "command" OR "executable"' - ); - } else { - if ($this->command === null) { - $this->realCommand = (string) $this->commandline; - } else { - if ($this->commandline->getExecutable() === null) { - $this->realCommand = $this->command; - - //we need to escape the command only if it's specified directly - // commandline takes care of "executable" already - if ($this->escape === true) { - $this->realCommand = escapeshellcmd($this->realCommand); - } - } else { - throw new BuildException( - 'ExecTask: Either use "command" OR "executable"' - ); - } - } - } - if ($this->error !== null) { $this->realCommand .= ' 2> ' . escapeshellarg($this->error->getPath()); $this->log( - "Writing error output to: " . $this->error->getPath(), + 'Writing error output to: ' . $this->error->getPath(), $this->logLevel ); } @@ -229,12 +221,12 @@ protected function buildCommand() if ($this->output !== null) { $this->realCommand .= ' 1> ' . escapeshellarg($this->output->getPath()); $this->log( - "Writing standard output to: " . $this->output->getPath(), + 'Writing standard output to: ' . $this->output->getPath(), $this->logLevel ); } elseif ($this->spawn) { $this->realCommand .= ' 1>/dev/null'; - $this->log("Sending output to /dev/null", $this->logLevel); + $this->log('Sending output to /dev/null', $this->logLevel); } // If neither output nor error are being written to file @@ -255,18 +247,21 @@ protected function buildCommand() * Executes the command and returns return code and output. * * @return array array(return code, array with output) + * @throws \BuildException */ protected function executeCommand() { - $this->log("Executing command: " . $this->realCommand, $this->logLevel); + $cmdl = (string) $this->commandline . $this->realCommand; + + $this->log('Executing command: ' . $cmdl, $this->logLevel); $output = []; $return = null; if ($this->passthru) { - passthru($this->realCommand, $return); + passthru($cmdl, $return); } else { - exec($this->realCommand, $output, $return); + exec($cmdl, $output, $return); } return [$return, $output]; @@ -284,7 +279,7 @@ protected function executeCommand() * @throws BuildException * @return void */ - protected function cleanup($return, $output) + protected function cleanup($return, $output): void { if ($this->dir !== null) { @chdir($this->currdir); @@ -295,9 +290,7 @@ protected function cleanup($return, $output) $this->log($line, $outloglevel); } - if ($this->returnProperty) { - $this->project->setProperty($this->returnProperty, $return); - } + $this->maybeSetReturnPropertyValue($return); if ($this->outputProperty) { $this->project->setProperty( @@ -308,8 +301,11 @@ protected function cleanup($return, $output) $this->setExitValue($return); - if ($return != 0 && $this->checkreturn) { - throw new BuildException("Task exited with code $return"); + if ($return !== 0) { + if ($this->checkreturn) { + throw new BuildException($this->getTaskType() . ' returned: ' . $return, $this->getLocation()); + } + $this->log('Result: ' . $return, Project::MSG_ERR); } } @@ -318,7 +314,7 @@ protected function cleanup($return, $output) * * @param int $value exit value of the process. */ - protected function setExitValue($value) + protected function setExitValue($value): void { $this->exitValue = $value; } @@ -329,7 +325,7 @@ protected function setExitValue($value) * @return int the exit value or self::INVALID if no exit value has * been received. */ - public function getExitValue() + public function getExitValue(): int { return $this->exitValue; } @@ -337,25 +333,33 @@ public function getExitValue() /** * The command to use. * - * @param mixed $command String or string-compatible (e.g. w/ __toString()). + * @param string $command String or string-compatible (e.g. w/ __toString()). * * @return void + * @throws \BuildException */ - public function setCommand($command) + public function setCommand($command): void { - $this->command = "" . $command; + $this->log("The command attribute is deprecated.\nPlease use the executable attribute and nested arg elements.", + Project::MSG_WARN); + $this->commandline = new Commandline($command); + $this->executable = $this->commandline->getExecutable(); } /** * The executable to use. * - * @param mixed $executable String or string-compatible (e.g. w/ __toString()). + * @param string|bool $value String or string-compatible (e.g. w/ __toString()). * * @return void */ - public function setExecutable($executable) + public function setExecutable($value): void { - $this->commandline->setExecutable((string) $executable); + if (is_bool($value)) { + $value = $value === true ? 'true' : 'false'; + } + $this->executable = $value; + $this->commandline->setExecutable($value); } /** @@ -365,7 +369,7 @@ public function setExecutable($executable) * * @return void */ - public function setEscape($escape) + public function setEscape($escape): void { $this->escape = (bool) $escape; } @@ -377,7 +381,7 @@ public function setEscape($escape) * * @return void */ - public function setDir(PhingFile $dir) + public function setDir(PhingFile $dir): void { $this->dir = $dir; } @@ -389,7 +393,7 @@ public function setDir(PhingFile $dir) * * @return void */ - public function setOs($os) + public function setOs($os): void { $this->os = (string) $os; } @@ -397,7 +401,7 @@ public function setOs($os) /** * List of operating systems on which the command may be executed. */ - public function getOs() + public function getOs(): string { return $this->os; } @@ -406,7 +410,7 @@ public function getOs() * Restrict this execution to a single OS Family * @param string $osFamily the family to restrict to. */ - public function setOsFamily($osFamily) + public function setOsFamily($osFamily): void { $this->osFamily = strtolower($osFamily); } @@ -426,7 +430,7 @@ public function getOsFamily() * * @return void */ - public function setOutput(PhingFile $f) + public function setOutput(PhingFile $f): void { $this->output = $f; } @@ -438,7 +442,7 @@ public function setOutput(PhingFile $f) * * @return void */ - public function setError(PhingFile $f) + public function setError(PhingFile $f): void { $this->error = $f; } @@ -450,7 +454,7 @@ public function setError(PhingFile $f) * * @return void */ - public function setPassthru($passthru) + public function setPassthru($passthru): void { $this->passthru = $passthru; } @@ -462,7 +466,7 @@ public function setPassthru($passthru) * * @return void */ - public function setLogoutput($logOutput) + public function setLogoutput($logOutput): void { $this->logOutput = $logOutput; } @@ -474,7 +478,7 @@ public function setLogoutput($logOutput) * * @return void */ - public function setSpawn($spawn) + public function setSpawn($spawn): void { $this->spawn = $spawn; } @@ -486,7 +490,7 @@ public function setSpawn($spawn) * * @return void */ - public function setCheckreturn($checkreturn) + public function setCheckreturn($checkreturn): void { $this->checkreturn = $checkreturn; } @@ -498,11 +502,18 @@ public function setCheckreturn($checkreturn) * * @return void */ - public function setReturnProperty($prop) + public function setReturnProperty($prop): void { $this->returnProperty = $prop; } + protected function maybeSetReturnPropertyValue(int $return) + { + if ($this->returnProperty) { + $this->getProject()->setNewProperty($this->returnProperty, $return); + } + } + /** * The name of property to set to output value from exec() call. * @@ -510,7 +521,7 @@ public function setReturnProperty($prop) * * @return void */ - public function setOutputProperty($prop) + public function setOutputProperty($prop): void { $this->outputProperty = $prop; } @@ -523,7 +534,7 @@ public function setOutputProperty($prop) * @throws BuildException * @return void */ - public function setLevel($level) + public function setLevel($level): void { switch ($level) { case 'error': @@ -575,9 +586,9 @@ public function createArg() *
  • false otherwise.
  • * */ - protected function isValidOs() + protected function isValidOs(): bool { - //hand osfamily off to Os class, if set + //hand osfamily off to OsCondition class, if set if ($this->osFamily !== null && !OsCondition::isFamily($this->osFamily)) { return false; } @@ -595,4 +606,88 @@ protected function isValidOs() } return true; } + + /** + * Set whether to attempt to resolve the executable to a file. + * + * @param bool $resolveExecutable if true, attempt to resolve the + * path of the executable. + */ + public function setResolveExecutable(boolean $resolveExecutable): void + { + $this->resolveExecutable = $resolveExecutable; + } + + /** + * Set whether to search nested, then + * system PATH environment variables for the executable. + * + * @param bool $searchPath if true, search PATHs. + */ + public function setSearchPath(boolean $searchPath): void + { + $this->searchPath = $searchPath; + } + + /** + * Indicates whether to attempt to resolve the executable to a + * file. + * @return bool the resolveExecutable flag + * + */ + public function getResolveExecutable(): bool + { + return $this->resolveExecutable; + } + + /** + * The method attempts to figure out where the executable is so that we can feed + * the full path. We first try basedir, then the exec dir, and then + * fallback to the straight executable name (i.e. on the path). + * + * @param string $exec the name of the executable. + * @param bool $mustSearchPath if true, the executable will be looked up in + * the PATH environment and the absolute path is returned. + * + * @return string the executable as a full path if it can be determined. + * @throws \BuildException + * @throws IOException + * @throws NullPointerException + */ + protected function resolveExecutable($exec, $mustSearchPath): ?string + { + if (!$this->resolveExecutable) { + return $exec; + } + // try to find the executable + $executableFile = $this->getProject()->resolveFile($exec); + if ($executableFile->exists()) { + return $executableFile->getAbsolutePath(); + } + // now try to resolve against the dir if given + if ($this->dir !== null) { + $executableFile = (new FileUtils())->resolveFile($this->dir, $exec); + if ($executableFile->exists()) { + return $executableFile->getAbsolutePath(); + } + } + // couldn't find it - must be on path + if ($mustSearchPath) { + $p = null; + if (getenv('path')) { + $p = new Path($this->getProject(), getenv('path')); + } + if ($p !== null) { + $dirs = $p->listPaths(); + foreach ($dirs as $dir) { + $executableFile = (new FileUtils())->resolveFile(new PhingFile($dir), $exec); + if ($executableFile->exists()) { + return $executableFile->getAbsolutePath(); + } + } + } + } + + return $exec; + } } diff --git a/classes/phing/tasks/system/condition/OsCondition.php b/classes/phing/tasks/system/condition/OsCondition.php index 9ca469adcc..a25d7403ba 100644 --- a/classes/phing/tasks/system/condition/OsCondition.php +++ b/classes/phing/tasks/system/condition/OsCondition.php @@ -64,8 +64,18 @@ public static function isOS($family) $osName = strtolower(Phing::getProperty("os.name")); if ($family !== null) { - if ($family === "windows") { - return StringHelper::startsWith("win", $osName); + $isWindows = StringHelper::startsWith('win', $osName); + + if ($family === 'windows') { + return $isWindows; + } + + if ($family === 'win32') { + return $isWindows && $osName === 'win32'; + } + + if ($family === 'winnt') { + return $isWindows && $osName === 'winnt'; } if ($family === "mac") { diff --git a/classes/phing/types/Commandline.php b/classes/phing/types/Commandline.php index 041f993a75..21d8668558 100644 --- a/classes/phing/types/Commandline.php +++ b/classes/phing/types/Commandline.php @@ -41,9 +41,8 @@ * @author Stefan Bodewig * @package phing.types */ -class Commandline +class Commandline implements Countable { - /** * @var CommandlineArgument[] */ @@ -56,6 +55,7 @@ class Commandline public $executable; // public so "inner" class can access const DISCLAIMER = "The ' characters around the executable and arguments are not part of the command."; + private $escape = false; /** * @param null $to_process @@ -98,16 +98,17 @@ public function createArgument($insertAtStart = false) /** * Sets the executable to run. - * @param $executable + * @param string $executable + * @param bool $translateFileSeparator */ - public function setExecutable($executable) + public function setExecutable($executable, $translateFileSeparator = true): void { - if (!$executable) { + if ($executable === null || $executable === '') { return; } - $this->executable = $executable; - $this->executable = strtr($this->executable, '/', DIRECTORY_SEPARATOR); - $this->executable = strtr($this->executable, '\\', DIRECTORY_SEPARATOR); + $this->executable = $translateFileSeparator + ? str_replace(['/', '\\'], PhingFile::$separator, $executable) + : $executable; } /** @@ -132,28 +133,28 @@ public function addArguments(array $arguments) * Returns the executable and all defined arguments. * @return array */ - public function getCommandline() + public function getCommandline(): array { $args = $this->getArguments(); - if ($this->executable === null) { - return $args; + if ($this->executable !== null && $this->executable !== '') { + array_unshift($args, $this->executable); } - return array_merge([$this->executable], $args); + return $args; } /** * Returns all arguments defined by addLine, * addValue or the argument object. */ - public function getArguments() + public function getArguments(): array { $result = []; foreach ($this->arguments as $arg) { $parts = $arg->getParts(); if ($parts !== null) { foreach ($parts as $part) { - $result[] = $arg->escape ? self::quoteArgument($part, true) : $part; + $result[] = $part; } } } @@ -161,13 +162,18 @@ public function getArguments() return $result; } + public function setEscape($flag) + { + $this->escape = $flag; + } + /** * @return string */ public function __toString() { try { - $cmd = self::toString($this->getCommandline()); + $cmd = $this->toString($this->getCommandline()); } catch (BuildException $be) { $cmd = ''; } @@ -182,25 +188,32 @@ public function __toString() * as is. If it contains double quotes, use single quotes - else * surround the argument by double quotes.

    * - * @exception BuildException if the argument contains both, single - * and double quotes. * @param $argument - * @param bool $escape - * @throws BuildException + * * @return string + * + * @throws BuildException if the argument contains both, single + * and double quotes. */ public static function quoteArgument($argument, $escape = false) { - if (strpos($argument, "\"") !== false && $argument !== '""') { + if ($escape) { + return escapeshellarg($argument); + } + + if (strpos($argument, '"') !== false) { if (strpos($argument, "'") !== false) { throw new BuildException("Can't handle single and double quotes in same argument"); } - return $argument; + return '\'' . $argument . '\''; } - if (strpos($argument, "'") !== false || strpos($argument, " ") !== false || $escape) { - return escapeshellarg($argument); + if (strpos($argument, "'") !== false + || strpos($argument, ' ') !== false + // WIN9x uses a bat file for executing commands + || (OsCondition::isFamily('win32') && strpos($argument, ';') !== false)) { + return '"' . $argument . '"'; } return $argument; @@ -209,39 +222,38 @@ public static function quoteArgument($argument, $escape = false) /** * Quotes the parts of the given array in way that makes them * usable as command line arguments. + * * @param $lines - * @param bool $escape - * @throws BuildException + * * @return string + * + * @throws BuildException */ - public static function toString($lines, $escape = false) + private function toString($lines = null): string { // empty path return empty string - if (!$lines) { - return ""; + if ($lines === null || count($lines) === 0) { + return ''; } - // path containing one or more elements - $result = ""; - for ($i = 0, $len = count($lines); $i < $len; $i++) { - if ($i > 0) { - $result .= ' '; - } - $result .= self::quoteArgument($lines[$i], $escape); - } + $cmd = array_shift($lines); - return $result; + return $cmd . ' ' . implode(' ', array_map(function ($arg) {return self::quoteArgument($arg, $this->escape);}, $lines)); } /** + * Crack a command line. * - * @param string $to_process - * @throws BuildException - * @return array + * @param string $toProcess the command line to process. + * + * @return string[] the command line broken into strings. + * An empty or null toProcess parameter results in a zero sized array. + * + * @throws \BuildException */ - public static function translateCommandline($to_process) + public static function translateCommandline(string $toProcess = null): array { - if (!$to_process) { + if ($toProcess === null || $toProcess === '') { return []; } @@ -256,8 +268,7 @@ public static function translateCommandline($to_process) $current = ""; $lastTokenHasBeenQuoted = false; - $tok = strtok($to_process, ""); - $tokens = preg_split('/(["\' ])/', $to_process, -1, PREG_SPLIT_DELIM_CAPTURE); + $tokens = preg_split('/(["\' ])/', $toProcess, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); while (($nextTok = array_shift($tokens)) !== null) { switch ($state) { case $inQuote: @@ -294,12 +305,12 @@ public static function translateCommandline($to_process) } } - if ($lastTokenHasBeenQuoted || strlen($current) != 0) { + if ($lastTokenHasBeenQuoted || $current !== '') { $args[] = $current; } - if ($state == $inQuote || $state == $inDoubleQuote) { - throw new BuildException("unbalanced quotes in " . $to_process); + if ($state === $inQuote || $state === $inDoubleQuote) { + throw new BuildException('unbalanced quotes in ' . $toProcess); } return $args; @@ -308,21 +319,18 @@ public static function translateCommandline($to_process) /** * @return int Number of components in current commandline. */ - public function size() + public function count(): int { return count($this->getCommandline()); } /** - * @return Commandline + * @throws \BuildException */ - public function __copy() + public function __clone() { - $c = new Commandline(); - $c->setExecutable($this->executable); + $c = new self(); $c->addArguments($this->getArguments()); - - return $c; } /** @@ -345,24 +353,26 @@ public function createMarker() * *

    This method assumes that the first entry in the array is the * executable to run.

    - * @param array $args CommandlineArgument[] to use + * @param array|Commandline $args CommandlineArgument[] to use * @return string */ public function describeCommand($args = null) { if ($args === null) { $args = $this->getCommandline(); + } elseif ($args instanceof self) { + $args = $args->getCommandline(); } if (!$args) { - return ""; + return ''; } $buf = "Executing '"; $buf .= $args[0]; $buf .= "'"; if (count($args) > 0) { - $buf .= " with "; + $buf .= ' with '; $buf .= $this->describeArguments($args, 1); } else { $buf .= self::DISCLAIMER; @@ -379,14 +389,14 @@ public function describeCommand($args = null) * @param int $offset ignore entries before this index * @return string */ - protected function describeArguments(array $args = null, $offset = 0) + public function describeArguments(array $args = null, $offset = 0) { if ($args === null) { $args = $this->getArguments(); } if ($args === null || count($args) <= $offset) { - return ""; + return ''; } $buf = "argument"; diff --git a/classes/phing/types/CommandlineArgument.php b/classes/phing/types/CommandlineArgument.php index 3286cae57c..01e2e70093 100644 --- a/classes/phing/types/CommandlineArgument.php +++ b/classes/phing/types/CommandlineArgument.php @@ -41,13 +41,14 @@ public function setValue($value) * Line to split into several commandline arguments. * * @param string $line line to split into several commandline arguments + * @throws \BuildException */ public function setLine($line) { if ($line === null) { return; } - $this->parts = $this->outer->translateCommandline($line); + $this->parts = $this->outer::translateCommandline($line); } /** @@ -55,11 +56,11 @@ public function setLine($line) * PATH - ensures the right separator for the local platform * is used. * - * @param string $value a single commandline argument + * @param Path $value a single commandline argument */ - public function setPath($value) + public function setPath(Path $value): void { - $this->parts = [(string)$value]; + $this->parts = [(string) $value]; } /** @@ -69,7 +70,7 @@ public function setPath($value) * @param PhingFile $value * @internal param a $value single commandline argument. */ - public function setFile(PhingFile $value) + public function setFile(PhingFile $value): void { $this->parts = [$value->getAbsolutePath()]; } @@ -78,7 +79,7 @@ public function setFile(PhingFile $value) * Returns the parts this Argument consists of. * @return array string[] */ - public function getParts() + public function getParts(): array { return $this->parts; } diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index 1bd7f947a1..a85c023cf4 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -94,9 +94,9 @@ protected function assertAttributeIsSetTo($property, $value, $propertyName = nul $this->assertEquals($value, $rprop->getValue($task)); } - public function testPropertySetCommand() + public function testPropertySetCommandline() { - $this->assertAttributeIsSetTo('command', "echo 'foo'"); + $this->assertAttributeIsSetTo('commandline', new Commandline("echo 'foo'")); } public function testPropertySetDir() @@ -239,7 +239,7 @@ public function testFailOnNonExistingDir() $this->fail('Expected BuildException was not thrown'); } catch (BuildException $e) { $this->assertContains( - str_replace('/', DIRECTORY_SEPARATOR, "'/this/dir/does/not/exist' is not a valid directory"), + str_replace('/', DIRECTORY_SEPARATOR, "'/this/dir/does/not/exist' does not exist"), $e->getMessage() ); } @@ -256,8 +256,8 @@ public function testChangeToDir() public function testCheckreturnTrue() { - if ($this->windows) { - $this->markTestSkipped("Windows does not have '/bin/true'"); + if (FileSystem::getFileSystem()->which('true') === false) { + $this->markTestSkipped("'true' not found."); } $this->executeTarget(__FUNCTION__); $this->assertTrue(true); @@ -265,12 +265,12 @@ public function testCheckreturnTrue() /** * @expectedException BuildException - * @expectedExceptionMessage Task exited with code 1 + * @expectedExceptionMessage exec returned: 1 */ public function testCheckreturnFalse() { - if ($this->windows) { - $this->markTestSkipped("Windows does not have '/bin/false'"); + if (FileSystem::getFileSystem()->which('false') === false) { + $this->markTestSkipped("'false' not found."); } $this->executeTarget(__FUNCTION__); } @@ -290,7 +290,7 @@ public function testReturnProperty() public function testEscape() { $this->executeTarget(__FUNCTION__); - $this->assertInLogs($this->windows ? 'foo | cat' : 'foo | cat'); + $this->assertInLogs($this->windows ? '"foo" "|" "cat"' : 'foo | cat'); } public function testPassthru() @@ -315,9 +315,6 @@ public function testOutput() public function testError() { - if ($this->windows) { - $this->markTestSkipped("The script is unlikely to run on Windows"); - } $file = tempnam(sys_get_temp_dir(), 'phing-exectest-'); $this->project->setProperty('execTmpFile', $file); $this->executeTarget(__FUNCTION__); @@ -346,16 +343,7 @@ public function testNestedArg() /** * @expectedException BuildException - * @expectedExceptionMessage ExecTask: Either use "command" OR "executable" - */ - public function testExecutableAndCommand() - { - $this->executeTarget(__FUNCTION__); - } - - /** - * @expectedException BuildException - * @expectedExceptionMessage ExecTask: Please provide "command" OR "executable" + * @expectedExceptionMessage ExecTask: Please provide "executable" */ public function testMissingExecutableAndCommand() { @@ -368,7 +356,7 @@ public function testMissingExecutableAndCommand() public function testEscapedArg() { $this->executeTarget(__FUNCTION__); - $this->assertPropertyEquals('outval', 'abc$b3!SB'); + $this->assertPropertyEquals('outval', $this->windows ? '"abc$b3 SB"' : 'abc$b3!SB'); } public function testEscapedArgWithoutWhitespace() diff --git a/test/etc/tasks/system/ExecTest.xml b/test/etc/tasks/system/ExecTest.xml index 279550c029..f5dac5c0d2 100644 --- a/test/etc/tasks/system/ExecTest.xml +++ b/test/etc/tasks/system/ExecTest.xml @@ -12,7 +12,7 @@ - + @@ -119,7 +119,7 @@ + osfamily="${current}" /> @@ -134,11 +134,11 @@ - + - + @@ -147,7 +147,7 @@ - + The return property's value is: "${execReturnProp}" @@ -166,7 +166,7 @@ - + @@ -181,12 +181,6 @@ - - - - - - @@ -199,8 +193,8 @@ - - + + From 0d4b6b82b5c2029d525361fae46a15fa5617b0ed Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 16:28:14 +0100 Subject: [PATCH 05/12] Reduced complexity by extending from ExecTask --- classes/phing/tasks/system/ApplyTask.php | 738 ++++++++---------- classes/phing/tasks/system/AttribTask.php | 2 +- classes/phing/tasks/system/ExecTask.php | 44 +- classes/phing/types/CommandlineMarker.php | 40 +- classes/phing/types/Environment.php | 75 ++ .../phing/types/environment/EnvVariable.php | 94 +++ .../phing/tasks/system/ApplyTaskTest.php | 38 +- test/etc/tasks/system/ApplyTest.xml | 72 +- 8 files changed, 658 insertions(+), 445 deletions(-) create mode 100644 classes/phing/types/Environment.php create mode 100644 classes/phing/types/environment/EnvVariable.php diff --git a/classes/phing/tasks/system/ApplyTask.php b/classes/phing/tasks/system/ApplyTask.php index be128833ba..4c70cc43e6 100644 --- a/classes/phing/tasks/system/ApplyTask.php +++ b/classes/phing/tasks/system/ApplyTask.php @@ -27,54 +27,13 @@ * * @todo Add support for mapper, targetfile expressions */ -class ApplyTask extends Task +class ApplyTask extends ExecTask { use ResourceAware; - /** - * Configuration(s) - * - */ - //[TBA]const TARGETFILE_ID = '__TARGETFILE__'; const SOURCEFILE_ID = '__SOURCEFILE__'; - - /** - * Commandline managing object - * @var Commandline - */ - protected $commandline; - - /** - * Working directory - * @var phingfile - */ - protected $dir; protected $currentdirectory; - /** - * Command to be executed - * @var string - */ - protected $realCommand; - - /** - * Escape (shell) command using 'escapeshellcmd' before execution - * @var boolean - */ - protected $escape = false; - - /** - * Where to direct output - * @var phingfile - */ - protected $output; - - /** - * Where to direct error - * @var phingfile - */ - protected $error; - /** * Whether output should be appended to or overwrite an existing file * @var boolean @@ -94,35 +53,12 @@ class ApplyTask extends Task */ protected $addsourcefile = true; - /** - * Whether to spawn the command execution as a background process - * @var boolean - */ - protected $spawn = false; - - /** - * Property name to set with return value - * @var string - */ - protected $returnProperty; - - /** - * Property name to set with output value - * @var string - */ - protected $outputProperty; - /** * Whether the filenames should be passed on the command line as relative pathnames (relative to the base directory of the corresponding fileset/list) * @var boolean */ protected $relative = false; - /** - * Operating system information - * @var string - */ - protected $os; protected $currentos; protected $osvariant; @@ -132,27 +68,12 @@ class ApplyTask extends Task */ protected $loglevel = null; - /** - * Fail on command that exits with a returncode other than zero - * @var boolean - * - */ - protected $failonerror = false; - - /** - * Whether to use PHP's passthru() function instead of exec() - * @var boolean - */ - protected $passthru = false; - - /** * Whether to use forward-slash as file-separator on the file names * @var boolean */ protected $forwardslash = false; - /** * Limit the amount of parallelism by passing at most this many sourcefiles at once * (Set it to <= 0 for unlimited) @@ -160,52 +81,46 @@ class ApplyTask extends Task */ protected $maxparallel = 0; - /** - * Sets the command executable information - * - * @param string $executable Executable path - * - * @return void - */ - public function setExecutable($executable) - { - $this->commandline->setExecutable((string) $executable); - } + protected static $types = [ + 'FILE' => 'file', + 'DIR' => 'dir', + 'BOTH' => 'both', + ]; - /** - * Specify the working directory for the command execution. - * - * @param PhingFile $dir Set the working directory as specified - * - * @return void - */ - public function setDir(PhingFile $dir) - { - $this->dir = $dir; - } + protected $type = 'file'; + /** @var CommandlineMarker $targetFilePos */ + protected $targetFilePos; + /** @var CommandlineMarker $srcFilePos */ + protected $srcFilePos; + protected $srcIsFirst = true; + + protected $skipEmpty = false; + private $force = false; + private $mapper; + private $destDir; + + /** @var Mapper $mapperElement */ + private $mapperElement; /** - * Escape command using 'escapeshellcmd' before execution - * - * @param boolean $escape Escape command before execution - * - * @return void + * Set whether empty filesets will be skipped. If true and + * no source files have been found or are newer than their + * corresponding target files, the command will not be run. + * @param boolean $skip whether to skip empty filesets. */ - public function setEscape($escape) + public function setSkipEmptyFilesets(boolean $skip) { - $this->escape = (bool) $escape; + $this->skipEmpty = $skip; } /** - * File to which output should be written - * - * @param PhingFile $outputfile Output log file + * Specify the directory where target files are to be placed. * - * @return void + * @param PhingFile $dest the File object representing the destination directory. */ - public function setOutput(PhingFile $outputfile) + public function setDest(PhingFile $dest) { - $this->output = $outputfile; + $this->destDir = $dest; } /** @@ -245,58 +160,6 @@ public function setAddsourcefile($addsourcefile) $this->addsourcefile = (bool) $addsourcefile; } - /** - * File to which error output should be written - * - * @param PhingFile $errorfile Error log file - * - * @return void - */ - public function setError(PhingFile $errorfile) - { - $this->error = $errorfile; - } - - - /** - * Whether to spawn the command and run as background process - * - * @param boolean $spawn If the command is to be run as a background process - * - * @return void - */ - public function setSpawn($spawn) - { - $this->spawn = (bool) $spawn; - } - - - /** - * The name of property to set to return value - * - * @param string $propertyname Property name - * - * @return void - */ - public function setReturnProperty($propertyname) - { - $this->returnProperty = (string) $propertyname; - } - - - /** - * The name of property to set to output value - * - * @param string $propertyname Property name - * - * @return void - */ - public function setOutputProperty($propertyname) - { - $this->outputProperty = (string) $propertyname; - } - - /** * Whether the filenames should be passed on the command line as relative * pathnames (relative to the base directory of the corresponding fileset/list) @@ -311,32 +174,6 @@ public function setRelative($relative) $this->relative = (bool) $relative; } - - /** - * Specify OS (or multiple OS) that must match in order to execute this command. - * - * @param string $os Operating system string (e.g. "Linux") - * - * @return void - */ - public function setOs($os) - { - $this->os = (string) $os; - } - - - /** - * Whether to use PHP's passthru() function instead of exec() - * - * @param boolean $passthru If passthru shall be used - * - * @return void - */ - public function setPassthru($passthru) - { - $this->passthru = (bool) $passthru; - } - /** * Fail on command exits with a returncode other than zero * @@ -346,15 +183,7 @@ public function setPassthru($passthru) */ public function setFailonerror($failonerror) { - $this->failonerror = (bool) $failonerror; - } - - /** - * @param $failonerror - */ - public function setCheckreturn($failonerror) - { - $this->setFailonerror($failonerror); + $this->checkreturn = (bool) $failonerror; } /** @@ -382,126 +211,197 @@ public function setMaxparallel($max) $this->maxparallel = (int) $max; } - /** [TBA] + public function setForce(bool $force) + { + $this->force = $force; + } + + /** + * Set whether the command works only on files, directories or both. + * @param string $type a FileDirBoth EnumeratedAttribute. + */ + public function setType(string $type) + { + $this->type = $type; + } + + /** * Supports embedded element. * - * @return void + * @return CommandlineMarker + * @throws \BuildException */ - /**public function createTargetfile() { - * return $this->commandline->addArguments( array(self::TARGETFILE_ID) ); - * }*/ + public function createTargetfile() + { + + if ($this->targetFilePos !== null) { + throw new BuildException($this->getTaskType() . " doesn\'t support multiple " + . "targetfile elements.", $this->getLocation()); + } + + $this->targetFilePos = $this->commandline->createMarker(); + $this->srcIsFirst = ($this->srcFilePos !== null); + + return $this->targetFilePos; + } /** * Supports embedded element. * - * @return void + * @return CommandlineMarker + * @throws \BuildException */ public function createSrcfile() { - return $this->commandline->addArguments([self::SOURCEFILE_ID]); + if ($this->srcFilePos !== null) { + throw new BuildException($this->getTaskType() . " doesn\'t support multiple " + . "srcfile elements.", $this->getLocation()); + } + + $this->srcFilePos = $this->commandline->createMarker(); + return $this->srcFilePos; } /** - * Supports embedded element. - * - * @return CommandlineArgument + * @return Mapper + * @throws \BuildException */ - public function createArg() + public function createMapper() { - return $this->commandline->createArgument(); + if ($this->mapperElement !== null) { + throw new BuildException( + 'Cannot define more than one mapper', + $this->getLocation() + ); + } + $this->mapperElement = new Mapper($this->getProject()); + return $this->mapperElement; } /**********************************************************************************/ /**************************** T A S K M E T H O D S ******************************/ /**********************************************************************************/ - /** - * Class Initialization - * @return void - */ - public function init() - { - $this->commandline = new Commandline(); - $this->loglevel = Project::MSG_VERBOSE; - } - /** * Do work - * @throws BuildException + * @throws \BuildException */ public function main() { - - // Log - $this->log('Started ', $this->loglevel); - - // Initialize // - $this->initialize(); - - // Validate O.S. applicability - if ($this->validateOS()) { - - // Build the command // - $this->buildCommand(); - - // Process // - // - FileLists - foreach ($this->filelists as $fl) { - $this->process($fl->getFiles($this->project), $fl->getDir($this->project)); + try { + // Log + $this->log('Started ', $this->loglevel); + // Initialize // + $this->prepare(); + $haveExecuted = false; + // Validate O.S. applicability + if ($this->isValidOs()) { + // Build the command // + $this->buildCommand(); + // Process // + $totalFiles = 0; + $totalDirs = 0; + $fileNames = []; + // - FileSets + foreach ($this->filesets as $fs) { + $currentType = $this->type; + if ($fs instanceof DirSet) { + if ($this->type !== self::$types['DIR']) { + $this->log("Found a nested dirset but type is $this->type . " + . "Temporarily switching to type=\"dir\" on the" + . " assumption that you really did mean" + . " not .", Project::MSG_DEBUG + ); + $currentType = 'dir'; + } + } + $base = $fs->getDir($this->project); + $ds = $fs->getDirectoryScanner($this->project); + if ($currentType !== self::$types['DIR']) { + $s = $this->getFiles($base, $ds); + foreach ($s as $fileName) { + $totalFiles++; + $fileNames[] = $fileName; + $baseDirs[] = $base; + } + } + if ($currentType !== self::$types['FILE']) { + $s = $this->getDirs($base, $ds); + foreach ($s as $fileName) { + $totalDirs++; + $fileNames[] = $fileName; + $baseDirs[] = $base; + } + } + if (count($fileNames) === 0 && $this->skipEmpty) { + $this->logSkippingFileset($currentType, $ds, $base); + continue; + } + $this->process( + $fs->getDirectoryScanner($this->project)->getIncludedFiles(), + $fs->getDir($this->project) + ); + $haveExecuted = true; + } + unset($this->filesets); + // - FileLists + /** @var FileList $fl */ + foreach ($this->filelists as $fl) { + $totalFiles++; + $this->process($fl->getFiles($this->project), $fl->getDir($this->project)); + $haveExecuted = true; + } + unset($this->filelists); } - unset($this->filelists); - - // - FileSets - foreach ($this->filesets as $fs) { - $this->process( - $fs->getDirectoryScanner($this->project)->getIncludedFiles(), - $fs->getDir($this->project) - ); + if ($haveExecuted) { + $this->log( + "Applied " . $this->commandline->getExecutable() . " to " + . $totalFiles . " file" + . ($totalFiles !== 1 ? "s" : "") . " and " + . $totalDirs . " director" + . ($totalDirs !== 1 ? "ies" : "y") . ".", + $this->loglevel); } - unset($this->filesets); + /// Cleanup // + $this->cleanup(); + // Log + $this->log('End ', $this->loglevel); + } catch (IOException | NullPointerException | UnexpectedValueException $e) { + throw new BuildException('Execute failed: ' . $e, $e, $this->getLocation()); } - - /// Cleanup // - $this->cleanup(); - - // Log - $this->log('End ', $this->loglevel); } /**********************************************************************************/ /********************** T A S K C O R E M E T H O D S ***************************/ /**********************************************************************************/ - /** - * Checks whether the current O.S. should be supported - * - * @return boolean False if the exec command shall not be run - */ - protected function validateOS() + protected function getFiles(PhingFile $baseDir, DirectoryScanner $ds) { + return $this->restrict($ds->getIncludedFiles(), $baseDir); + } - // Log - $this->log('Validating Operating System information ', $this->loglevel); - - // Checking whether'os' information is specified - if (empty($this->os)) { - - // Log - $this->log("Operating system information not specified. Skipped checking. ", $this->loglevel); - - return true; - } - - // Validating the operating system information - $matched = stripos($this->os, $this->currentos) !== false; + protected function getDirs(PhingFile $baseDir, DirectoryScanner $ds) + { + return $this->restrict($ds->getIncludedDirectories(), $baseDir); + } - // Log - $this->log( - "Operating system '" . $this->currentos . "' " . ($matched ? '' : 'not ') . "found in " . $this->os, - $this->loglevel - ); + protected function restrict($s, PhingFile $baseDir) + { + $sfs = new SourceFileScanner($this); + return ($this->mapper === null || $this->force) + ? $s + : $sfs->restrict($s, $baseDir, $this->destDir, $this->mapper); + } - return $matched; + private function logSkippingFileset($currentType, DirectoryScanner $ds, PhingFile $base) + { + $includedCount = ( + ($currentType !== self::$types['DIR']) ? $ds->getIncludedFilesCount() : 0 + ) + ( + ($currentType !== self::$types['FILES']) ? $ds->getIncludedDirectoriesCount() : 0); + $this->log("Skipping fileset for directory " . $base . ". It is " + . (($includedCount > 0) ? "up to date." : "empty."), + $this->loglevel); } /** @@ -509,18 +409,21 @@ protected function validateOS() * - Required information validation * - Working directory * - * @param none - * * @return void + * @throws \BuildException + * @throws IOException */ - private function initialize() + protected function prepare() { - // Log $this->log('Initializing started ', $this->loglevel); ///// Validating the required parameters ///// + if (!in_array($this->type, self::$types)) { + throw new BuildException('Type must be one of \'file\', \'dir\' or \'both\'.'); + } + // Executable if ($this->commandline->getExecutable() === null) { $this->throwBuildException('Please provide "executable" information'); @@ -542,8 +445,7 @@ private function initialize() // Log $this->log( - 'Working directory change ' . ($dirchangestatus ? 'successful' : 'failed') . ' to ' . $this->dir->getPath( - ), + 'Working directory change ' . ($dirchangestatus ? 'successful' : 'failed') . ' to ' . $this->dir->getPath(), $this->loglevel ); } @@ -557,9 +459,9 @@ private function initialize() $this->log('Operating System identified : ' . $this->currentos, $this->loglevel); // Getting the O.S. type identifier - // Validating the 'filesystem' for determining the OS type [UNIX, WINDOWS] + // Validating the 'filesystem' for determining the OS type [UNIX, WINNT and WIN32] // (Another usage could be with 'os.name' for determination) - if ('WINDOWS' == Phing::getProperty('host.fstype')) { + if ('WIN' === strtoupper(substr(Phing::getProperty('host.fstype'), 0, 3))) { $this->osvariant = 'WIN'; // Probable Windows flavour } else { $this->osvariant = 'LIN'; // Probable GNU/Linux flavour @@ -568,18 +470,38 @@ private function initialize() // Log $this->log('Operating System variant identified : ' . $this->osvariant, $this->loglevel); + if (count($this->filesets) === 0 && count($this->filelists) === 0 && count($this->getDirSets()) === 0) { + throw new BuildException("no resources specified", + $this->getLocation()); + } + if ($this->targetFilePos !== null && $this->mapperElement === null) { + throw new BuildException("targetfile specified without mapper", + $this->getLocation()); + } + if ($this->destDir !== null && $this->mapperElement === null) { + throw new BuildException("dest specified without mapper", + $this->getLocation()); + } + + if ($this->mapperElement !== null) { + $this->mapper = $this->mapperElement->getImplementation(); + $this->log('Mapper identified : ' . get_class($this->mapper), $this->loglevel); + } + + $this->commandline->setEscape($this->escape); + // Log $this->log('Initializing completed ', $this->loglevel); - - return; } /** * Builds the full command to execute and stores it in $realCommand. * * @return void + * + * @throws \BuildException */ - private function buildCommand() + protected function buildCommand() { // Log @@ -590,39 +512,29 @@ private function buildCommand() // Adding the source filename at the end of command, validating the existing // sourcefile position explicit mentioning - if (($this->addsourcefile === true) && (strpos($this->realCommand, self::SOURCEFILE_ID) === false)) { + if ($this->addsourcefile === true) { $this->realCommand .= ' ' . self::SOURCEFILE_ID; } // Setting command output redirection with content appending if ($this->output !== null) { - $this->realCommand .= ' 1>'; - $this->realCommand .= ($this->appendoutput ? '>' : ''); // Append output - $this->realCommand .= ' ' . escapeshellarg($this->output->getPath()); + $this->realCommand .= sprintf(' 1>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->output->getPath())); } elseif ($this->spawn) { // Validating the 'spawn' configuration, and redirecting the output to 'null' - - // Validating the O.S. variant - if ('WIN' == $this->osvariant) { - $this->realCommand .= ' > NUL'; // MS Windows output nullification - } else { - $this->realCommand .= ' 1>/dev/null'; // GNU/Linux output nullification - } + $this->realCommand .= sprintf(' %s', 'WIN' === $this->osvariant ? '> NUL' : '1>/dev/null'); $this->log("For process spawning, setting Output nullification ", $this->loglevel); } // Setting command error redirection with content appending if ($this->error !== null) { - $this->realCommand .= ' 2>'; - $this->realCommand .= ($this->appendoutput ? '>' : ''); // Append error - $this->realCommand .= ' ' . escapeshellarg($this->error->getPath()); + $this->realCommand .= sprintf(' 2>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->error->getPath())); } // Setting the execution as a background process if ($this->spawn) { // Validating the O.S. variant - if ('WIN' == $this->osvariant) { + if ('WIN' === $this->osvariant) { $this->realCommand = 'start /b ' . $this->realCommand; // MS Windows background process forking } else { $this->realCommand .= ' &'; // GNU/Linux background process forking @@ -634,114 +546,138 @@ private function buildCommand() // Log $this->log('Command building completed ', $this->loglevel); - - return; } /** * Processes the files list with provided information for execution * - * @param array $files File list for processing + * @param array $srcFiles File list for processing * @param string $basedir Base directory of the file list * * @return void + * @throws \BuildException + * @throws IOException + * @throws NullPointerException */ - private function process($files, $basedir) + private function process($srcFiles, $basedir) { - // Log - $this->log("Processing Filelist with base directory ($basedir) ", $this->loglevel); + $this->log("Processing files with base directory ($basedir) ", $this->loglevel); + $targets = []; + if ($this->targetFilePos !== null) { + $addedFiles = []; + foreach ($srcFiles as $count => $file) { + if ($this->mapper !== null) { + $subTargets = $this->mapper->main($file); + if ($subTargets !== null) { + foreach ($subTargets as $subTarget) { + if ($this->relative) { + $name = $subTarget; + } else { + $name = (new PhingFile($this->destDir, $subTarget))->getAbsolutePath(); + } + if ($this->forwardslash && PhingFile::$separator !== '/') { + $name = str_replace(PhingFile::$separator, '/', $name); + } + if (!isset($addedFiles[$name])) { + $targets[] = $name; + $addedFiles[] = $name; + } + } + } + } + } + } + $targetFiles = $targets; - // Process each file in the list for applying the 'realcommand' - foreach ($files as $count => $file) { + if (!$this->addsourcefile) { + $srcFiles = []; + } + $orig = $this->commandline->getCommandline(); + $result = []; // range(0,count($orig) + count($srcFiles) + count($targetFiles)); - // Preparing the absolute filename with relative path information - $absolutefilename = $this->getFilePath($file, $basedir, $this->relative); + $srcIndex = count($orig); + if ($this->srcFilePos !== null) { + $srcIndex = $this->srcFilePos->getPosition(); + } + if ($this->targetFilePos !== null) { + $targetIndex = $this->targetFilePos->getPosition(); - // Checking whether 'parallel' information is enabled. If enabled, append all - // the file names as arguments, and run only once. - if ($this->parallel) { + if ($srcIndex < $targetIndex || ($srcIndex === $targetIndex && $this->srcIsFirst)) { + // 0 --> srcIndex + $result[] = $orig; - // Checking whether 'maxparallel' setting describes parallelism limitation - // by passing at most 'maxparallel' many sourcefiles at once - $slicedfiles = array_splice( - $files, - 0, - (($this->maxparallel > 0) ? $this->maxparallel : count($files)) - ); + // srcIndex --> targetIndex + $result += array_slice($orig, $srcIndex + count($srcFiles), $targetIndex - $srcIndex, true); - $absolutefilename = implode(' ', $this->getFilePath($slicedfiles, $basedir, $this->relative)); - } + $this->insertTargetFiles($targetFiles, $result, + $targetIndex + count($srcFiles), + $this->targetFilePos->getPrefix(), + $this->targetFilePos->getSuffix()); - // Checking whether the forward-slash as file-separator has been set. - // (Applicability: The source {and target} file names must use the forward slash as file separator) - if ($this->forwardslash) { - $absolutefilename = str_replace(DIRECTORY_SEPARATOR, '/', $absolutefilename); + // targetIndex --> end + $result = array_merge(array_slice($orig, $targetIndex + count($srcFiles) + count($targetFiles), + count($orig) - $targetIndex, true), $result); + } else { + // 0 --> targetIndex + $result[] = $orig; + $result[] = $targetFiles; + $result = array_merge(... $result); + + // targetIndex --> srcIndex + $result = array_merge(array_slice($orig, $targetIndex + count($targetFiles), $srcIndex - $targetIndex, + true), $result); + + // srcIndex --> end + $result = array_merge(array_slice($orig, $srcIndex + count($srcFiles) + count($targetFiles), + count($orig) - $srcIndex, true), $result); + $srcIndex += count($targetFiles); } - // Preparing the command to be executed - $filecommand = str_replace([self::SOURCEFILE_ID], [$absolutefilename], $this->realCommand); - - // Command execution - list($returncode, $output) = $this->executeCommand($filecommand); + } else { // no targetFilePos - // Process the stuff on the first command execution only - if (0 == $count) { - - // Sets the return property - if ($this->returnProperty) { - $this->project->setProperty($this->returnProperty, $returncode); - } + // 0 --> srcIndex + $result = array_merge(array_slice($orig, 0, $srcIndex, true), $result); + // srcIndex --> end + $result = array_merge(array_slice($orig, $srcIndex + count($srcFiles), count($orig) - $srcIndex, true), + $result); + } + // fill in source file names + foreach ($srcFiles as $i => $file) { + if ($this->relative) { + $src = $file; + } else { + $src = (new PhingFile($basedir, $file))->getAbsolutePath(); } - - // Sets the output property - if ($this->outputProperty) { - $previousValue = $this->project->getProperty($this->outputProperty); - if (! empty($previousValue)) { - $previousValue .= "\n"; - } - $this->project->setProperty($this->outputProperty, $previousValue . implode("\n", $output)); + if ($this->forwardslash && PhingFile::$separator !== '/') { + $src = str_replace(PhingFile::$separator, '/', $src); } - - // Validating the 'return-code' - if (($this->failonerror) && ($returncode != 0)) { - $this->throwBuildException("Task exited with code ($returncode)"); + if ($this->srcFilePos !== null && (count($this->srcFilePos->getPrefix()) > 0 + || count($this->srcFilePos->getSuffix()) > 0)) { + $src = $this->srcFilePos->getPrefix() . $src . $this->srcFilePos->getSuffix(); } + $result[$srcIndex + $i] = $src; + } - // Validate the 'parallel' information for command execution. If the command has been - // executed with the filenames as argument, considering 'maxparallel', just break. - if (($this->parallel) && (!array_key_exists($count, $files))) { - break; - } - } // Each file processing loop ends + $this->commandline = new Commandline(implode(' ', $result)); - return; - } + [$returncode, $output] = $this->executeCommand(); - /** - * Executes the specified command and returns the return code & output. - * - * @param string $command - * - * @return array array(return code, array with output) - */ - private function executeCommand($command) - { - - // Var(s) - $output = []; - $return = null; - - // Validating the command executor container - ($this->passthru ? passthru($command, $return) : exec($command, $output, $return)); + $this->maybeSetReturnPropertyValue($returncode); - // Log - $this->log( - 'Command execution : (' . ($this->passthru ? 'passthru' : 'exec') . ') : ' . $command . " : completed with return code ($return) ", - $this->loglevel - ); + // Sets the output property + if ($this->outputProperty) { + $previousValue = $this->project->getProperty($this->outputProperty); + if (!empty($previousValue)) { + $previousValue .= "\n"; + } + $this->project->setProperty($this->outputProperty, $previousValue . implode("\n", $output)); + } - return [$return, $output]; + // Validating the 'return-code' + if ($this->checkreturn && ($returncode !== 0)) { + $this->throwBuildException("Task exited with code ($returncode)"); + } } /** @@ -750,36 +686,39 @@ private function executeCommand($command) * * @return void */ - private function cleanup() + protected function cleanup($return = null, $output = null): void { - // Restore working directory if ($this->dir !== null) { @chdir($this->currentdirectory); } - - return; } /** * Prepares the filename per base directory and relative path information * - * @param $filename + * @param array|string $filename * @param $basedir * @param $relative * * @return mixed processed filenames + * + * @throws IOException */ public function getFilePath($filename, $basedir, $relative) { // Validating the 'file' information - $files = (is_array($filename)) ? $filename : [$filename]; + $files = (array) $filename; // Processing the file information foreach ($files as $index => $file) { - $absolutefilename = (($relative === false) ? ($basedir . DIRECTORY_SEPARATOR) : ''); + $absolutefilename = (($relative === false) ? ($basedir . PhingFile::$separator) : ''); $absolutefilename .= $file; - $files[$index] = $absolutefilename; + if ($relative === false) { + $files[$index] = (new FileUtils())->normalize($absolutefilename); + } else { + $files[$index] = $absolutefilename; + } } return (is_array($filename) ? $files : $files[0]); @@ -788,12 +727,13 @@ public function getFilePath($filename, $basedir, $relative) /** * Throws the exception with specified information * - * @param $information Exception information + * @param string $information Exception information * * @throws BuildException + * * @return void */ - private function throwBuildException($information) + private function throwBuildException($information): void { throw new BuildException('ApplyTask: ' . (string) $information); } diff --git a/classes/phing/tasks/system/AttribTask.php b/classes/phing/tasks/system/AttribTask.php index e554728036..b1c946c92c 100644 --- a/classes/phing/tasks/system/AttribTask.php +++ b/classes/phing/tasks/system/AttribTask.php @@ -133,7 +133,7 @@ protected function checkConfiguration() * @param mixed $e * @throws BuildException */ - public function setExecutable($e) + public function setExecutable($e): void { throw new BuildException( $this->getTaskType() . ' doesn\'t support the executable attribute', diff --git a/classes/phing/tasks/system/ExecTask.php b/classes/phing/tasks/system/ExecTask.php index c2d9e7257c..2d6becce60 100644 --- a/classes/phing/tasks/system/ExecTask.php +++ b/classes/phing/tasks/system/ExecTask.php @@ -125,6 +125,7 @@ class ExecTask extends Task private $executable; private $resolveExecutable = false; private $searchPath = false; + private $env; /** * @throws \BuildException @@ -133,6 +134,7 @@ public function __construct() { parent::__construct(); $this->commandline = new Commandline(); + $this->env = new Environment(); } /** @@ -251,7 +253,7 @@ protected function buildCommand() */ protected function executeCommand() { - $cmdl = (string) $this->commandline . $this->realCommand; + $cmdl = (string) $this->commandline; // . $this->realCommand; $this->log('Executing command: ' . $cmdl, $this->logLevel); @@ -559,6 +561,16 @@ public function setLevel($level): void } } + /** + * Add an environment variable to the launched process. + * + * @param EnvVariable $var new environment variable. + */ + public function addEnv(EnvVariable $var) + { + $this->env->addVariable($var); + } + /** * Creates a nested tag. * @@ -674,7 +686,16 @@ protected function resolveExecutable($exec, $mustSearchPath): ?string // couldn't find it - must be on path if ($mustSearchPath) { $p = null; - if (getenv('path')) { + $environment = $this->env->getVariables(); + if ($environment !== null) { + foreach ($environment as $env) { + if ($this->isPath($env)) { + $p = new Path($this->getProject(), $this->getPath($env)); + break; + } + } + } + if ($p === null) { $p = new Path($this->getProject(), getenv('path')); } if ($p !== null) { @@ -690,4 +711,23 @@ protected function resolveExecutable($exec, $mustSearchPath): ?string return $exec; } + + private function isPath($line) + { + return StringHelper::startsWith('PATH=', $line) || StringHelper::startsWith('Path=', $line); + } + + private function getPath($value) + { + if (is_string($value)) { + return StringHelper::substring($value, strlen("PATH=")); + } + + if (is_array($value)) { + $p = $value['PATH']; + return $p ?? $value['Path']; + } + + throw new InvalidArgumentException('$value should be of type array or string.'); + } } diff --git a/classes/phing/types/CommandlineMarker.php b/classes/phing/types/CommandlineMarker.php index af0a92c9e5..65f0b6fab5 100644 --- a/classes/phing/types/CommandlineMarker.php +++ b/classes/phing/types/CommandlineMarker.php @@ -14,6 +14,8 @@ class CommandlineMarker private $position; private $realPos = -1; private $outer; + private $prefix; + private $suffix; /** * @param Commandline $outer @@ -33,7 +35,7 @@ public function __construct(Commandline $outer, $position) */ public function getPosition() { - if ($this->realPos == -1) { + if ($this->realPos === -1) { $this->realPos = ($this->outer->executable === null ? 0 : 1); for ($i = 0; $i < $this->position; $i++) { $arg = $this->outer->arguments[$i]; @@ -43,4 +45,40 @@ public function getPosition() return $this->realPos; } + + /** + * Set the prefix to be placed in front of the inserted argument. + * + * @param string $prefix fixed prefix string. + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix !== null ? $prefix : ''; + } + + /** + * Get the prefix to be placed in front of the inserted argument. + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Set the suffix to be placed at the end of the inserted argument. + * + * @param string $suffix fixed suffix string. + */ + public function setSuffix($suffix) + { + $this->suffix = $suffix !== null ? $suffix : ''; + } + + /** + * Get the suffix to be placed at the end of the inserted argument. + */ + public function getSuffix() + { + return $this->suffix; + } } diff --git a/classes/phing/types/Environment.php b/classes/phing/types/Environment.php new file mode 100644 index 0000000000..44eb266586 --- /dev/null +++ b/classes/phing/types/Environment.php @@ -0,0 +1,75 @@ +. + */ + +/** + * Wrapper for environment variables. + * + */ +class Environment +{ + // CheckStyle:VisibilityModifier OFF - bc + + /** + * a vector of type Environment.Variable + * @see Variable + */ + protected $variables; + + /** + * constructor + */ + public function __construct() + { + $this->variables = new ArrayObject(); + } + + /** + * add a variable. + * Validity checking is not performed at this point. Duplicates + * are not caught either. + * @param EnvVariable $var new variable. + */ + public function addVariable(EnvVariable $var) + { + $this->variables->append($var); + } + + /** + * get the variable list as an array + * @return array of key=value assignment strings + * @throws BuildException if any variable is misconfigured + */ + public function getVariables() + { + if ($this->variables->count() === 0) { + return null; + } + return $this->variables->getArrayCopy(); + } + + /** + * Get the raw vector of variables. This is not a clone. + * @return ArrayObject a potentially empty (but never null) vector of elements of type + * Variable + */ + public function getVariablesObject() + { + return $this->variables; + } +} diff --git a/classes/phing/types/environment/EnvVariable.php b/classes/phing/types/environment/EnvVariable.php new file mode 100644 index 0000000000..93240f07cd --- /dev/null +++ b/classes/phing/types/environment/EnvVariable.php @@ -0,0 +1,94 @@ +key = $key; + } + + /** + * set the value + * @param string $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * key accessor + * @return string key + */ + public function getKey() + { + return $this->key; + } + + /** + * value accessor + * @return string value + */ + public function getValue() + { + return $this->value; + } + + /** + * stringify path and assign to the value. + * The value will contain all path elements separated by the appropriate + * separator + * @param Path $path + */ + public function setPath(Path $path) + { + $this->value = (string) $path; + } + + /** + * get the absolute path of a file and assign it to the value + * @param PhingFile $file file to use as the value + */ + public function setFile(PhingFile $file) + { + $this->value = $file->getAbsolutePath(); + } + + /** + * get the assignment string + * This is not ready for insertion into a property file without following + * the escaping rules of the properties class. + * @return string of the form key=value. + * @throws BuildException if key or value are unassigned + */ + public function getContent() + { + $this->validate(); + return trim($this->key) . '=' . trim($this->value); + } + + /** + * checks whether all required attributes have been specified. + * @throws BuildException if key or value are unassigned + */ + public function validate() + { + if ($this->key === null || $this->value === null) { + throw new BuildException('key and value must be specified for environment variables.'); + } + } +} diff --git a/test/classes/phing/tasks/system/ApplyTaskTest.php b/test/classes/phing/tasks/system/ApplyTaskTest.php index 2fffb5821e..f675610750 100644 --- a/test/classes/phing/tasks/system/ApplyTaskTest.php +++ b/test/classes/phing/tasks/system/ApplyTaskTest.php @@ -61,7 +61,7 @@ public function testPropertySetOs() */ public function testPropertySetDir() { - $this->assertAttributeIsSetTo('dir', new PhingFile('/tmp/')); + $this->assertAttributeIsSetTo('dir', new PhingFile($this->project->getProperty('php.tmpdir'))); } /** @@ -109,7 +109,7 @@ public function testPropertySetOutputProperty() */ public function testPropertySetCheckReturn() { - $this->assertAttributeIsSetTo('checkreturn', true, 'failonerror'); + $this->assertAttributeIsSetTo('checkreturn', true); } /** @@ -117,7 +117,7 @@ public function testPropertySetCheckReturn() */ public function testPropertySetOutput() { - $this->assertAttributeIsSetTo('output', new PhingFile('/tmp/outputfilename')); + $this->assertAttributeIsSetTo('output', new PhingFile($this->project->getProperty('php.tmpdir') . '/outputfilename')); } /** @@ -125,7 +125,7 @@ public function testPropertySetOutput() */ public function testPropertySetError() { - $this->assertAttributeIsSetTo('error', new PhingFile('/tmp/errorfilename')); + $this->assertAttributeIsSetTo('error', new PhingFile($this->project->getProperty('php.tmpdir') . '/errorfilename')); } /** @@ -184,7 +184,7 @@ public function testDoNotExecuteOnWrongOs() // Process $this->executeTarget(__FUNCTION__); - $this->assertInLogs('Not found in unknownos'); + $this->assertInLogs('was not found in the specified list of valid OSes: unknownos'); $this->assertNotContains('this should not be executed', $this->getOutput()); } @@ -203,8 +203,7 @@ public function testExecuteOnCorrectOs() */ public function testFailOnNonExistingDir() { - $nonExistentDir = DIRECTORY_SEPARATOR - . 'tmp' . DIRECTORY_SEPARATOR + $nonExistentDir = $this->project->getProperty('php.tmpdir') . DIRECTORY_SEPARATOR . 'non' . DIRECTORY_SEPARATOR . 'existent' . DIRECTORY_SEPARATOR . 'dir'; @@ -296,7 +295,7 @@ public function testEscape() public function testPassThru() { $this->executeTarget(__FUNCTION__); - $this->assertInLogs('Command execution : (passthru)'); + $this->assertInLogs('Executing command:'); } /** @@ -392,7 +391,7 @@ public function testMissingExecutable() public function testEscapedArg() { $this->executeTarget(__FUNCTION__); - $this->assertPropertyEquals('outval', 'abc$b3!SB'); + $this->assertPropertyEquals('outval', $this->windows ? '"abc$b3 SB"' : 'abc$b3!SB'); } /** @@ -404,7 +403,7 @@ public function testRelativeSourceFilenames() if ($this->windows) { $this->markTestSkipped("Windows does not have 'ls'"); } - + $this->executeTarget(__FUNCTION__); $this->assertNotInLogs('/etc/'); } @@ -454,9 +453,26 @@ public function testParallel() foreach($this->logBuffer as $log) { $messages[] = $log['message']; } - $this->assertEquals(1, substr_count(implode("\n", $messages), 'Command execution :')); + $this->assertEquals(1, substr_count(implode("\n", $messages), 'Executing command:')); + } + + public function testMapperSupport() + { + // Getting a temp. file + $tempfile = tempnam(sys_get_temp_dir(), 'phing-exectest-'); + + // Setting the property + $this->project->setProperty('execTmpFile', $tempfile); + + $this->executeTarget(__FUNCTION__); + $messages = []; + foreach($this->logBuffer as $log) { + $messages[] = $log['message']; + } + $this->assertTrue(in_array('Applied echo to 4 files and 0 directories.', $messages)); } + /**********************************************************************************/ /************************** H E L P E R M E T H O D S ****************************/ /**********************************************************************************/ diff --git a/test/etc/tasks/system/ApplyTest.xml b/test/etc/tasks/system/ApplyTest.xml index 1eeace9b12..7a7b2d8747 100644 --- a/test/etc/tasks/system/ApplyTest.xml +++ b/test/etc/tasks/system/ApplyTest.xml @@ -17,39 +17,39 @@ - + - + - + - + - + - + - + - + - + - + - + - + @@ -76,7 +76,7 @@ - + @@ -85,7 +85,7 @@ - + @@ -94,9 +94,9 @@ - + - + @@ -104,7 +104,7 @@ - + @@ -118,7 +118,7 @@ - + @@ -126,9 +126,9 @@ - + - + @@ -145,7 +145,7 @@ The return property's value is: "${execReturnProp}" - + @@ -163,9 +163,9 @@ - + - + @@ -180,9 +180,9 @@ - + - + @@ -191,7 +191,7 @@ - + @@ -199,13 +199,13 @@ - + - + @@ -230,7 +230,7 @@ - + @@ -238,11 +238,21 @@ - + - + + + + + + + + + + + \ No newline at end of file From 18726cc87d9e0f0953def2fdab9f6ba260cf03b9 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 16:39:14 +0100 Subject: [PATCH 06/12] Removed trailing whitespace --- classes/phing/tasks/system/ApplyTask.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/classes/phing/tasks/system/ApplyTask.php b/classes/phing/tasks/system/ApplyTask.php index 4c70cc43e6..ed44e8a367 100644 --- a/classes/phing/tasks/system/ApplyTask.php +++ b/classes/phing/tasks/system/ApplyTask.php @@ -541,6 +541,8 @@ protected function buildCommand() } } + $this->realCommand = rtrim($this->realCommand); + // Log $this->log('Command built : ' . $this->realCommand, $this->loglevel); From eb8a922b90ac34f9736c509fc4e11c6eda2328b9 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 16:51:53 +0100 Subject: [PATCH 07/12] Added additional stuff --- classes/phing/tasks/system/ExecTask.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/phing/tasks/system/ExecTask.php b/classes/phing/tasks/system/ExecTask.php index 2d6becce60..c66a989e93 100644 --- a/classes/phing/tasks/system/ExecTask.php +++ b/classes/phing/tasks/system/ExecTask.php @@ -253,7 +253,7 @@ protected function buildCommand() */ protected function executeCommand() { - $cmdl = (string) $this->commandline; // . $this->realCommand; + $cmdl = (string) $this->commandline . $this->realCommand; $this->log('Executing command: ' . $cmdl, $this->logLevel); From 164ce97438842749da4437d4a2e242a0e41e3bd4 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 17:32:14 +0100 Subject: [PATCH 08/12] Fixed executCommand --- classes/phing/tasks/system/ApplyTask.php | 24 +++++++++++ classes/phing/util/DirectoryScanner.php | 53 ++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/classes/phing/tasks/system/ApplyTask.php b/classes/phing/tasks/system/ApplyTask.php index ed44e8a367..cce622e843 100644 --- a/classes/phing/tasks/system/ApplyTask.php +++ b/classes/phing/tasks/system/ApplyTask.php @@ -682,6 +682,30 @@ private function process($srcFiles, $basedir) } } + /** + * Executes the command and returns return code and output. + * + * @return array array(return code, array with output) + * @throws \BuildException + */ + protected function executeCommand() + { + $cmdl = $this->realCommand; + + $this->log('Executing command: ' . $cmdl, $this->logLevel); + + $output = []; + $return = null; + + if ($this->passthru) { + passthru($cmdl, $return); + } else { + exec($cmdl, $output, $return); + } + + return [$return, $output]; + } + /** * Runs cleanup tasks post execution * - Restore working directory diff --git a/classes/phing/util/DirectoryScanner.php b/classes/phing/util/DirectoryScanner.php index 5a525aff3d..c9df6a7425 100644 --- a/classes/phing/util/DirectoryScanner.php +++ b/classes/phing/util/DirectoryScanner.php @@ -714,15 +714,37 @@ protected function isExcluded($_name) return false; } + /** + * Return the count of included files. + * + * @return int + * + * @throws UnexpectedValueException + */ + public function getIncludedFilesCount(): int + { + if ($this->filesIncluded === null) { + throw new UnexpectedValueException('Must call scan() first'); + } + return count($this->filesIncluded); + } + /** * Get the names of the files that matched at least one of the include * patterns, and matched none of the exclude patterns. * The names are relative to the basedir. * * @return array names of the files + * @throws \UnexpectedValueException */ - public function getIncludedFiles() + public function getIncludedFiles(): array { + if ($this->filesIncluded === null) { + throw new UnexpectedValueException('Must call scan() first'); + } + + sort($this->filesIncluded); + return $this->filesIncluded; } @@ -778,13 +800,35 @@ public function getDeselectedFiles() * The names are relative to the basedir. * * @return array the names of the directories + * @throws \UnexpectedValueException */ public function getIncludedDirectories() { + if ($this->dirsIncluded === null) { + throw new UnexpectedValueException('Must call scan() first'); + } + + sort($this->dirsIncluded); + return $this->dirsIncluded; } + /** + * Return the count of included directories. + * + * @return int + * + * @throws UnexpectedValueException + */ + public function getIncludedDirectoriesCount(): int + { + if ($this->dirsIncluded === null) { + throw new UnexpectedValueException('Must call scan() first'); + } + return count($this->dirsIncluded); + } + /** * Get the names of the directories that matched at none of the include * patterns. @@ -869,10 +913,13 @@ public function isEverythingIncluded() /** * Tests whether a name should be selected. * - * @param string $name The filename to check for selecting. - * @param string $file The full file path. + * @param string $name The filename to check for selecting. + * @param string $file The full file path. * @return boolean False when the selectors says that the file * should not be selected, True otherwise. + * @throws \BuildException + * @throws \IOException + * @throws NullPointerException */ protected function isSelected($name, $file) { From 11a88f0941bcd4dedac8218e3c3e1812a444cca4 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 17:43:53 +0100 Subject: [PATCH 09/12] Fixed test --- test/classes/phing/tasks/system/ApplyTaskTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/classes/phing/tasks/system/ApplyTaskTest.php b/test/classes/phing/tasks/system/ApplyTaskTest.php index f675610750..34dda0f785 100644 --- a/test/classes/phing/tasks/system/ApplyTaskTest.php +++ b/test/classes/phing/tasks/system/ApplyTaskTest.php @@ -421,7 +421,7 @@ public function testSourceFilename() $this->executeTarget(__FUNCTION__); // As the addsourcefilename is 'off', only the executable should be processed in the execution - $this->assertInLogs(': ls :'); + $this->assertInLogs('Executing command: ls'); } /** From d899e57e884e43c1c807d5981e5d5acad6baa087 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 17:59:33 +0100 Subject: [PATCH 10/12] Some minor fixes --- classes/phing/types/Commandline.php | 4 +--- test/classes/phing/tasks/system/ApplyTaskTest.php | 4 ++-- test/classes/phing/tasks/system/ExecTaskTest.php | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/classes/phing/types/Commandline.php b/classes/phing/types/Commandline.php index 21d8668558..d07276910e 100644 --- a/classes/phing/types/Commandline.php +++ b/classes/phing/types/Commandline.php @@ -236,9 +236,7 @@ private function toString($lines = null): string return ''; } - $cmd = array_shift($lines); - - return $cmd . ' ' . implode(' ', array_map(function ($arg) {return self::quoteArgument($arg, $this->escape);}, $lines)); + return implode(' ', array_map(function ($arg) {return self::quoteArgument($arg, $this->escape);}, $lines)); } /** diff --git a/test/classes/phing/tasks/system/ApplyTaskTest.php b/test/classes/phing/tasks/system/ApplyTaskTest.php index 34dda0f785..c8ed99ba99 100644 --- a/test/classes/phing/tasks/system/ApplyTaskTest.php +++ b/test/classes/phing/tasks/system/ApplyTaskTest.php @@ -391,7 +391,7 @@ public function testMissingExecutable() public function testEscapedArg() { $this->executeTarget(__FUNCTION__); - $this->assertPropertyEquals('outval', $this->windows ? '"abc$b3 SB"' : 'abc$b3!SB'); + $this->assertPropertyEquals('outval', $this->windows ? 'abc$b3 SB' : 'abc$b3!SB'); } /** @@ -440,7 +440,7 @@ public function testOutputAppend() // Validating the output $output = @file_get_contents($tempfile); @unlink($tempfile); - $this->assertEquals("Append OK\nAppend OK", rtrim($output)); + $this->assertEquals($this->windows ? "\"Append OK\" \r\n\"Append OK\"" : "Append OK\nAppend OK", rtrim($output)); } /** diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index a85c023cf4..c55e98a9a7 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -356,14 +356,14 @@ public function testMissingExecutableAndCommand() public function testEscapedArg() { $this->executeTarget(__FUNCTION__); - $this->assertPropertyEquals('outval', $this->windows ? '"abc$b3 SB"' : 'abc$b3!SB'); + $this->assertPropertyEquals('outval', $this->windows ? 'abc$b3 SB' : 'abc$b3!SB'); } public function testEscapedArgWithoutWhitespace() { $arg = 'foo|bar'; $this->executeTarget(__FUNCTION__); - $this->assertInLogs($this->windows ? 'echo "foo|bar" 2>&1' : 'echo \'foo|bar\' 2>&1'); + $this->assertInLogs($this->windows ? '"echo" "foo|bar" 2>&1' : 'echo \'foo|bar\' 2>&1'); $this->assertNotInLogs($this->windows ? 'echo " foo|bar " 2>&1' : 'echo \' foo|bar \' 2>&1'); } } From 6a9d6d57307c955befa3cc092736a976df53c2d2 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sat, 17 Feb 2018 18:04:31 +0100 Subject: [PATCH 11/12] Fixed last on linux --- test/classes/phing/tasks/system/ExecTaskTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/classes/phing/tasks/system/ExecTaskTest.php b/test/classes/phing/tasks/system/ExecTaskTest.php index c55e98a9a7..316900928a 100644 --- a/test/classes/phing/tasks/system/ExecTaskTest.php +++ b/test/classes/phing/tasks/system/ExecTaskTest.php @@ -363,7 +363,7 @@ public function testEscapedArgWithoutWhitespace() { $arg = 'foo|bar'; $this->executeTarget(__FUNCTION__); - $this->assertInLogs($this->windows ? '"echo" "foo|bar" 2>&1' : 'echo \'foo|bar\' 2>&1'); + $this->assertInLogs($this->windows ? '"echo" "foo|bar" 2>&1' : '\'echo\' \'foo|bar\' 2>&1'); $this->assertNotInLogs($this->windows ? 'echo " foo|bar " 2>&1' : 'echo \' foo|bar \' 2>&1'); } } From 29aa2d39eb6dd210f4204174cc6e630960c555c1 Mon Sep 17 00:00:00 2001 From: siad007 Date: Sun, 18 Feb 2018 16:18:53 +0100 Subject: [PATCH 12/12] ApplyTask additions --- classes/phing/tasks/system/ApplyTask.php | 50 +++------ classes/phing/tasks/system/ExecTask.php | 8 +- classes/phing/types/Environment.php | 2 +- docs/guide/en/source/appendixes/coretasks.xml | 106 +++++++++++++++++- .../phing/tasks/system/ApplyTaskTest.php | 2 +- 5 files changed, 125 insertions(+), 43 deletions(-) diff --git a/classes/phing/tasks/system/ApplyTask.php b/classes/phing/tasks/system/ApplyTask.php index cce622e843..76af02e51a 100644 --- a/classes/phing/tasks/system/ApplyTask.php +++ b/classes/phing/tasks/system/ApplyTask.php @@ -101,6 +101,7 @@ class ApplyTask extends ExecTask /** @var Mapper $mapperElement */ private $mapperElement; + private $additionalCmds; /** * Set whether empty filesets will be skipped. If true and @@ -510,6 +511,8 @@ protected function buildCommand() // Building the executable $this->realCommand = (string) $this->commandline; + $this->additionalCmds = ''; + // Adding the source filename at the end of command, validating the existing // sourcefile position explicit mentioning if ($this->addsourcefile === true) { @@ -518,16 +521,16 @@ protected function buildCommand() // Setting command output redirection with content appending if ($this->output !== null) { - $this->realCommand .= sprintf(' 1>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->output->getPath())); + $this->additionalCmds .= sprintf(' 1>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->output->getPath())); } elseif ($this->spawn) { // Validating the 'spawn' configuration, and redirecting the output to 'null' - $this->realCommand .= sprintf(' %s', 'WIN' === $this->osvariant ? '> NUL' : '1>/dev/null'); + $this->additionalCmds .= sprintf(' %s', 'WIN' === $this->osvariant ? '> NUL' : '1>/dev/null'); $this->log("For process spawning, setting Output nullification ", $this->loglevel); } // Setting command error redirection with content appending if ($this->error !== null) { - $this->realCommand .= sprintf(' 2>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->error->getPath())); + $this->additionalCmds .= sprintf(' 2>%s %s', $this->appendoutput ? '>' : '', escapeshellarg($this->error->getPath())); } // Setting the execution as a background process @@ -535,16 +538,16 @@ protected function buildCommand() // Validating the O.S. variant if ('WIN' === $this->osvariant) { - $this->realCommand = 'start /b ' . $this->realCommand; // MS Windows background process forking + $this->additionalCmds = 'start /b ' . $this->additionalCmds; // MS Windows background process forking } else { - $this->realCommand .= ' &'; // GNU/Linux background process forking + $this->additionalCmds .= ' &'; // GNU/Linux background process forking } } - $this->realCommand = rtrim($this->realCommand); + $this->additionalCmds = rtrim($this->additionalCmds); // Log - $this->log('Command built : ' . $this->realCommand, $this->loglevel); + $this->log('Command built : ' . $this->realCommand . $this->additionalCmds, $this->loglevel); // Log $this->log('Command building completed ', $this->loglevel); @@ -612,10 +615,9 @@ private function process($srcFiles, $basedir) // srcIndex --> targetIndex $result += array_slice($orig, $srcIndex + count($srcFiles), $targetIndex - $srcIndex, true); - $this->insertTargetFiles($targetFiles, $result, - $targetIndex + count($srcFiles), - $this->targetFilePos->getPrefix(), - $this->targetFilePos->getSuffix()); + $result[] = $orig; + $result[] = $targetFiles; + $result = array_merge(... $result); // targetIndex --> end $result = array_merge(array_slice($orig, $targetIndex + count($srcFiles) + count($targetFiles), @@ -662,6 +664,8 @@ private function process($srcFiles, $basedir) } $this->commandline = new Commandline(implode(' ', $result)); + $this->commandline->setEscape($this->escape); + $this->realCommand = (string) $this->commandline . $this->additionalCmds; [$returncode, $output] = $this->executeCommand(); @@ -682,30 +686,6 @@ private function process($srcFiles, $basedir) } } - /** - * Executes the command and returns return code and output. - * - * @return array array(return code, array with output) - * @throws \BuildException - */ - protected function executeCommand() - { - $cmdl = $this->realCommand; - - $this->log('Executing command: ' . $cmdl, $this->logLevel); - - $output = []; - $return = null; - - if ($this->passthru) { - passthru($cmdl, $return); - } else { - exec($cmdl, $output, $return); - } - - return [$return, $output]; - } - /** * Runs cleanup tasks post execution * - Restore working directory diff --git a/classes/phing/tasks/system/ExecTask.php b/classes/phing/tasks/system/ExecTask.php index c66a989e93..f443f5dd2c 100644 --- a/classes/phing/tasks/system/ExecTask.php +++ b/classes/phing/tasks/system/ExecTask.php @@ -243,6 +243,8 @@ protected function buildCommand() if ($this->spawn) { $this->realCommand .= ' &'; } + + $this->realCommand = (string) $this->commandline . $this->realCommand; } /** @@ -253,7 +255,7 @@ protected function buildCommand() */ protected function executeCommand() { - $cmdl = (string) $this->commandline . $this->realCommand; + $cmdl = $this->realCommand; $this->log('Executing command: ' . $cmdl, $this->logLevel); @@ -625,7 +627,7 @@ protected function isValidOs(): bool * @param bool $resolveExecutable if true, attempt to resolve the * path of the executable. */ - public function setResolveExecutable(boolean $resolveExecutable): void + public function setResolveExecutable($resolveExecutable): void { $this->resolveExecutable = $resolveExecutable; } @@ -636,7 +638,7 @@ public function setResolveExecutable(boolean $resolveExecutable): void * * @param bool $searchPath if true, search PATHs. */ - public function setSearchPath(boolean $searchPath): void + public function setSearchPath($searchPath): void { $this->searchPath = $searchPath; } diff --git a/classes/phing/types/Environment.php b/classes/phing/types/Environment.php index 44eb266586..fff6b02d7c 100644 --- a/classes/phing/types/Environment.php +++ b/classes/phing/types/Environment.php @@ -60,7 +60,7 @@ public function getVariables() if ($this->variables->count() === 0) { return null; } - return $this->variables->getArrayCopy(); + return array_map(function ($env) {return $env->getContent();}, $this->variables->getArrayCopy()); } /** diff --git a/docs/guide/en/source/appendixes/coretasks.xml b/docs/guide/en/source/appendixes/coretasks.xml index fbc6eee052..2633d449b1 100644 --- a/docs/guide/en/source/appendixes/coretasks.xml +++ b/docs/guide/en/source/appendixes/coretasks.xml @@ -496,8 +496,34 @@ No - - + + skipemptyfilesets + Boolean + Don't run the command, if no source files have been found or are newer than their + corresponding target files. Despite its name, this attribute applies to filelists as well. + false + No + + + + type + String + One of file, dir or both. If set to file, only the names of plain files will be sent to + the command. If set to dir, only the names of directories are considered. + Note: The type attribute does not apply to nested dirsets - dirsets always implicitly assume + type to be dir. + file + No + + + + force + Boolean + Whether to bypass timestamp comparisons for target files. + false + No + + @@ -585,7 +611,9 @@ fileset filelist dirset + mapper srcfile + targetfile @@ -1782,7 +1810,8 @@ verbose="true" failonerror="false" /> command String - The command that is to be executed. + NOTE: This attribute is deprecated. Please use executable with nested args. + The command that is to be executed. n/a One of the two @@ -1892,6 +1921,24 @@ verbose="true" failonerror="false" /> verbose No + + resolveexecutable + Boolean + When this attribute is true, the name of the executable is resolved firstly against the + project basedir and if that does not exist, against the execution directory if specified. + On Unix systems, if you only want to allow execution of commands in the user's path, + set this to false. + false + No + + + searchpath + Boolean + When this attribute is true, then system path environment variables will be searched when + resolving the location of the executable. + false + No + @@ -1972,6 +2019,59 @@ verbose="true" failonerror="false" /> + env + It is possible to specify environment variables to pass to the system command via nested + <env> elements. + + Attributes + + + + + + + + + Name + Type + Description + Default + Required + + + + + key + String + The name of the environment variable. + n/a + Yes + + + value + String + The literal value for the environment variable. + n/a + One of these + + + file + String + The value for the environment variable. Will be replaced by the absolute + filename of the file by Phing. + n/a + + + path + String + The value for a PATH like environment variable. You can use ; or : as + path separators and Phing will convert it to the platform's local conventions. + n/a + + + +
    +
    diff --git a/test/classes/phing/tasks/system/ApplyTaskTest.php b/test/classes/phing/tasks/system/ApplyTaskTest.php index c8ed99ba99..f63a5dfc3e 100644 --- a/test/classes/phing/tasks/system/ApplyTaskTest.php +++ b/test/classes/phing/tasks/system/ApplyTaskTest.php @@ -440,7 +440,7 @@ public function testOutputAppend() // Validating the output $output = @file_get_contents($tempfile); @unlink($tempfile); - $this->assertEquals($this->windows ? "\"Append OK\" \r\n\"Append OK\"" : "Append OK\nAppend OK", rtrim($output)); + $this->assertEquals($this->windows ? "Append OK \r\nAppend OK" : "Append OK\nAppend OK", rtrim($output)); } /**