Passed
Push — master ( dd99a9...c8547b )
by Michiel
10:09
created

Commandline::toString()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 1
dl 0
loc 14
ccs 9
cts 10
cp 0.9
crap 3.009
rs 10
c 0
b 0
f 0
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
 * Commandline objects help handling command lines specifying processes to
22
 * execute.
23
 *
24
 * The class can be used to define a command line as nested elements or as a
25
 * helper to define a command line by an application.
26
 * <p>
27
 * <code>
28
 * &lt;someelement&gt;<br>
29
 * &nbsp;&nbsp;&lt;acommandline executable="/executable/to/run"&gt;<br>
30
 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument value="argument 1" /&gt;<br>
31
 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument line="argument_1 argument_2 argument_3" /&gt;<br>
32
 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;argument value="argument 4" /&gt;<br>
33
 * &nbsp;&nbsp;&lt;/acommandline&gt;<br>
34
 * &lt;/someelement&gt;<br>
35
 * </code>
36
 * The element <code>someelement</code> must provide a method
37
 * <code>createAcommandline</code> which returns an instance of this class.
38
 *
39
 * @author  [email protected]
40
 * @author  <a href="mailto:[email protected]">Stefan Bodewig</a>
41
 * @package phing.types
42
 */
43
class Commandline implements Countable
44
{
45
    /**
46
     * @var CommandlineArgument[]
47
     */
48
    public $arguments = []; // public so "inner" class can access
49
50
    /**
51
     * Full path (if not on %PATH% env var) to executable program.
52
     *
53
     * @var string
54
     */
55
    public $executable; // public so "inner" class can access
56
57
    public const DISCLAIMER = "The ' characters around the executable and arguments are not part of the command.";
58
    private $escape = false;
59
60
    /**
61
     * @param null $to_process
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $to_process is correct as it would always require null to be passed?
Loading history...
62
     * @throws BuildException
63
     */
64 107
    public function __construct($to_process = null)
65
    {
66 107
        if ($to_process !== null) {
0 ignored issues
show
introduced by
The condition $to_process !== null is always false.
Loading history...
67 53
            $tmp = static::translateCommandline($to_process);
68 53
            if ($tmp) {
69 53
                $this->setExecutable(array_shift($tmp)); // removes first el
70 53
                foreach ($tmp as $arg) { // iterate through remaining elements
71 44
                    $this->createArgument()->setValue($arg);
72
                }
73
            }
74
        }
75 107
    }
76
77
    /**
78
     * Creates an argument object and adds it to our list of args.
79
     *
80
     * <p>Each commandline object has at most one instance of the
81
     * argument class.</p>
82
     *
83
     * @param  boolean $insertAtStart if true, the argument is inserted at the
84
     *                                beginning of the list of args, otherwise
85
     *                                it is appended.
86
     * @return CommandlineArgument
87
     */
88 65
    public function createArgument($insertAtStart = false)
89
    {
90 65
        $argument = new CommandlineArgument($this);
91 65
        if ($insertAtStart) {
92 2
            array_unshift($this->arguments, $argument);
93
        } else {
94 65
            $this->arguments[] = $argument;
95
        }
96
97 65
        return $argument;
98
    }
99
100
    /**
101
     * Sets the executable to run.
102
     *
103
     * @param string $executable
104
     * @param bool $translateFileSeparator
105
     */
106 94
    public function setExecutable($executable, $translateFileSeparator = true): void
107
    {
108 94
        if ($executable === null || $executable === '') {
109 2
            return;
110
        }
111 93
        $this->executable = $translateFileSeparator
112 93
            ? str_replace(['/', '\\'], FileUtils::getSeparator(), $executable)
113
            : $executable;
114 93
    }
115
116
    /**
117
     * @return string
118
     */
119 70
    public function getExecutable()
120
    {
121 70
        return $this->executable;
122
    }
123
124
    /**
125
     * @param array $arguments
126
     */
127 1
    public function addArguments(array $arguments)
128
    {
129 1
        foreach ($arguments as $arg) {
130 1
            $this->createArgument()->setValue($arg);
131
        }
132 1
    }
133
134
    /**
135
     * Returns the executable and all defined arguments.
136
     *
137
     * @return array
138
     */
139 51
    public function getCommandline(): array
140
    {
141 51
        $args = $this->getArguments();
142 51
        if ($this->executable !== null && $this->executable !== '') {
143 51
            array_unshift($args, $this->executable);
144
        }
145
146 51
        return $args;
147
    }
148
149
    /**
150
     * Returns all arguments defined by <code>addLine</code>,
151
     * <code>addValue</code> or the argument object.
152
     */
153 52
    public function getArguments(): array
154
    {
155 52
        $result = [];
156 52
        foreach ($this->arguments as $arg) {
157 40
            $parts = $arg->getParts();
158 40
            if ($parts !== null) {
159 40
                foreach ($parts as $part) {
160 40
                    $result[] = $part;
161
                }
162
            }
163
        }
164
165 52
        return $result;
166
    }
167
168 46
    public function setEscape($flag)
169
    {
170 46
        $this->escape = $flag;
171 46
    }
172
173
    /**
174
     * @return string
175
     */
176 46
    public function __toString()
177
    {
178
        try {
179 46
            $cmd = $this->toString($this->getCommandline());
180
        } catch (BuildException $be) {
181
            $cmd = '';
182
        }
183
184 46
        return $cmd;
185
    }
186
187
    /**
188
     * Put quotes around the given String if necessary.
189
     *
190
     * <p>If the argument doesn't include spaces or quotes, return it
191
     * as is. If it contains double quotes, use single quotes - else
192
     * surround the argument by double quotes.</p>
193
     *
194
     * @param $argument
195
     *
196
     * @return string
197
     *
198
     * @throws BuildException if the argument contains both, single
199
     *                           and double quotes.
200
     */
201 46
    public static function quoteArgument($argument, $escape = false)
202
    {
203 46
        if ($escape) {
204 5
            return escapeshellarg($argument);
205
        }
206
207 41
        if (strpos($argument, '"') !== false) {
208
            if (strpos($argument, "'") !== false) {
209
                throw new BuildException("Can't handle single and double quotes in same argument");
210
            }
211
212
            return '\'' . $argument . '\'';
213
        }
214
215
        if (
216 41
            strpos($argument, "'") !== false
217 41
            || strpos($argument, ' ') !== false
218
            // WIN9x uses a bat file for executing commands
219 41
            || (OsCondition::isFamily('win32')
220
            && strpos($argument, ';') !== false)
221
        ) {
222 5
            return '"' . $argument . '"';
223
        }
224
225 41
        return $argument;
226
    }
227
228
    /**
229
     * Quotes the parts of the given array in way that makes them
230
     * usable as command line arguments.
231
     *
232
     * @param $lines
233
     *
234
     * @return string
235
     *
236
     * @throws BuildException
237
     */
238 46
    private function toString($lines = null): string
239
    {
240
        // empty path return empty string
241 46
        if ($lines === null || count($lines) === 0) {
242
            return '';
243
        }
244
245 46
        return implode(
246 46
            ' ',
247 46
            array_map(
248 46
                function ($arg) {
249 46
                    return self::quoteArgument($arg, $this->escape);
250 46
                },
251 46
                $lines
252
            )
253
        );
254
    }
255
256
    /**
257
     * Crack a command line.
258
     *
259
     * @param string $toProcess the command line to process.
260
     *
261
     * @return string[] the command line broken into strings.
262
     *                  An empty or null toProcess parameter results in a zero sized array.
263
     *
264
     * @throws \BuildException
265
     */
266 62
    public static function translateCommandline(string $toProcess = null): array
267
    {
268 62
        if ($toProcess === null || $toProcess === '') {
269
            return [];
270
        }
271
272
        // parse with a simple finite state machine
273
274 62
        $normal = 0;
275 62
        $inQuote = 1;
276 62
        $inDoubleQuote = 2;
277
278 62
        $state = $normal;
279 62
        $args = [];
280 62
        $current = "";
281 62
        $lastTokenHasBeenQuoted = false;
282
283 62
        $tokens = preg_split('/(["\' ])/', $toProcess, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
284 62
        while (($nextTok = array_shift($tokens)) !== null) {
0 ignored issues
show
Bug introduced by
It seems like $tokens can also be of type false; however, parameter $array of array_shift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

284
        while (($nextTok = array_shift(/** @scrutinizer ignore-type */ $tokens)) !== null) {
Loading history...
285
            switch ($state) {
286 62
                case $inQuote:
287 24
                    if ("'" === $nextTok) {
288 24
                        $lastTokenHasBeenQuoted = true;
289 24
                        $state = $normal;
290
                    } else {
291 24
                        $current .= $nextTok;
292
                    }
293 24
                    break;
294 62
                case $inDoubleQuote:
295 1
                    if ("\"" === $nextTok) {
296 1
                        $lastTokenHasBeenQuoted = true;
297 1
                        $state = $normal;
298
                    } else {
299 1
                        $current .= $nextTok;
300
                    }
301 1
                    break;
302
                default:
303 62
                    if ("'" === $nextTok) {
304 24
                        $state = $inQuote;
305 62
                    } elseif ("\"" === $nextTok) {
306 1
                        $state = $inDoubleQuote;
307 62
                    } elseif (" " === $nextTok) {
308 51
                        if ($lastTokenHasBeenQuoted || $current !== '') {
309 51
                            $args[] = $current;
310 51
                            $current = "";
311
                        }
312
                    } else {
313 62
                        $current .= $nextTok;
314
                    }
315 62
                    $lastTokenHasBeenQuoted = false;
316 62
                    break;
317
            }
318
        }
319
320 62
        if ($lastTokenHasBeenQuoted || $current !== '') {
0 ignored issues
show
introduced by
The condition $current !== '' is always false.
Loading history...
321 62
            $args[] = $current;
322
        }
323
324 62
        if ($state === $inQuote || $state === $inDoubleQuote) {
325 1
            throw new BuildException('unbalanced quotes in ' . $toProcess);
326
        }
327
328 62
        return $args;
329
    }
330
331
    /**
332
     * @return int Number of components in current commandline.
333
     */
334
    public function count(): int
335
    {
336
        return count($this->getCommandline());
337
    }
338
339
    /**
340
     * @throws \BuildException
341
     */
342
    public function __clone()
343
    {
344
        $c = new self();
345
        $c->addArguments($this->getArguments());
346
    }
347
348
    /**
349
     * Return a marker.
350
     *
351
     * <p>This marker can be used to locate a position on the
352
     * commandline - to insert something for example - when all
353
     * parameters have been set.</p>
354
     *
355
     * @return CommandlineMarker
356
     */
357 3
    public function createMarker()
358
    {
359 3
        return new CommandlineMarker($this, count($this->arguments));
360
    }
361
362
    /**
363
     * Returns a String that describes the command and arguments
364
     * suitable for verbose output before a call to
365
     * <code>Runtime.exec(String[])</code>.
366
     *
367
     * <p>This method assumes that the first entry in the array is the
368
     * executable to run.</p>
369
     *
370
     * @param  array|Commandline $args CommandlineArgument[] to use
371
     * @return string
372
     */
373 6
    public function describeCommand($args = null)
374
    {
375 6
        if ($args === null) {
376 6
            $args = $this->getCommandline();
377
        } elseif ($args instanceof self) {
378
            $args = $args->getCommandline();
379
        }
380
381 6
        if (!$args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
382
            return '';
383
        }
384
385 6
        $buf = "Executing '";
386 6
        $buf .= $args[0];
387 6
        $buf .= "'";
388 6
        if (count($args) > 0) {
389 6
            $buf .= ' with ';
390 6
            $buf .= $this->describeArguments($args, 1);
391
        } else {
392
            $buf .= self::DISCLAIMER;
393
        }
394
395 6
        return $buf;
396
    }
397
398
    /**
399
     * Returns a String that describes the arguments suitable for
400
     * verbose output before a call to
401
     * <code>Runtime.exec(String[])</code>
402
     *
403
     * @param  array $args arguments to use (default is to use current class args)
404
     * @param  int $offset ignore entries before this index
405
     * @return string
406
     */
407 6
    public function describeArguments(array $args = null, $offset = 0)
408
    {
409 6
        if ($args === null) {
410
            $args = $this->getArguments();
411
        }
412
413 6
        if ($args === null || count($args) <= $offset) {
414
            return '';
415
        }
416
417 6
        $buf = "argument";
418 6
        if (count($args) > $offset) {
419 6
            $buf .= "s";
420
        }
421 6
        $buf .= ":" . PHP_EOL;
422 6
        for ($i = $offset, $alen = count($args); $i < $alen; $i++) {
423 6
            $buf .= "'" . $args[$i] . "'" . PHP_EOL;
424
        }
425 6
        $buf .= self::DISCLAIMER;
426
427 6
        return $buf;
428
    }
429
}
430