1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19

20
/**
21
 * Executes a command on the shell.
22
 *
23
 * @author  Andreas Aderhold <andi@binarycloud.com>
24
 * @author  Hans Lellelid <hans@xmpl.org>
25
 * @author  Christian Weiske <cweiske@cweiske.de>
26
 * @package phing.tasks.system
27
 */
28
class ExecTask extends Task
29
{
30
    use LogLevelAware;
31

32
    public const INVALID = PHP_INT_MAX;
33

34
    private $exitValue = self::INVALID;
35

36
    /**
37
     * Command to be executed
38
     *
39
     * @var string
40
     */
41
    protected $realCommand;
42

43
    /**
44
     * Commandline managing object
45
     *
46
     * @var Commandline
47
     */
48
    protected $commandline;
49

50
    /**
51
     * Working directory.
52
     *
53
     * @var PhingFile
54
     */
55
    protected $dir;
56

57
    protected $currdir;
58

59
    /**
60
     * Operating system.
61
     *
62
     * @var string
63
     */
64
    protected $os;
65

66
    /**
67
     * Whether to escape shell command using escapeshellcmd().
68
     *
69
     * @var boolean
70
     */
71
    protected $escape = false;
72

73
    /**
74
     * Where to direct output.
75
     *
76
     * @var PhingFile
77
     */
78
    protected $output;
79

80
    /**
81
     * Whether to use PHP's passthru() function instead of exec()
82
     *
83
     * @var boolean
84
     */
85
    protected $passthru = false;
86

87
    /**
88
     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
89
     *
90
     * @var boolean
91
     */
92
    protected $logOutput = false;
93

94
    /**
95
     * Where to direct error output.
96
     *
97
     * @var PhingFile
98
     */
99
    protected $error;
100

101
    /**
102
     * If spawn is set then [unix] programs will redirect stdout and add '&'.
103
     *
104
     * @var boolean
105
     */
106
    protected $spawn = false;
107

108
    /**
109
     * Property name to set with return value from exec call.
110
     *
111
     * @var string
112
     */
113
    protected $returnProperty;
114

115
    /**
116
     * Property name to set with output value from exec call.
117
     *
118
     * @var string
119
     */
120
    protected $outputProperty;
121

122
    /**
123
     * Whether to check the return code.
124
     *
125
     * @var boolean
126
     */
127
    protected $checkreturn = false;
128

129
    private $osFamily;
130
    private $executable;
131
    private $resolveExecutable = false;
132
    private $searchPath = false;
133
    private $env;
134

135
    /**
136
     * @throws \BuildException
137
     */
138 1
    public function __construct()
139
    {
140 1
        parent::__construct();
141 1
        $this->commandline = new Commandline();
142 1
        $this->env = new Environment();
143
    }
144

145
    /**
146
     * Main method: wraps execute() command.
147
     *
148
     * @throws \BuildException
149
     */
150 1
    public function main()
151
    {
152 1
        if (!$this->isValidOs()) {
153 1
            return null;
154
        }
155

156
        try {
157 1
            $this->commandline->setExecutable($this->resolveExecutable($this->executable, $this->searchPath));
158 0
        } catch (IOException | NullPointerException $e) {
159 0
            throw new BuildException($e);
160
        }
161

162 1
        $this->prepare();
163 1
        $this->buildCommand();
164 1
        [$return, $output] = $this->executeCommand();
165 1
        $this->cleanup($return, $output);
166

167 1
        return $return;
168
    }
169

170
    /**
171
     * Prepares the command building and execution, i.e.
172
     * changes to the specified directory.
173
     *
174
     * @throws BuildException
175
     * @return void
176
     */
177 1
    protected function prepare()
178
    {
179 1
        if ($this->dir === null) {
180 1
            $this->dir = $this->getProject()->getBasedir();
181
        }
182

183 1
        if ($this->commandline->getExecutable() === null) {
184 1
            throw new BuildException(
185 1
                'ExecTask: Please provide "executable"'
186
            );
187
        }
188

189
        // expand any symbolic links first
190
        try {
191 1
            if (!$this->dir->getCanonicalFile()->exists()) {
192 1
                throw new BuildException(
193 1
                    "The directory '" . (string) $this->dir . "' does not exist"
194
                );
195
            }
196 1
            if (!$this->dir->getCanonicalFile()->isDirectory()) {
197 0
                throw new BuildException(
198 0
                    "'" . (string) $this->dir . "' is not a directory"
199
                );
200
            }
201 1
        } catch (IOException $e) {
202 0
            throw new BuildException(
203 0
                "'" . (string) $this->dir . "' is not a readable directory"
204
            );
205
        }
206 1
        $this->currdir = getcwd();
207 1
        @chdir($this->dir->getPath());
208

209 1
        $this->commandline->setEscape($this->escape);
210
    }
211

212
    /**
213
     * @param int $exitValue
214
     * @return bool
215
     */
216 1
    public function isFailure($exitValue = null)
217
    {
218 1
        if ($exitValue === null) {
219 0
            $exitValue = $this->getExitValue();
220
        }
221

222 1
        return $exitValue !== 0;
223
    }
224

225
    /**
226
     * Builds the full command to execute and stores it in $command.
227
     *
228
     * @throws BuildException
229
     * @return void
230
     * @uses   $command
231
     */
232 1
    protected function buildCommand()
233
    {
234 1
        if ($this->error !== null) {
235 1
            $this->realCommand .= ' 2> ' . escapeshellarg($this->error->getPath());
236 1
            $this->log(
237 1
                'Writing error output to: ' . $this->error->getPath(),
238 1
                $this->logLevel
239
            );
240
        }
241

242 1
        if ($this->output !== null) {
243 1
            $this->realCommand .= ' 1> ' . escapeshellarg($this->output->getPath());
244 1
            $this->log(
245 1
                'Writing standard output to: ' . $this->output->getPath(),
246 1
                $this->logLevel
247
            );
248 1
        } elseif ($this->spawn) {
249 1
            $this->realCommand .= ' 1>/dev/null';
250 1
            $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 1
        if ($this->output === null && $this->error === null && $this->passthru === false) {
258 1
            $this->realCommand .= ' 2>&1';
259
        }
260

261
        // we ignore the spawn boolean for windows
262 1
        if ($this->spawn) {
263 1
            $this->realCommand .= ' &';
264
        }
265

266 1
        $envString = '';
267 1
        $environment = $this->env->getVariables();
268 1
        if ($environment !== null) {
269 1
            foreach ($environment as $variable) {
270 1
                if ($this->isPath($variable)) {
271 1
                    continue;
272
                }
273 1
                $this->log('Setting environment variable: ' . $variable, Project::MSG_VERBOSE);
274 1
                $envString .= $variable . '; ';
275
            }
276
        }
277

278 1
        $this->realCommand = $envString . $this->commandline . $this->realCommand;
279
    }
280

281
    /**
282
     * Executes the command and returns return code and output.
283
     *
284
     * @return array array(return code, array with output)
285
     * @throws \BuildException
286
     */
287 1
    protected function executeCommand()
288
    {
289 1
        $cmdl = $this->realCommand;
290

291 1
        $this->log('Executing command: ' . $cmdl, $this->logLevel);
292

293 1
        $output = [];
294 1
        $return = null;
295

296 1
        if ($this->passthru) {
297 1
            passthru($cmdl, $return);
298
        } else {
299 1
            exec($cmdl, $output, $return);
300
        }
301

302 1
        return [$return, $output];
303
    }
304

305
    /**
306
     * Runs all tasks after command execution:
307
     * - change working directory back
308
     * - log output
309
     * - verify return value
310
     *
311
     * @param integer $return Return code
312
     * @param array $output Array with command output
313
     *
314
     * @throws BuildException
315
     * @return void
316
     */
317 1
    protected function cleanup($return, $output): void
318
    {
319 1
        if ($this->dir !== null) {
320 1
            @chdir($this->currdir);
321
        }
322

323 1
        $outloglevel = $this->logOutput ? Project::MSG_INFO : Project::MSG_VERBOSE;
324 1
        foreach ($output as $line) {
325 1
            $this->log($line, $outloglevel);
326
        }
327

328 1
        $this->maybeSetReturnPropertyValue($return);
329

330 1
        if ($this->outputProperty) {
331 1
            $this->project->setProperty(
332 1
                $this->outputProperty,
333 1
                implode("\n", $output)
334
            );
335
        }
336

337 1
        $this->setExitValue($return);
338

339 1
        if ($return !== 0) {
340 1
            if ($this->checkreturn) {
341 1
                throw new BuildException($this->getTaskType() . ' returned: ' . $return, $this->getLocation());
342
            }
343 1
            $this->log('Result: ' . $return, Project::MSG_ERR);
344
        }
345
    }
346

347
    /**
348
     * Set the exit value.
349
     *
350
     * @param int $value exit value of the process.
351
     */
352 1
    protected function setExitValue($value): void
353
    {
354 1
        $this->exitValue = $value;
355
    }
356

357
    /**
358
     * Query the exit value of the process.
359
     *
360
     * @return int the exit value or self::INVALID if no exit value has
361
     *             been received.
362
     */
363 0
    public function getExitValue(): int
364
    {
365 0
        return $this->exitValue;
366
    }
367

368
    /**
369
     * The command to use.
370
     *
371
     * @param string $command String or string-compatible (e.g. w/ __toString()).
372
     *
373
     * @return void
374
     * @throws \BuildException
375
     */
376 1
    public function setCommand($command): void
377
    {
378 1
        $this->log(
379 1
            "The command attribute is deprecated.\nPlease use the executable attribute and nested arg elements.",
380 1
            Project::MSG_WARN
381
        );
382 1
        $this->commandline = new Commandline($command);
383 1
        $this->executable = $this->commandline->getExecutable();
384
    }
385

386
    /**
387
     * The executable to use.
388
     *
389
     * @param string|bool $value String or string-compatible (e.g. w/ __toString()).
390
     *
391
     * @return void
392
     */
393 1
    public function setExecutable($value): void
394
    {
395 1
        if (is_bool($value)) {
396 1
            $value = $value === true ? 'true' : 'false';
397
        }
398 1
        $this->executable = $value;
399 1
        $this->commandline->setExecutable($value);
400
    }
401

402
    /**
403
     * Whether to use escapeshellcmd() to escape command.
404
     *
405
     * @param boolean $escape If the command shall be escaped or not
406
     *
407
     * @return void
408
     */
409 1
    public function setEscape(bool $escape): void
410
    {
411 1
        $this->escape = $escape;
412
    }
413

414
    /**
415
     * Specify the working directory for executing this command.
416
     *
417
     * @param PhingFile $dir Working directory
418
     *
419
     * @return void
420
     */
421 1
    public function setDir(PhingFile $dir): void
422
    {
423 1
        $this->dir = $dir;
424
    }
425

426
    /**
427
     * Specify OS (or multiple OS) that must match in order to execute this command.
428
     *
429
     * @param string $os Operating system string (e.g. "Linux")
430
     *
431
     * @return void
432
     */
433 1
    public function setOs($os): void
434
    {
435 1
        $this->os = (string) $os;
436
    }
437

438
    /**
439
     * List of operating systems on which the command may be executed.
440
     */
441 0
    public function getOs(): string
442
    {
443 0
        return $this->os;
444
    }
445

446
    /**
447
     * Restrict this execution to a single OS Family
448
     *
449
     * @param string $osFamily the family to restrict to.
450
     */
451 1
    public function setOsFamily($osFamily): void
452
    {
453 1
        $this->osFamily = strtolower($osFamily);
454
    }
455

456
    /**
457
     * Restrict this execution to a single OS Family
458
     */
459 0
    public function getOsFamily()
460
    {
461 0
        return $this->osFamily;
462
    }
463

464
    /**
465
     * File to which output should be written.
466
     *
467
     * @param PhingFile $f Output log file
468
     *
469
     * @return void
470
     */
471 1
    public function setOutput(PhingFile $f): void
472
    {
473 1
        $this->output = $f;
474
    }
475

476
    /**
477
     * File to which error output should be written.
478
     *
479
     * @param PhingFile $f Error log file
480
     *
481
     * @return void
482
     */
483 1
    public function setError(PhingFile $f): void
484
    {
485 1
        $this->error = $f;
486
    }
487

488
    /**
489
     * Whether to use PHP's passthru() function instead of exec()
490
     *
491
     * @param boolean $passthru If passthru shall be used
492
     *
493
     * @return void
494
     */
495 1
    public function setPassthru($passthru): void
496
    {
497 1
        $this->passthru = $passthru;
498
    }
499

500
    /**
501
     * Whether to log returned output as MSG_INFO instead of MSG_VERBOSE
502
     *
503
     * @param boolean $logOutput If output shall be logged visibly
504
     *
505
     * @return void
506
     */
507 1
    public function setLogoutput($logOutput): void
508
    {
509 1
        $this->logOutput = $logOutput;
510
    }
511

512
    /**
513
     * Whether to suppress all output and run in the background.
514
     *
515
     * @param boolean $spawn If the command is to be run in the background
516
     *
517
     * @return void
518
     */
519 1
    public function setSpawn($spawn): void
520
    {
521 1
        $this->spawn = $spawn;
522
    }
523

524
    /**
525
     * Whether to check the return code.
526
     *
527
     * @param boolean $checkreturn If the return code shall be checked
528
     *
529
     * @return void
530
     */
531 1
    public function setCheckreturn($checkreturn): void
532
    {
533 1
        $this->checkreturn = $checkreturn;
534
    }
535

536
    /**
537
     * The name of property to set to return value from exec() call.
538
     *
539
     * @param string $prop Property name
540
     *
541
     * @return void
542
     */
543 1
    public function setReturnProperty($prop): void
544
    {
545 1
        $this->returnProperty = $prop;
546
    }
547

548 1
    protected function maybeSetReturnPropertyValue(int $return)
549
    {
550 1
        if ($this->returnProperty) {
551 1
            $this->getProject()->setNewProperty($this->returnProperty, $return);
552
        }
553
    }
554

555
    /**
556
     * The name of property to set to output value from exec() call.
557
     *
558
     * @param string $prop Property name
559
     *
560
     * @return void
561
     */
562 1
    public function setOutputProperty($prop): void
563
    {
564 1
        $this->outputProperty = $prop;
565
    }
566

567
    /**
568
     * Add an environment variable to the launched process.
569
     *
570
     * @param EnvVariable $var new environment variable.
571
     */
572 1
    public function addEnv(EnvVariable $var)
573
    {
574 1
        $this->env->addVariable($var);
575
    }
576

577
    /**
578
     * Creates a nested <arg> tag.
579
     *
580
     * @return CommandlineArgument Argument object
581
     */
582 1
    public function createArg()
583
    {
584 1
        return $this->commandline->createArgument();
585
    }
586

587
    /**
588
     * Is this the OS the user wanted?
589
     *
590
     * @return boolean.
591
     * <ul>
592
     * <li>
593
     * <li><code>true</code> if the os and osfamily attributes are null.</li>
594
     * <li><code>true</code> if osfamily is set, and the os family and must match
595
     * that of the current OS, according to the logic of
596
     * {@link Os#isOs(String, String, String, String)}, and the result of the
597
     * <code>os</code> attribute must also evaluate true.
598
     * </li>
599
     * <li>
600
     * <code>true</code> if os is set, and the system.property os.name
601
     * is found in the os attribute,</li>
602
     * <li><code>false</code> otherwise.</li>
603
     * </ul>
604
     */
605 1
    protected function isValidOs(): bool
606
    {
607
        //hand osfamily off to OsCondition class, if set
608 1
        if ($this->osFamily !== null && !OsCondition::isFamily($this->osFamily)) {
609 0
            return false;
610
        }
611
        //the Exec OS check is different from Os.isOs(), which
612
        //probes for a specific OS. Instead it searches the os field
613
        //for the current os.name
614 1
        $myos = Phing::getProperty("os.name");
615 1
        $this->log("Current OS is " . $myos, Project::MSG_VERBOSE);
616 1
        if (($this->os !== null) && (strpos($this->os, $myos) === false)) {
617
            // this command will be executed only on the specified OS
618 1
            $this->log(
619 1
                "This OS, " . $myos
620 1
                . " was not found in the specified list of valid OSes: " . $this->os,
621 1
                Project::MSG_VERBOSE
622
            );
623 1
            return false;
624
        }
625 1
        return true;
626
    }
627

628
    /**
629
     * Set whether to attempt to resolve the executable to a file.
630
     *
631
     * @param bool $resolveExecutable if true, attempt to resolve the
632
     * path of the executable.
633
     */
634 1
    public function setResolveExecutable($resolveExecutable): void
635
    {
636 1
        $this->resolveExecutable = $resolveExecutable;
637
    }
638

639
    /**
640
     * Set whether to search nested, then
641
     * system PATH environment variables for the executable.
642
     *
643
     * @param bool $searchPath if true, search PATHs.
644
     */
645 1
    public function setSearchPath($searchPath): void
646
    {
647 1
        $this->searchPath = $searchPath;
648
    }
649

650
    /**
651
     * Indicates whether to attempt to resolve the executable to a
652
     * file.
653
     *
654
     * @return bool the resolveExecutable flag
655
     */
656 0
    public function getResolveExecutable(): bool
657
    {
658 0
        return $this->resolveExecutable;
659
    }
660

661
    /**
662
     * The method attempts to figure out where the executable is so that we can feed
663
     * the full path. We first try basedir, then the exec dir, and then
664
     * fallback to the straight executable name (i.e. on the path).
665
     *
666
     * @param string $exec the name of the executable.
667
     * @param bool $mustSearchPath if true, the executable will be looked up in
668
     *                               the PATH environment and the absolute path
669
     *                               is returned.
670
     *
671
     * @return string the executable as a full path if it can be determined.
672
     * @throws \BuildException
673
     * @throws IOException
674
     * @throws NullPointerException
675
     */
676 1
    protected function resolveExecutable($exec, $mustSearchPath): ?string
677
    {
678 1
        if (!$this->resolveExecutable) {
679 1
            return $exec;
680
        }
681
        // try to find the executable
682 1
        $executableFile = $this->getProject()->resolveFile($exec);
683 1
        if ($executableFile->exists()) {
684 0
            return $executableFile->getAbsolutePath();
685
        }
686
        // now try to resolve against the dir if given
687 1
        if ($this->dir !== null) {
688 0
            $executableFile = (new FileUtils())->resolveFile($this->dir, $exec);
689 0
            if ($executableFile->exists()) {
690 0
                return $executableFile->getAbsolutePath();
691
            }
692
        }
693
        // couldn't find it - must be on path
694 1
        if ($mustSearchPath) {
695 1
            $p = null;
696 1
            $environment = $this->env->getVariables();
697 1
            if ($environment !== null) {
698 1
                foreach ($environment as $env) {
699 1
                    if ($this->isPath($env)) {
700 1
                        $p = new Path($this->getProject(), $this->getPath($env));
701 1
                        break;
702
                    }
703
                }
704
            }
705 1
            if ($p === null) {
706 0
                $p = new Path($this->getProject(), getenv('path'));
707
            }
708 1
            if ($p !== null) {
709 1
                $dirs = $p->listPaths();
710 1
                foreach ($dirs as $dir) {
711 1
                    $executableFile = (new FileUtils())->resolveFile(new PhingFile($dir), $exec);
712 1
                    if ($executableFile->exists()) {
713 1
                        return $executableFile->getAbsolutePath();
714
                    }
715
                }
716
            }
717
        }
718

719 0
        return $exec;
720
    }
721

722 1
    private function isPath($line)
723
    {
724 1
        return StringHelper::startsWith('PATH=', $line) || StringHelper::startsWith('Path=', $line);
725
    }
726

727 1
    private function getPath($value)
728
    {
729 1
        if (is_string($value)) {
730 1
            return StringHelper::substring($value, strlen("PATH="));
731
        }
732

733 0
        if (is_array($value)) {
734 0
            $p = $value['PATH'];
735 0
            return $p ?? $value['Path'];
736
        }
737

738 0
        throw new InvalidArgumentException('$value should be of type array or string.');
739
    }
740
}

Read our documentation on viewing source code .

Loading