1<?php
2
3/**
4 *  $Id: f519e4e90fdc87bd40ee563645b5def3e3ff0c0a $
5 *
6 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
7 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
8 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
10 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
12 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
13 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
14 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
15 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
16 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
17 *
18 * This software consists of voluntary contributions made by many individuals
19 * and is licensed under the LGPL. For more information please see
20 * <http://phing.info>.
21 */
22
23require_once 'phing/Task.php';
24
25/**
26 * Executes a command on the shell.
27 *
28 * @author  Andreas Aderhold <andi@binarycloud.com>
29 * @author  Hans Lellelid <hans@xmpl.org>
30 * @author  Christian Weiske <cweiske@cweiske.de>
31 * @version $Id: f519e4e90fdc87bd40ee563645b5def3e3ff0c0a $
32 * @package phing.tasks.system
33 */
34class ExecTask extends Task
35{
36
37    /**
38     * Command to be executed
39     * @var string
40     */
41    protected $realCommand;
42
43    /**
44     * Given command
45     * @var string
46     */
47    protected $command;
48
49    /**
50     * Commandline managing object
51     *
52     * @var Commandline
53     */
54    protected $commandline;
55
56    /**
57     * Working directory.
58     * @var PhingFile
59     */
60    protected $dir;
61
62    /**
63     * Operating system.
64     * @var string
65     */
66    protected $os;
67
68    /**
69     * Whether to escape shell command using escapeshellcmd().
70     * @var boolean
71     */
72    protected $escape = false;
73
74    /**
75     * Where to direct output.
76     * @var File
77     */
78    protected $output;
79
80    /**
81     * Whether to use PHP's passthru() function instead of exec()
82     * @var boolean
83     */
84    protected $passthru = false;
85
86    /**
87     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
88     * @var boolean
89     */
90    protected $logOutput = false;
91
92    /**
93     * Logging level for status messages
94     * @var integer
95     */
96    protected $logLevel = Project::MSG_VERBOSE;
97
98    /**
99     * Where to direct error output.
100     * @var File
101     */
102    protected $error;
103
104    /**
105     * If spawn is set then [unix] programs will redirect stdout and add '&'.
106     * @var boolean
107     */
108    protected $spawn = false;
109
110    /**
111     * Property name to set with return value from exec call.
112     *
113     * @var string
114     */
115    protected $returnProperty;
116
117    /**
118     * Property name to set with output value from exec call.
119     *
120     * @var string
121     */
122    protected $outputProperty;
123
124    /**
125     * Whether to check the return code.
126     * @var boolean
127     */
128    protected $checkreturn = false;
129
130
131
132    public function __construct()
133    {
134        $this->commandline = new Commandline();
135    }
136
137    /**
138     * Main method: wraps execute() command.
139     *
140     * @return void
141     */
142    public function main()
143    {
144        if (!$this->isApplicable()) {
145            return;
146        }
147
148        $this->prepare();
149        $this->buildCommand();
150        list($return, $output) = $this->executeCommand();
151        $this->cleanup($return, $output);
152    }
153
154    /**
155     * Checks whether the command shall be executed
156     *
157     * @return boolean False if the exec command shall not be run
158     */
159    protected function isApplicable()
160    {
161        if ($this->os === null) {
162            return true;
163        }
164
165        $myos = Phing::getProperty('os.name');
166        $this->log('Myos = ' . $myos, Project::MSG_VERBOSE);
167
168        if (strpos($this->os, $myos) !== false) {
169            // this command will be executed only on the specified OS
170            // OS matches
171            return true;
172        }
173
174        $this->log(
175            sprintf(
176                'Operating system %s not found in %s',
177                $myos, $this->os
178            ),
179            Project::MSG_VERBOSE
180        );
181        return false;
182    }
183
184    /**
185     * Prepares the command building and execution, i.e.
186     * changes to the specified directory.
187     *
188     * @return void
189     */
190    protected function prepare()
191    {
192        if ($this->dir === null) {
193            return;
194        }
195
196        // expand any symbolic links first
197        if (!$this->dir->getCanonicalFile()->isDirectory()) {
198            throw new BuildException(
199                "'" . (string) $this->dir . "' is not a valid directory"
200            );
201        }
202        $this->currdir = getcwd();
203        @chdir($this->dir->getPath());
204    }
205
206    /**
207     * Builds the full command to execute and stores it in $command.
208     *
209     * @return void
210     * @uses   $command
211     */
212    protected function buildCommand()
213    {
214        if ($this->command === null && $this->commandline->getExecutable() === null) {
215            throw new BuildException(
216                'ExecTask: Please provide "command" OR "executable"'
217            );
218        } else if ($this->command === null) {
219            $this->realCommand = Commandline::toString($this->commandline->getCommandline(), $this->escape);
220        } else if ($this->commandline->getExecutable() === null) {
221            $this->realCommand = $this->command;
222
223            //we need to escape the command only if it's specified directly
224            // commandline takes care of "executable" already
225            if ($this->escape == true) {
226                $this->realCommand = escapeshellcmd($this->realCommand);
227            }
228        } else {
229            throw new BuildException(
230                'ExecTask: Either use "command" OR "executable"'
231            );
232        }
233
234        if ($this->error !== null) {
235            $this->realCommand .= ' 2> ' . escapeshellarg($this->error->getPath());
236            $this->log(
237                "Writing error output to: " . $this->error->getPath(),
238                $this->logLevel
239            );
240        }
241
242        if ($this->output !== null) {
243            $this->realCommand .= ' 1> ' . escapeshellarg($this->output->getPath());
244            $this->log(
245                "Writing standard output to: " . $this->output->getPath(),
246                $this->logLevel
247            );
248        } elseif ($this->spawn) {
249            $this->realCommand .= ' 1>/dev/null';
250            $this->log("Sending output to /dev/null", $this->logLevel);
251        }
252
253        // If neither output nor error are being written to file
254        // then we'll redirect error to stdout so that we can dump
255        // it to screen below.
256
257        if ($this->output === null && $this->error === null && $this->passthru === false) {
258            $this->realCommand .= ' 2>&1';
259        }
260
261        // we ignore the spawn boolean for windows
262        if ($this->spawn) {
263            $this->realCommand .= ' &';
264        }
265    }
266
267    /**
268     * Executes the command and returns return code and output.
269     *
270     * @return array array(return code, array with output)
271     */
272    protected function executeCommand()
273    {
274        $this->log("Executing command: " . $this->realCommand, $this->logLevel);
275
276        $output = array();
277        $return = null;
278
279        if ($this->passthru) {
280            passthru($this->realCommand, $return);
281        } else {
282            exec($this->realCommand, $output, $return);
283        }
284
285        return array($return, $output);
286    }
287
288    /**
289     * Runs all tasks after command execution:
290     * - change working directory back
291     * - log output
292     * - verify return value
293     *
294     * @param integer $return Return code
295     * @param array   $output Array with command output
296     *
297     * @return void
298     */
299    protected function cleanup($return, $output)
300    {
301        if ($this->dir !== null) {
302            @chdir($this->currdir);
303        }
304
305        $outloglevel = $this->logOutput ? Project::MSG_INFO : Project::MSG_VERBOSE;
306        foreach ($output as $line) {
307            $this->log($line, $outloglevel);
308        }
309
310        if ($this->returnProperty) {
311            $this->project->setProperty($this->returnProperty, $return);
312        }
313
314        if ($this->outputProperty) {
315            $this->project->setProperty(
316                $this->outputProperty, implode("\n", $output)
317            );
318        }
319
320        if ($return != 0 && $this->checkreturn) {
321            throw new BuildException("Task exited with code $return");
322        }
323    }
324
325
326    /**
327     * The command to use.
328     *
329     * @param mixed $command String or string-compatible (e.g. w/ __toString()).
330     *
331     * @return void
332     */
333    public function setCommand($command)
334    {
335        $this->command = "" . $command;
336    }
337
338    /**
339     * The executable to use.
340     *
341     * @param mixed $executable String or string-compatible (e.g. w/ __toString()).
342     *
343     * @return void
344     */
345    public function setExecutable($executable)
346    {
347        $this->commandline->setExecutable((string)$executable);
348    }
349
350    /**
351     * Whether to use escapeshellcmd() to escape command.
352     *
353     * @param boolean $escape If the command shall be escaped or not
354     *
355     * @return void
356     */
357    public function setEscape($escape)
358    {
359        $this->escape = (bool) $escape;
360    }
361
362    /**
363     * Specify the working directory for executing this command.
364     *
365     * @param PhingFile $dir Working directory
366     *
367     * @return void
368     */
369    public function setDir(PhingFile $dir)
370    {
371        $this->dir = $dir;
372    }
373
374    /**
375     * Specify OS (or muliple OS) that must match in order to execute this command.
376     *
377     * @param string $os Operating system string (e.g. "Linux")
378     *
379     * @return void
380     */
381    public function setOs($os)
382    {
383        $this->os = (string) $os;
384    }
385
386    /**
387     * File to which output should be written.
388     *
389     * @param PhingFile $f Output log file
390     *
391     * @return void
392     */
393    public function setOutput(PhingFile $f)
394    {
395        $this->output = $f;
396    }
397
398    /**
399     * File to which error output should be written.
400     *
401     * @param PhingFile $f Error log file
402     *
403     * @return void
404     */
405    public function setError(PhingFile $f)
406    {
407        $this->error = $f;
408    }
409
410    /**
411     * Whether to use PHP's passthru() function instead of exec()
412     *
413     * @param boolean $passthru If passthru shall be used
414     *
415     * @return void
416     */
417    public function setPassthru($passthru)
418    {
419        $this->passthru = (bool) $passthru;
420    }
421
422    /**
423     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
424     *
425     * @param boolean $logOutput If output shall be logged visibly
426     *
427     * @return void
428     */
429    public function setLogoutput($logOutput)
430    {
431        $this->logOutput = (bool) $logOutput;
432    }
433
434    /**
435     * Whether to suppress all output and run in the background.
436     *
437     * @param boolean $spawn If the command is to be run in the background
438     *
439     * @return void
440     */
441    public function setSpawn($spawn)
442    {
443        $this->spawn  = (bool) $spawn;
444    }
445
446    /**
447     * Whether to check the return code.
448     *
449     * @param boolean $checkreturn If the return code shall be checked
450     *
451     * @return void
452     */
453    public function setCheckreturn($checkreturn)
454    {
455        $this->checkreturn = (bool) $checkreturn;
456    }
457
458    /**
459     * The name of property to set to return value from exec() call.
460     *
461     * @param string $prop Property name
462     *
463     * @return void
464     */
465    public function setReturnProperty($prop)
466    {
467        $this->returnProperty = $prop;
468    }
469
470    /**
471     * The name of property to set to output value from exec() call.
472     *
473     * @param string $prop Property name
474     *
475     * @return void
476     */
477    public function setOutputProperty($prop)
478    {
479        $this->outputProperty = $prop;
480    }
481
482    /**
483     * Set level of log messages generated (default = verbose)
484     *
485     * @param string $level Log level
486     *
487     * @return void
488     */
489    public function setLevel($level)
490    {
491        switch ($level) {
492        case 'error':
493            $this->logLevel = Project::MSG_ERR;
494            break;
495        case 'warning':
496            $this->logLevel = Project::MSG_WARN;
497            break;
498        case 'info':
499            $this->logLevel = Project::MSG_INFO;
500            break;
501        case 'verbose':
502            $this->logLevel = Project::MSG_VERBOSE;
503            break;
504        case 'debug':
505            $this->logLevel = Project::MSG_DEBUG;
506            break;
507        default:
508            throw new BuildException(
509                sprintf('Unknown log level "%s"', $level)
510            );
511        }
512    }
513
514    /**
515     * Creates a nested <arg> tag.
516     *
517     * @return CommandlineArgument Argument object
518     */
519    public function createArg()
520    {
521        return $this->commandline->createArgument();
522    }
523}
524
525