1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @Author : a.zinovyev |
4
|
|
|
* @Package: rsync |
5
|
|
|
* @License: http://www.opensource.org/licenses/mit-license.php |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace xobotyi\rsync; |
9
|
|
|
|
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Class Command |
13
|
|
|
* |
14
|
|
|
* @package xobotyi\rsync |
15
|
|
|
*/ |
16
|
|
|
abstract class Command |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* @var array |
20
|
|
|
*/ |
21
|
|
|
protected $OPTIONS_LIST = []; |
22
|
|
|
/** |
23
|
|
|
* @var string |
24
|
|
|
*/ |
25
|
|
|
private $cwd = './'; |
26
|
|
|
/** |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
private $executable; |
30
|
|
|
/** |
31
|
|
|
* @var int|null |
32
|
|
|
*/ |
33
|
|
|
private $exitCode; |
34
|
|
|
/** |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
private $options = []; |
38
|
|
|
/** |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
private $parameters = []; |
42
|
|
|
/** |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
private $stderr; |
46
|
|
|
/** |
47
|
|
|
* @var string |
48
|
|
|
*/ |
49
|
|
|
private $stdout; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Command constructor. |
53
|
|
|
* |
54
|
|
|
* @param string $executable |
55
|
|
|
* @param string $cwd |
56
|
|
|
* |
57
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
58
|
|
|
*/ |
59
|
|
|
public function __construct(string $executable, string $cwd = './') { |
60
|
|
|
$this->setExecutable($executable) |
61
|
|
|
->setCWD($cwd); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Check if given variable can be casted to the string |
66
|
|
|
* |
67
|
|
|
* @param $var |
68
|
|
|
* |
69
|
|
|
* @return bool |
70
|
|
|
*/ |
71
|
|
|
private static function isStringable(&$var) :bool { |
72
|
|
|
return (\is_string($var) || \is_numeric($var) || (\is_object($var) && \method_exists($var, '__toString'))); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* |
77
|
|
|
* |
78
|
|
|
* @param array $arrayToStore |
79
|
|
|
* @param string $option |
80
|
|
|
* @param $value |
81
|
|
|
* |
82
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
83
|
|
|
*/ |
84
|
|
|
private static function StoreOption(array &$arrayToStore, string $option, $value) :void { |
85
|
|
|
if ($value === true) { |
|
|
|
|
86
|
|
|
} |
87
|
|
|
else if ($value === false) { |
88
|
|
|
unset($arrayToStore[$option]); |
89
|
|
|
|
90
|
|
|
return; |
91
|
|
|
} |
92
|
|
|
else if (is_array($value)) { |
93
|
|
|
foreach ($value as &$val) { |
94
|
|
|
if (!self::isStringable($val)) { |
95
|
|
|
throw new Exception\Command("Option {$option} has non-stringable element"); |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
} |
99
|
|
|
else if (!self::isStringable($value)) { |
100
|
|
|
throw new Exception\Command("Option {$option} got non-stringable value"); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
$arrayToStore[$option] = $value; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Check if given path is an executable file |
108
|
|
|
* |
109
|
|
|
* @param string $exec path to executable |
110
|
|
|
* |
111
|
|
|
* @return bool |
112
|
|
|
*/ |
113
|
|
|
public static function isExecutable(string $exec) :bool { |
114
|
|
|
if (substr(strtolower(php_uname('s')), 0, 3) === 'win') { |
115
|
|
|
if (strpos($exec, '/') !== false || strpos($exec, '\\') !== false) { |
116
|
|
|
$exec = dirname($exec); |
117
|
|
|
$exec = ($exec ? $exec . ':' : '') . basename($exec); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
exec('where' . ' /Q ' . escapeshellcmd($exec), $output, $code); |
121
|
|
|
|
122
|
|
|
return $code === 0; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
return (bool)shell_exec('which' . ' ' . escapeshellcmd($exec)); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Build the command |
130
|
|
|
* |
131
|
|
|
* @return string |
132
|
|
|
*/ |
133
|
|
|
public function __toString() :string { |
134
|
|
|
$options = $this->getOptionsString(); |
135
|
|
|
$parameters = $this->getParametersString(); |
136
|
|
|
|
137
|
|
|
return $this->executable . ($options ?: '') . ($parameters ?: ''); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Build the options string |
142
|
|
|
* |
143
|
|
|
* @return string |
144
|
|
|
*/ |
145
|
|
|
public function getOptionsString() :string { |
146
|
|
|
if (empty($this->options)) { |
147
|
|
|
return ''; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$shortOptions = $longOptions = $parametrizedOptions = ''; |
151
|
|
|
|
152
|
|
|
foreach ($this->options as $opt => $value) { |
153
|
|
|
$option = $this->OPTIONS_LIST[$opt]['option']; |
154
|
|
|
$isLongOption = strlen($option) > 1; |
155
|
|
|
|
156
|
|
|
if (!($this->OPTIONS_LIST[$opt]['argument'] ?? false)) { |
157
|
|
|
$isLongOption |
158
|
|
|
? $longOptions .= ' --' . $option |
159
|
|
|
: $shortOptions .= $option; |
160
|
|
|
|
161
|
|
|
continue; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
$option = ($isLongOption ? ' --' : ' -') . $option; |
165
|
|
|
|
166
|
|
|
if ($this->OPTIONS_LIST[$opt]['repeatable'] ?? false) { |
167
|
|
|
foreach ($value as $val) { |
168
|
|
|
$parametrizedOptions .= $option . ' ' . escapeshellarg($val); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
continue; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
$parametrizedOptions .= $option . ' ' . escapeshellarg($value); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$shortOptions = $shortOptions ?: ''; |
178
|
|
|
$longOptions = $longOptions ?: ''; |
179
|
|
|
$parametrizedOptions = $parametrizedOptions ?: ''; |
180
|
|
|
|
181
|
|
|
return ($shortOptions ? ' -' . $shortOptions : '') |
182
|
|
|
. $longOptions |
183
|
|
|
. $parametrizedOptions; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Build the parameters string |
188
|
|
|
* |
189
|
|
|
* @return string |
190
|
|
|
*/ |
191
|
|
|
public function getParametersString() :string { |
192
|
|
|
if (empty($this->parameters)) { |
193
|
|
|
return ''; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$parametersStr = ''; |
197
|
|
|
|
198
|
|
|
foreach ($this->parameters as $value) { |
199
|
|
|
$parametersStr .= ' ' . $value; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
return $parametersStr; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Add single parameter to the string. New parameter will be appended to the end. |
207
|
|
|
* |
208
|
|
|
* @param $parameter |
209
|
|
|
* |
210
|
|
|
* @return $this |
211
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
212
|
|
|
*/ |
213
|
|
|
public function addParameter($parameter) { |
214
|
|
|
if (!self::isStringable($parameter)) { |
215
|
|
|
throw new Exception\Command("Got non-stringable parameter"); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
$this->parameters[] = $parameter; |
219
|
|
|
|
220
|
|
|
return $this; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Empty the parameters. |
225
|
|
|
* |
226
|
|
|
* @return \xobotyi\rsync\Command |
227
|
|
|
*/ |
228
|
|
|
public function clearParameters() :self { |
229
|
|
|
$this->parameters = []; |
230
|
|
|
|
231
|
|
|
return $this; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Execute the command by opening the child process. |
236
|
|
|
* |
237
|
|
|
* @return \xobotyi\rsync\Command |
238
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
239
|
|
|
*/ |
240
|
|
|
public function execute() :self { |
241
|
|
|
$this->exitCode = 1; // exit 0 on ok |
242
|
|
|
$this->stdout = ''; // output of the command |
243
|
|
|
$this->stderr = ''; // errors during execution |
244
|
|
|
|
245
|
|
|
$descriptor = [ |
246
|
|
|
0 => ["pipe", "r"], // stdin is a pipe that the child will read from |
247
|
|
|
1 => ["pipe", "w"], // stdout is a pipe that the child will write to |
248
|
|
|
2 => ["pipe", "w"] // stderr is a pipe |
249
|
|
|
]; |
250
|
|
|
|
251
|
|
|
$proc = proc_open((string)$this, $descriptor, $pipes, $this->cwd); |
252
|
|
|
|
253
|
|
|
if ($proc === false) { |
254
|
|
|
throw new Exception\Command("Unable to execute command '{$this}'"); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$this->stdout = trim(stream_get_contents($pipes[1])); |
258
|
|
|
$this->stderr = trim(stream_get_contents($pipes[2])); |
259
|
|
|
|
260
|
|
|
fclose($pipes[0]); |
261
|
|
|
fclose($pipes[1]); |
262
|
|
|
fclose($pipes[2]); |
263
|
|
|
|
264
|
|
|
$this->exitCode = proc_close($proc); |
265
|
|
|
|
266
|
|
|
return $this; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Return the current working directory. |
271
|
|
|
* |
272
|
|
|
* @return string |
273
|
|
|
*/ |
274
|
|
|
public function getCWD() :string { |
275
|
|
|
return $this->cwd; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Set the current working directory. Will be used during the command execution. |
280
|
|
|
* |
281
|
|
|
* @param string $cwd CWD path |
282
|
|
|
* |
283
|
|
|
* @return \xobotyi\rsync\Command |
284
|
|
|
*/ |
285
|
|
|
public function setCWD(string $cwd) :self { |
286
|
|
|
$this->cwd = $cwd; |
287
|
|
|
|
288
|
|
|
return $this; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Get the executable path |
293
|
|
|
* |
294
|
|
|
* @return string |
295
|
|
|
*/ |
296
|
|
|
public function getExecutable() :string { |
297
|
|
|
return $this->executable; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Set the executable path |
302
|
|
|
* |
303
|
|
|
* @param string $executable |
304
|
|
|
* |
305
|
|
|
* @return $this |
306
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
307
|
|
|
*/ |
308
|
|
|
public function setExecutable(string $executable) { |
309
|
|
|
if (!($executable = \trim($executable))) { |
310
|
|
|
throw new Exception\Command("Executable path must be a valuable string"); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
if (!self::isExecutable($executable)) { |
314
|
|
|
throw new Exception\Command("{$executable} is not executable"); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
$this->executable = $executable; |
318
|
|
|
|
319
|
|
|
return $this; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Return the last command execution exitCode, null if command hasn't been executed yet |
324
|
|
|
* |
325
|
|
|
* @return string|null |
326
|
|
|
*/ |
327
|
|
|
public function getExitCode() :?string { |
328
|
|
|
return $this->exitCode; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Get the options array |
333
|
|
|
* |
334
|
|
|
* @return array |
335
|
|
|
*/ |
336
|
|
|
public function getOptions() :array { |
337
|
|
|
return $this->options; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Set the bunch of option |
342
|
|
|
* |
343
|
|
|
* @param array $options an array of options, where array keys are options names and array values are options values |
344
|
|
|
* |
345
|
|
|
* @return $this |
346
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
347
|
|
|
*/ |
348
|
|
|
public function setOptions(array $options) { |
349
|
|
|
foreach ($options as $option => $value) { |
350
|
|
|
$this->setOption($option, $value); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
return $this; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Set the command's option. |
358
|
|
|
* |
359
|
|
|
* @param string $optName option name (see the constants list for options names and its descriptions) |
360
|
|
|
* @param mixed $val option value, by default is true, if has false value - option wil be removed from result |
361
|
|
|
* command. |
362
|
|
|
* |
363
|
|
|
* @return $this |
364
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
365
|
|
|
*/ |
366
|
|
|
public function setOption(string $optName, $val = true) { |
367
|
|
|
if (!($this->OPTIONS_LIST[$optName] ?? false)) { |
368
|
|
|
throw new Exception\Command("Option {$optName} is not supported"); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
if (!is_bool($val) && !($this->OPTIONS_LIST[$optName]['argument'] ?? false)) { |
372
|
|
|
throw new Exception\Command("Option {$optName} can not have any argument"); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
if (is_array($val) && !($this->OPTIONS_LIST[$optName]['repeatable'] ?? false)) { |
376
|
|
|
throw new Exception\Command("Option {$optName} is not repeatable (its value cant be an array)"); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
self::StoreOption($this->options, $optName, $val); |
380
|
|
|
|
381
|
|
|
return $this; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Return the parameters of command |
386
|
|
|
* |
387
|
|
|
* @return array |
388
|
|
|
*/ |
389
|
|
|
public function getParameters() :array { |
390
|
|
|
return $this->parameters; |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
* Set the bunch of command parameters |
395
|
|
|
* |
396
|
|
|
* @param array $parameters array of strings to set as command parameters, each string will be appended to the end |
397
|
|
|
* of command |
398
|
|
|
* |
399
|
|
|
* @return $this |
400
|
|
|
* @throws \xobotyi\rsync\Exception\Command |
401
|
|
|
*/ |
402
|
|
|
public function setParameters(array $parameters) { |
403
|
|
|
$params = []; |
404
|
|
|
foreach ($parameters as &$value) { |
405
|
|
|
if (!self::isStringable($value)) { |
406
|
|
|
throw new Exception\Command("Got non-stringable parameter"); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
$params[] = (string)$value; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
$this->parameters = $params; |
413
|
|
|
|
414
|
|
|
return $this; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Return the last command execution stderr, null if command hasn't been executed yet |
419
|
|
|
* |
420
|
|
|
* @return string|null |
421
|
|
|
*/ |
422
|
|
|
public function getStderr() :?string { |
423
|
|
|
|
424
|
|
|
return $this->stderr; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Return the last command execution stdout, null if command hasn't been executed yet |
429
|
|
|
* |
430
|
|
|
* @return string|null |
431
|
|
|
*/ |
432
|
|
|
public function getStdout() :?string { |
433
|
|
|
return $this->stdout; |
434
|
|
|
} |
435
|
|
|
} |
This check looks for the bodies of
if
statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.These
if
bodies can be removed. If you have an empty if but statements in theelse
branch, consider inverting the condition.could be turned into
This is much more concise to read.