shellcommand.class.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <?php
  2. namespace mikehaertl\shellcommand;
  3. /**
  4. * Command
  5. *
  6. * This class represents a shell command.
  7. *
  8. * Its meant for exuting a single command and capturing stdout and stderr.
  9. *
  10. * Example:
  11. *
  12. * ```
  13. * $command = new Command('/usr/local/bin/mycommand -a -b');
  14. * $command->addArg('--name=', "d'Artagnan");
  15. * if ($command->execute()) {
  16. * echo $command->getOutput();
  17. * } else {
  18. * echo $command->getError();
  19. * $exitCode = $command->getExitCode();
  20. * }
  21. * ```
  22. *
  23. * @author Michael Härtl <haertl.mike@gmail.com>
  24. * @license http://www.opensource.org/licenses/MIT
  25. */
  26. class Command
  27. {
  28. /**
  29. * @var bool whether to escape any argument passed through `addArg()`.
  30. * Default is `true`.
  31. */
  32. public $escapeArgs = true;
  33. /**
  34. * @var bool whether to escape the command passed to `setCommand()` or the
  35. * constructor. This is only useful if `$escapeArgs` is `false`. Default
  36. * is `false`.
  37. */
  38. public $escapeCommand = false;
  39. /**
  40. * @var bool whether to use `exec()` instead of `proc_open()`. This can be
  41. * used on Windows system to workaround some quirks there. Note, that any
  42. * errors from your command will be output directly to the PHP output
  43. * stream. `getStdErr()` will also not work anymore and thus you also won't
  44. * get the error output from `getError()` in this case. You also can't pass
  45. * any environment variables to the command if this is enabled. Default is
  46. * `false`.
  47. */
  48. public $useExec = false;
  49. /**
  50. * @var bool whether to capture stderr (2>&1) when `useExec` is true. This
  51. * will try to redirect the stderr to stdout and provide the complete
  52. * output of both in `getStdErr()` and `getError()`. Default is `true`.
  53. */
  54. public $captureStdErr = true;
  55. /**
  56. * @var string|null the initial working dir for `proc_open()`. Default is
  57. * `null` for current PHP working dir.
  58. */
  59. public $procCwd;
  60. /**
  61. * @var array|null an array with environment variables to pass to
  62. * `proc_open()`. Default is `null` for none.
  63. */
  64. public $procEnv;
  65. /**
  66. * @var array|null an array of other_options for `proc_open()`. Default is
  67. * `null` for none.
  68. */
  69. public $procOptions;
  70. /**
  71. * @var bool|null whether to set the stdin/stdout/stderr streams to
  72. * non-blocking mode when `proc_open()` is used. This allows to have huge
  73. * inputs/outputs without making the process hang. The default is `null`
  74. * which will enable the feature on Non-Windows systems. Set it to `true`
  75. * or `false` to manually enable/disable it. It does not work on Windows.
  76. */
  77. public $nonBlockingMode;
  78. /**
  79. * @var int the time in seconds after which a command should be terminated.
  80. * This only works in non-blocking mode. Default is `null` which means the
  81. * process is never terminated.
  82. */
  83. public $timeout;
  84. /**
  85. * @var null|string the locale to temporarily set before calling
  86. * `escapeshellargs()`. Default is `null` for none.
  87. */
  88. public $locale;
  89. /**
  90. * @var null|string|resource to pipe to standard input
  91. */
  92. protected $_stdIn;
  93. /**
  94. * @var string the command to execute
  95. */
  96. protected $_command;
  97. /**
  98. * @var array the list of command arguments
  99. */
  100. protected $_args = array();
  101. /**
  102. * @var string the stdout output
  103. */
  104. protected $_stdOut = '';
  105. /**
  106. * @var string the stderr output
  107. */
  108. protected $_stdErr = '';
  109. /**
  110. * @var int the exit code
  111. */
  112. protected $_exitCode;
  113. /**
  114. * @var string the error message
  115. */
  116. protected $_error = '';
  117. /**
  118. * @var bool whether the command was successfully executed
  119. */
  120. protected $_executed = false;
  121. /**
  122. * @param string|array $options either a command string or an options array
  123. * @see setOptions
  124. */
  125. public function __construct($options = null)
  126. {
  127. if (is_array($options)) {
  128. $this->setOptions($options);
  129. } elseif (is_string($options)) {
  130. $this->setCommand($options);
  131. }
  132. }
  133. /**
  134. * @param array $options array of name => value options (i.e. public
  135. * properties) that should be applied to this object. You can also pass
  136. * options that use a setter, e.g. you can pass a `fileName` option which
  137. * will be passed to `setFileName()`.
  138. * @throws \Exception on unknown option keys
  139. * @return static for method chaining
  140. */
  141. public function setOptions($options)
  142. {
  143. foreach ($options as $key => $value) {
  144. if (property_exists($this, $key)) {
  145. $this->$key = $value;
  146. } else {
  147. $method = 'set'.ucfirst($key);
  148. if (method_exists($this, $method)) {
  149. call_user_func(array($this,$method), $value);
  150. } else {
  151. throw new \Exception("Unknown configuration option '$key'");
  152. }
  153. }
  154. }
  155. return $this;
  156. }
  157. /**
  158. * @param string $command the command or full command string to execute,
  159. * like 'gzip' or 'gzip -d'. You can still call addArg() to add more
  160. * arguments to the command. If `$escapeCommand` was set to true, the command
  161. * gets escaped with `escapeshellcmd()`.
  162. * @return static for method chaining
  163. */
  164. public function setCommand($command)
  165. {
  166. if ($this->escapeCommand) {
  167. $command = escapeshellcmd($command);
  168. }
  169. if ($this->getIsWindows()) {
  170. // Make sure to switch to correct drive like "E:" first if we have
  171. // a full path in command
  172. if (isset($command[1]) && $command[1] === ':') {
  173. $position = 1;
  174. // Could be a quoted absolute path because of spaces.
  175. // i.e. "C:\Program Files (x86)\file.exe"
  176. } elseif (isset($command[2]) && $command[2] === ':') {
  177. $position = 2;
  178. } else {
  179. $position = false;
  180. }
  181. // Absolute path. If it's a relative path, let it slide.
  182. if ($position) {
  183. $command = sprintf(
  184. $command[$position - 1] . ': && cd %s && %s',
  185. escapeshellarg(dirname($command)),
  186. escapeshellarg(basename($command))
  187. );
  188. }
  189. }
  190. $this->_command = $command;
  191. return $this;
  192. }
  193. /**
  194. * @param string|resource $stdIn If set, the string will be piped to the
  195. * command via standard input. This enables the same functionality as
  196. * piping on the command line. It can also be a resource like a file
  197. * handle or a stream in which case its content will be piped into the
  198. * command like an input redirection.
  199. * @return static for method chaining
  200. */
  201. public function setStdIn($stdIn) {
  202. $this->_stdIn = $stdIn;
  203. return $this;
  204. }
  205. /**
  206. * @return string|null the command that was set through `setCommand()` or
  207. * passed to the constructor. `null` if none.
  208. */
  209. public function getCommand()
  210. {
  211. return $this->_command;
  212. }
  213. /**
  214. * @return string|bool the full command string to execute. If no command
  215. * was set with `setCommand()` or passed to the constructor it will return
  216. * `false`.
  217. */
  218. public function getExecCommand()
  219. {
  220. $command = $this->getCommand();
  221. if (!$command) {
  222. $this->_error = 'Could not locate any executable command';
  223. return false;
  224. }
  225. $args = $this->getArgs();
  226. return $args ? $command.' '.$args : $command;
  227. }
  228. /**
  229. * @param string $args the command arguments as string like `'--arg1=value1
  230. * --arg2=value2'`. Note that this string will not get escaped. This will
  231. * overwrite the args added with `addArgs()`.
  232. * @return static for method chaining
  233. */
  234. public function setArgs($args)
  235. {
  236. $this->_args = array($args);
  237. return $this;
  238. }
  239. /**
  240. * @return string the command args that where set with `setArgs()` or added
  241. * with `addArg()` separated by spaces.
  242. */
  243. public function getArgs()
  244. {
  245. return implode(' ', $this->_args);
  246. }
  247. /**
  248. * @param string $key the argument key to add e.g. `--feature` or
  249. * `--name=`. If the key does not end with `=`, the (optional) $value will
  250. * be separated by a space. The key will get escaped if `$escapeArgs` is `true`.
  251. * @param string|array|null $value the optional argument value which will
  252. * get escaped if $escapeArgs is true. An array can be passed to add more
  253. * than one value for a key, e.g.
  254. * `addArg('--exclude', array('val1','val2'))`
  255. * which will create the option
  256. * `'--exclude' 'val1' 'val2'`.
  257. * @param bool|null $escape if set, this overrides the `$escapeArgs` setting
  258. * and enforces escaping/no escaping of keys and values
  259. * @return static for method chaining
  260. */
  261. public function addArg($key, $value = null, $escape = null)
  262. {
  263. $doEscape = $escape !== null ? $escape : $this->escapeArgs;
  264. $useLocale = $doEscape && $this->locale !== null;
  265. if ($useLocale) {
  266. $locale = setlocale(LC_CTYPE, 0); // Returns current locale setting
  267. setlocale(LC_CTYPE, $this->locale);
  268. }
  269. if ($value === null) {
  270. $this->_args[] = $doEscape ? escapeshellarg($key) : $key;
  271. } else {
  272. if (substr($key, -1) === '=') {
  273. $separator = '=';
  274. $argKey = substr($key, 0, -1);
  275. } else {
  276. $separator = ' ';
  277. $argKey = $key;
  278. }
  279. $argKey = $doEscape ? escapeshellarg($argKey) : $argKey;
  280. if (is_array($value)) {
  281. $params = array();
  282. foreach ($value as $v) {
  283. $params[] = $doEscape ? escapeshellarg($v) : $v;
  284. }
  285. $this->_args[] = $argKey . $separator . implode(' ', $params);
  286. } else {
  287. $this->_args[] = $argKey . $separator .
  288. ($doEscape ? escapeshellarg($value) : $value);
  289. }
  290. }
  291. if ($useLocale) {
  292. setlocale(LC_CTYPE, $locale);
  293. }
  294. return $this;
  295. }
  296. /**
  297. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  298. * @param string $characters the list of characters to trim. The default
  299. * is ` \t\n\r\0\v\f`.
  300. * @return string the command output (stdout). Empty if none.
  301. */
  302. public function getOutput($trim = true, $characters = " \t\n\r\0\v\f")
  303. {
  304. return $trim ? trim($this->_stdOut, $characters) : $this->_stdOut;
  305. }
  306. /**
  307. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  308. * @param string $characters the list of characters to trim. The default
  309. * is ` \t\n\r\0\v\f`.
  310. * @return string the error message, either stderr or an internal message.
  311. * Empty string if none.
  312. */
  313. public function getError($trim = true, $characters = " \t\n\r\0\v\f")
  314. {
  315. return $trim ? trim($this->_error, $characters) : $this->_error;
  316. }
  317. /**
  318. * @param bool $trim whether to `trim()` the return value. The default is `true`.
  319. * @param string $characters the list of characters to trim. The default
  320. * is ` \t\n\r\0\v\f`.
  321. * @return string the stderr output. Empty if none.
  322. */
  323. public function getStdErr($trim = true, $characters = " \t\n\r\0\v\f")
  324. {
  325. return $trim ? trim($this->_stdErr, $characters) : $this->_stdErr;
  326. }
  327. /**
  328. * @return int|null the exit code or null if command was not executed yet
  329. */
  330. public function getExitCode()
  331. {
  332. return $this->_exitCode;
  333. }
  334. /**
  335. * @return string whether the command was successfully executed
  336. */
  337. public function getExecuted()
  338. {
  339. return $this->_executed;
  340. }
  341. /**
  342. * Execute the command
  343. *
  344. * @return bool whether execution was successful. If `false`, error details
  345. * can be obtained from `getError()`, `getStdErr()` and `getExitCode()`.
  346. */
  347. public function execute()
  348. {
  349. $command = $this->getExecCommand();
  350. if (!$command) {
  351. return false;
  352. }
  353. if ($this->useExec) {
  354. $execCommand = $this->captureStdErr ? "$command 2>&1" : $command;
  355. exec($execCommand, $output, $this->_exitCode);
  356. $this->_stdOut = implode("\n", $output);
  357. if ($this->_exitCode !== 0) {
  358. $this->_stdErr = $this->_stdOut;
  359. $this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr;
  360. return false;
  361. }
  362. } else {
  363. $isInputStream = $this->_stdIn !== null &&
  364. is_resource($this->_stdIn) &&
  365. in_array(get_resource_type($this->_stdIn), array('file', 'stream'));
  366. $isInputString = is_string($this->_stdIn);
  367. $hasInput = $isInputStream || $isInputString;
  368. $hasTimeout = $this->timeout !== null && $this->timeout > 0;
  369. $descriptors = array(
  370. 1 => array('pipe','w'),
  371. 2 => array('pipe', $this->getIsWindows() ? 'a' : 'w'),
  372. );
  373. if ($hasInput) {
  374. $descriptors[0] = array('pipe', 'r');
  375. }
  376. // Issue #20 Set non-blocking mode to fix hanging processes
  377. $nonBlocking = $this->nonBlockingMode === null ?
  378. !$this->getIsWindows() : $this->nonBlockingMode;
  379. $startTime = $hasTimeout ? time() : 0;
  380. $process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions);
  381. if (is_resource($process)) {
  382. if ($nonBlocking) {
  383. stream_set_blocking($pipes[1], false);
  384. stream_set_blocking($pipes[2], false);
  385. if ($hasInput) {
  386. $writtenBytes = 0;
  387. $isInputOpen = true;
  388. stream_set_blocking($pipes[0], false);
  389. if ($isInputStream) {
  390. stream_set_blocking($this->_stdIn, false);
  391. }
  392. }
  393. // Due to the non-blocking streams we now have to check in
  394. // a loop if the process is still running. We also need to
  395. // ensure that all the pipes are written/read alternately
  396. // until there's nothing left to write/read.
  397. $isRunning = true;
  398. while ($isRunning) {
  399. $status = proc_get_status($process);
  400. $isRunning = $status['running'];
  401. // We first write to stdIn if we have an input. For big
  402. // inputs it will only write until the input buffer of
  403. // the command is full (the command may now wait that
  404. // we read the output buffers - see below). So we may
  405. // have to continue writing in another cycle.
  406. //
  407. // After everything is written it's safe to close the
  408. // input pipe.
  409. if ($isRunning && $hasInput && $isInputOpen) {
  410. if ($isInputStream) {
  411. $written = stream_copy_to_stream($this->_stdIn, $pipes[0], 16 * 1024, $writtenBytes);
  412. if ($written === false || $written === 0) {
  413. $isInputOpen = false;
  414. fclose($pipes[0]);
  415. } else {
  416. $writtenBytes += $written;
  417. }
  418. } else {
  419. if ($writtenBytes < strlen($this->_stdIn)) {
  420. $writtenBytes += fwrite($pipes[0], substr($this->_stdIn, $writtenBytes));
  421. } else {
  422. $isInputOpen = false;
  423. fclose($pipes[0]);
  424. }
  425. }
  426. }
  427. // Read out the output buffers because if they are full
  428. // the command may block execution. We do this even if
  429. // $isRunning is `false`, because there could be output
  430. // left in the buffers.
  431. //
  432. // The latter is only an assumption and needs to be
  433. // verified - but it does not hurt either and works as
  434. // expected.
  435. //
  436. while (($out = fgets($pipes[1])) !== false) {
  437. $this->_stdOut .= $out;
  438. }
  439. while (($err = fgets($pipes[2])) !== false) {
  440. $this->_stdErr .= $err;
  441. }
  442. $runTime = $hasTimeout ? time() - $startTime : 0;
  443. if ($isRunning && $hasTimeout && $runTime >= $this->timeout) {
  444. // Only send a SIGTERM and handle status in the next cycle
  445. proc_terminate($process);
  446. }
  447. if (!$isRunning) {
  448. $this->_exitCode = $status['exitcode'];
  449. if ($this->_exitCode !== 0 && empty($this->_stdErr)) {
  450. if ($status['stopped']) {
  451. $signal = $status['stopsig'];
  452. $this->_stdErr = "Command stopped by signal $signal";
  453. } elseif ($status['signaled']) {
  454. $signal = $status['termsig'];
  455. $this->_stdErr = "Command terminated by signal $signal";
  456. } else {
  457. $this->_stdErr = 'Command unexpectedly terminated without error message';
  458. }
  459. }
  460. fclose($pipes[1]);
  461. fclose($pipes[2]);
  462. proc_close($process);
  463. } else {
  464. // The command is still running. Let's wait some
  465. // time before we start the next cycle.
  466. usleep(10000);
  467. }
  468. }
  469. } else {
  470. if ($hasInput) {
  471. if ($isInputStream) {
  472. stream_copy_to_stream($this->_stdIn, $pipes[0]);
  473. } elseif ($isInputString) {
  474. fwrite($pipes[0], $this->_stdIn);
  475. }
  476. fclose($pipes[0]);
  477. }
  478. $this->_stdOut = stream_get_contents($pipes[1]);
  479. $this->_stdErr = stream_get_contents($pipes[2]);
  480. fclose($pipes[1]);
  481. fclose($pipes[2]);
  482. $this->_exitCode = proc_close($process);
  483. }
  484. if ($this->_exitCode !== 0) {
  485. $this->_error = $this->_stdErr ?
  486. $this->_stdErr :
  487. "Failed without error message: $command (Exit code: {$this->_exitCode})";
  488. return false;
  489. }
  490. } else {
  491. $this->_error = "Could not run command $command";
  492. return false;
  493. }
  494. }
  495. $this->_executed = true;
  496. return true;
  497. }
  498. /**
  499. * @return bool whether we are on a Windows OS
  500. */
  501. public function getIsWindows()
  502. {
  503. return strncasecmp(PHP_OS, 'WIN', 3)===0;
  504. }
  505. /**
  506. * @return string the current command string to execute
  507. */
  508. public function __toString()
  509. {
  510. return (string) $this->getExecCommand();
  511. }
  512. }