PageRenderTime 63ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/MrClay/Cli.php

https://github.com/soitun/minify
PHP | 392 lines | 238 code | 42 blank | 112 comment | 52 complexity | ae008982e793f2b60392b136bde76d34 MD5 | raw file
  1. <?php
  2. namespace MrClay;
  3. use MrClay\Cli\Arg;
  4. use InvalidArgumentException;
  5. /**
  6. * Forms a front controller for a console app, handling and validating arguments (options)
  7. *
  8. * Instantiate, add arguments, then call validate(). Afterwards, the user's valid arguments
  9. * and their values will be available in $cli->values.
  10. *
  11. * You may also specify that some arguments be used to provide input/output. By communicating
  12. * solely through the file pointers provided by openInput()/openOutput(), you can make your
  13. * app more flexible to end users.
  14. *
  15. * @author Steve Clay <steve@mrclay.org>
  16. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  17. */
  18. class Cli
  19. {
  20. /**
  21. * @var array validation errors
  22. */
  23. public $errors = array();
  24. /**
  25. * @var array option values available after validation.
  26. *
  27. * E.g. array(
  28. * 'a' => false // option was missing
  29. * ,'b' => true // option was present
  30. * ,'c' => "Hello" // option had value
  31. * ,'f' => "/home/user/file" // file path from root
  32. * ,'f.raw' => "~/file" // file path as given to option
  33. * )
  34. */
  35. public $values = array();
  36. /**
  37. * @var array
  38. */
  39. public $moreArgs = array();
  40. /**
  41. * @var array
  42. */
  43. public $debug = array();
  44. /**
  45. * @var bool The user wants help info
  46. */
  47. public $isHelpRequest = false;
  48. /**
  49. * @var Arg[]
  50. */
  51. protected $_args = array();
  52. /**
  53. * @var resource
  54. */
  55. protected $_stdin = null;
  56. /**
  57. * @var resource
  58. */
  59. protected $_stdout = null;
  60. /**
  61. * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined
  62. */
  63. public function __construct($exitIfNoStdin = true)
  64. {
  65. if ($exitIfNoStdin && ! defined('STDIN')) {
  66. exit('This script is for command-line use only.');
  67. }
  68. if (isset($GLOBALS['argv'][1])
  69. && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) {
  70. $this->isHelpRequest = true;
  71. }
  72. }
  73. /**
  74. * @param Arg|string $letter
  75. * @return Arg
  76. */
  77. public function addOptionalArg($letter)
  78. {
  79. return $this->addArgument($letter, false);
  80. }
  81. /**
  82. * @param Arg|string $letter
  83. * @return Arg
  84. */
  85. public function addRequiredArg($letter)
  86. {
  87. return $this->addArgument($letter, true);
  88. }
  89. /**
  90. * @param string $letter
  91. * @param bool $required
  92. * @param Arg|null $arg
  93. * @return Arg
  94. * @throws InvalidArgumentException
  95. */
  96. public function addArgument($letter, $required, Arg $arg = null)
  97. {
  98. if (! preg_match('/^[a-zA-Z]$/', $letter)) {
  99. throw new InvalidArgumentException('$letter must be in [a-zA-Z]');
  100. }
  101. if (! $arg) {
  102. $arg = new Arg($required);
  103. }
  104. $this->_args[$letter] = $arg;
  105. return $arg;
  106. }
  107. /**
  108. * @param string $letter
  109. * @return Arg|null
  110. */
  111. public function getArgument($letter)
  112. {
  113. return isset($this->_args[$letter]) ? $this->_args[$letter] : null;
  114. }
  115. /*
  116. * Read and validate options
  117. *
  118. * @return bool true if all options are valid
  119. */
  120. public function validate()
  121. {
  122. $options = '';
  123. $this->errors = array();
  124. $this->values = array();
  125. $this->_stdin = null;
  126. if ($this->isHelpRequest) {
  127. return false;
  128. }
  129. $lettersUsed = '';
  130. foreach ($this->_args as $letter => $arg) {
  131. /* @var Arg $arg */
  132. $options .= $letter;
  133. $lettersUsed .= $letter;
  134. if ($arg->mayHaveValue || $arg->mustHaveValue) {
  135. $options .= ($arg->mustHaveValue ? ':' : '::');
  136. }
  137. }
  138. $this->debug['argv'] = $GLOBALS['argv'];
  139. $argvCopy = array_slice($GLOBALS['argv'], 1);
  140. $o = getopt($options);
  141. $this->debug['getopt_options'] = $options;
  142. $this->debug['getopt_return'] = $o;
  143. foreach ($this->_args as $letter => $arg) {
  144. /* @var Arg $arg */
  145. $this->values[$letter] = false;
  146. if (isset($o[$letter])) {
  147. if (is_bool($o[$letter])) {
  148. // remove from argv copy
  149. $k = array_search("-$letter", $argvCopy);
  150. if ($k !== false) {
  151. array_splice($argvCopy, $k, 1);
  152. }
  153. if ($arg->mustHaveValue) {
  154. $this->addError($letter, "Missing value");
  155. } else {
  156. $this->values[$letter] = true;
  157. }
  158. } else {
  159. // string
  160. $this->values[$letter] = $o[$letter];
  161. $v =& $this->values[$letter];
  162. // remove from argv copy
  163. // first look for -ovalue or -o=value
  164. $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/";
  165. $foundInArgv = false;
  166. foreach ($argvCopy as $k => $argV) {
  167. if (preg_match($pattern, $argV)) {
  168. array_splice($argvCopy, $k, 1);
  169. $foundInArgv = true;
  170. break;
  171. }
  172. }
  173. if (! $foundInArgv) {
  174. // space separated
  175. $k = array_search("-$letter", $argvCopy);
  176. if ($k !== false) {
  177. array_splice($argvCopy, $k, 2);
  178. }
  179. }
  180. // check that value isn't really another option
  181. if (strlen($lettersUsed) > 1) {
  182. $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i";
  183. if (preg_match($pattern, $v)) {
  184. $this->addError($letter, "Value was read as another option: %s", $v);
  185. return false;
  186. }
  187. }
  188. if ($arg->assertFile || $arg->assertDir) {
  189. if ($v[0] !== '/' && $v[0] !== '~') {
  190. $this->values["$letter.raw"] = $v;
  191. $v = getcwd() . "/$v";
  192. }
  193. }
  194. if ($arg->assertFile) {
  195. if ($arg->useAsInfile) {
  196. $this->_stdin = $v;
  197. } elseif ($arg->useAsOutfile) {
  198. $this->_stdout = $v;
  199. }
  200. if ($arg->assertReadable && ! is_readable($v)) {
  201. $this->addError($letter, "File not readable: %s", $v);
  202. continue;
  203. }
  204. if ($arg->assertWritable) {
  205. if (is_file($v)) {
  206. if (! is_writable($v)) {
  207. $this->addError($letter, "File not writable: %s", $v);
  208. }
  209. } else {
  210. if (! is_writable(dirname($v))) {
  211. $this->addError($letter, "Directory not writable: %s", dirname($v));
  212. }
  213. }
  214. }
  215. } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) {
  216. $this->addError($letter, "Directory not readable: %s", $v);
  217. }
  218. }
  219. } else {
  220. if ($arg->isRequired()) {
  221. $this->addError($letter, "Missing");
  222. }
  223. }
  224. }
  225. $this->moreArgs = $argvCopy;
  226. reset($this->moreArgs);
  227. return empty($this->errors);
  228. }
  229. /**
  230. * Get the full paths of file(s) passed in as unspecified arguments
  231. *
  232. * @return array
  233. */
  234. public function getPathArgs()
  235. {
  236. $r = $this->moreArgs;
  237. foreach ($r as $k => $v) {
  238. if ($v[0] !== '/' && $v[0] !== '~') {
  239. $v = getcwd() . "/$v";
  240. $v = str_replace('/./', '/', $v);
  241. do {
  242. $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed);
  243. } while ($changed);
  244. $r[$k] = $v;
  245. }
  246. }
  247. return $r;
  248. }
  249. /**
  250. * Get a short list of errors with options
  251. *
  252. * @return string
  253. */
  254. public function getErrorReport()
  255. {
  256. if (empty($this->errors)) {
  257. return '';
  258. }
  259. $r = "Some arguments did not pass validation:\n";
  260. foreach ($this->errors as $letter => $arr) {
  261. $r .= " $letter : " . implode(', ', $arr) . "\n";
  262. }
  263. $r .= "\n";
  264. return $r;
  265. }
  266. /**
  267. * @return string
  268. */
  269. public function getArgumentsListing()
  270. {
  271. $r = "\n";
  272. foreach ($this->_args as $letter => $arg) {
  273. /* @var Arg $arg */
  274. $desc = $arg->getDescription();
  275. $flag = " -$letter ";
  276. if ($arg->mayHaveValue) {
  277. $flag .= "[VAL]";
  278. } elseif ($arg->mustHaveValue) {
  279. $flag .= "VAL";
  280. }
  281. if ($arg->assertFile) {
  282. $flag = str_replace('VAL', 'FILE', $flag);
  283. } elseif ($arg->assertDir) {
  284. $flag = str_replace('VAL', 'DIR', $flag);
  285. }
  286. if ($arg->isRequired()) {
  287. $desc = "(required) $desc";
  288. }
  289. $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT);
  290. $desc = wordwrap($desc, 70);
  291. $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n";
  292. }
  293. return $r;
  294. }
  295. /**
  296. * Get resource of open input stream. May be STDIN or a file pointer
  297. * to the file specified by an option with 'STDIN'.
  298. *
  299. * @return resource
  300. */
  301. public function openInput()
  302. {
  303. if (null === $this->_stdin) {
  304. return STDIN;
  305. } else {
  306. $this->_stdin = fopen($this->_stdin, 'rb');
  307. return $this->_stdin;
  308. }
  309. }
  310. public function closeInput()
  311. {
  312. if (null !== $this->_stdin) {
  313. fclose($this->_stdin);
  314. }
  315. }
  316. /**
  317. * Get resource of open output stream. May be STDOUT or a file pointer
  318. * to the file specified by an option with 'STDOUT'. The file will be
  319. * truncated to 0 bytes on opening.
  320. *
  321. * @return resource
  322. */
  323. public function openOutput()
  324. {
  325. if (null === $this->_stdout) {
  326. return STDOUT;
  327. } else {
  328. $this->_stdout = fopen($this->_stdout, 'wb');
  329. return $this->_stdout;
  330. }
  331. }
  332. public function closeOutput()
  333. {
  334. if (null !== $this->_stdout) {
  335. fclose($this->_stdout);
  336. }
  337. }
  338. /**
  339. * @param string $letter
  340. * @param string $msg
  341. * @param string $value
  342. */
  343. protected function addError($letter, $msg, $value = null)
  344. {
  345. if ($value !== null) {
  346. $value = var_export($value, 1);
  347. }
  348. $this->errors[$letter][] = sprintf($msg, $value);
  349. }
  350. }