Exec::escapeArgument()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
1
<?php
2
namespace nochso\Omni;
3
4
/**
5
 * Exec creates objects that help manage `\exec()` calls.
6
 *
7
 * The returned object itself is callable, which is the same as calling `run()`.
8
 *
9
 * Arguments are automatically escaped if needed.
10
 *
11
 * Methods `run()`, `create()` and `__invoke()` take any amount of arguments.
12
 * If you have an array of arguments, unpack it first: `run(...$args)`
13
 *
14
 * @see \nochso\Omni\OS::hasBinary Check if the binary/command is available before you run it.
15
 */
16
class Exec {
17
	/**
18
	 * @var string[]
19
	 */
20
	private $prefixes;
21
	/**
22
	 * @var string[]
23
	 */
24
	private $output;
25
	/**
26
	 * @var int
27
	 */
28
	private $status;
29
	/**
30
	 * @var string
31
	 */
32
	private $lastCommand;
33
34
	/**
35
	 * Create a new callable `Exec` object.
36
	 *
37
	 * @param string[] $prefixes,... Optional arguments will always be added to the beginning of the command.
0 ignored issues
show
Documentation introduced by
There is no parameter named $prefixes,.... Did you maybe mean $prefixes?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
38
	 *
39
	 * @return \nochso\Omni\Exec
40
	 */
41
	public static function create(...$prefixes) {
42
		$exec = new self();
43
		$exec->prefixes = $prefixes;
44
		return $exec;
45
	}
46
47
	/**
48
	 * Run a command with auto-escaped arguments.
49
	 *
50
	 * @param string[] $arguments,... Optional arguments will be added after the prefixes.
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
51
	 *
52
	 * @return $this
53
	 */
54
	public function run(...$arguments) {
55
		$this->lastCommand = $this->getCommand(...$arguments);
56
		exec($this->lastCommand, $output, $status);
57
		$this->output = $output;
0 ignored issues
show
Documentation Bug introduced by
It seems like $output can be null. However, the property $output is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
58
		$this->status = $status;
59
		return $this;
60
	}
61
62
	/**
63
	 * getCommand returns the string to be used by `\exec()`.
64
	 *
65
	 * @param string[] $arguments,...
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
66
	 *
67
	 * @return string
68
	 */
69
	public function getCommand(...$arguments) {
70
		$command = [];
71
		$allArguments = array_merge($this->prefixes, $arguments);
72
		if (count($allArguments) === 0) {
73
			$allArguments[] = '';
74
		}
75
		foreach ($allArguments as $argument) {
76
			$command[] = $this->escapeArgument($argument);
77
		}
78
		$commandString = implode(' ', $command);
79
		return $commandString;
80
	}
81
82
	/**
83
	 * getLastCommand returns the string last used by a previous call to `run()`.
84
	 *
85
	 * @return string|null
86
	 */
87
	public function getLastCommand() {
88
		return $this->lastCommand;
89
	}
90
91
	/**
92
	 * getOutput of last execution.
93
	 *
94
	 * @return string[]
95
	 */
96
	public function getOutput() {
97
		return $this->output;
98
	}
99
100
	/**
101
	 * getStatus code of last execution.
102
	 *
103
	 * @return int
104
	 */
105
	public function getStatus() {
106
		return $this->status;
107
	}
108
109
	/**
110
	 * __invoke allows using this object as a callable by calling `run()`.
111
	 *
112
	 * e.g. `$runner('argument');`
113
	 *
114
	 * @param array $arguments,...
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
115
	 *
116
	 * @return \nochso\Omni\Exec
117
	 */
118
	public function __invoke(...$arguments) {
119
		return $this->run(...$arguments);
120
	}
121
122
	/**
123
	 * @param string $argument
124
	 *
125
	 * @return string
126
	 */
127
	private function escapeArgument($argument) {
128
		// Always escape an empty argument so it doesn't get lost.
129
		if ($argument === '') {
130
			return escapeshellarg($argument);
131
		}
132
		if (!OS::isWindows()) {
133
			return $this->escapeLinuxArgument($argument);
134
		}
135
		return $this->escapeWindowsArgument($argument);
136
	}
137
138
	/**
139
	 * @param string $argument
140
	 *
141
	 * @return string
142
	 */
143
	private function escapeLinuxArgument($argument) {
144
		$escapedArgument = escapeshellarg($argument);
145
		// Is escaping really needed?
146
		if ($argument !== '--' && mb_substr($escapedArgument, 1, -1) === $argument && preg_match('/^[a-z0-9-]+$/i', $argument) === 1) {
147
			return $argument;
148
		}
149
		return $escapedArgument;
150
	}
151
152
	/**
153
	 * @param string $argument
154
	 *
155
	 * @return string
156
	 *
157
	 * @link https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
158
	 */
159
	private function escapeWindowsArgument($argument) {
160
		// Check if there's anything to escape
161
		if (strpbrk($argument, " \t\n\v\"\\") === false) {
162
			return $argument;
163
		}
164
		$escapedArgument = '"';
165
		$strlen = mb_strlen($argument);
166
		for ($i = 0; $i < $strlen; $i++) {
167
			$backslashes = 0;
168
			while ($i < $strlen && mb_substr($argument, $i, 1) === '\\') {
169
				$i++;
170
				$backslashes++;
171
			}
172
			if ($i === $strlen) {
173
				// Escape all backslashes, but let the terminating double quote be interpreted as a meta character
174
				$escapedArgument .= str_repeat('\\', $backslashes * 2);
175
			} elseif (mb_substr($argument, $i, 1) === '"') {
176
				// Escape all backslashes and the following quotation mark
177
				$escapedArgument .= str_repeat('\\', $backslashes * 2 + 1);
178
				$escapedArgument .= '"';
179
			} else {
180
				$escapedArgument .= str_repeat('\\', $backslashes);
181
				$escapedArgument .= mb_substr($argument, $i, 1);
182
			}
183
		}
184
		$escapedArgument .= '"';
185
		return $escapedArgument;
186
	}
187
}
188