PageRenderTime 25ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Cli.php

https://github.com/shaneharter/sheldon
PHP | 330 lines | 190 code | 42 blank | 98 comment | 19 complexity | 503909da50f616350f8458c75fc6d1c0 MD5 | raw file
  1. <?php
  2. namespace Sheldon;
  3. use Zend\Console\Getopt;
  4. /**
  5. * Extend this to build a commandline user-interface with
  6. * 1. Optional Commandline Args
  7. * 2. Required Commandline Args, with enforcement
  8. * 3. Optional Positional Args
  9. * 3. A humanized Usage block with optional detailed man instructions
  10. *
  11. * Uses ZF Getopt class. Proxies unhandled method calls through to the underlying Getopt object.
  12. */
  13. class Cli
  14. {
  15. /**
  16. * @var Getopt
  17. */
  18. protected $cli;
  19. /**
  20. * If your command accepts required parameters, define them here, formatted for the ZendFramework Getopt library.
  21. * You'll have access to passed arguments via the $cli property, or by using the $this->option('name') helper
  22. * @link http://framework.zend.com/manual/1.12/en/zend.console.cli.rules.html
  23. * @var array
  24. */
  25. protected $required = array();
  26. /**
  27. * If your command accepts optional parameters, define them here, formatted for the ZendFramework Getopt library.
  28. * You'll have access to passed arguments via the $cli property, or by using the $this->option('name') helper
  29. * @link http://framework.zend.com/manual/1.12/en/zend.console.cli.rules.html
  30. * @var array
  31. */
  32. protected $optional = array();
  33. /**
  34. * Any built-in, default options. All are optional by design. Formatted for the ZendFramework Getopt library.
  35. * You'll have access to passed arguments via the $cli property, or by using the $this->option('name') helper
  36. * @link http://framework.zend.com/manual/1.12/en/zend.console.cli.rules.html
  37. * @var array
  38. */
  39. protected $builtin = array(
  40. 'help|h' => 'Display Usage Information'
  41. );
  42. /**
  43. * A usage-block-formatted text description of any arguments used by this app that are not defined in the $required
  44. * or $optional array. For example, positional arguments.
  45. * @var Array
  46. */
  47. protected $arguments = array();
  48. /**
  49. * The brief description of the app, as used in the Usage block, etc
  50. * @var
  51. */
  52. protected $description;
  53. /**
  54. * Text instructions printed below options in full usage-block help messages. Each item in the array is a paragraph
  55. * or section. If your array has a string key, it's printed as a section header.
  56. * @example $instructions = array('Interactive Mode' => 'Running in interactive mode blah blah blah...');
  57. * @var array
  58. */
  59. protected $instructions = array();
  60. public function __get($var)
  61. {
  62. if (in_array($var, array('cli', 'required', 'optional', 'description', 'arguments')))
  63. {
  64. return $this->{$var};
  65. }
  66. return null;
  67. }
  68. /**
  69. * Parse a single item from the $optional or $required options arrays into a human readable string.
  70. * @param $option
  71. * @example 'verbose|v-s' to '-v|--verbose [string]'
  72. * @param bool $required Is this a required param?
  73. */
  74. public static function humanize($option, $required = true)
  75. {
  76. $legend = array (
  77. 's' => 'string',
  78. 'w' => 'value',
  79. 'i' => 'integer',
  80. '#' => 'numeric flag',
  81. '-' => '[',
  82. '=' => '<'
  83. );
  84. // Option keys have an optional validatoion indicator that starts with = or -.
  85. @list($keys, $validator) = preg_split('/([-=][swi#])/', $option, -1, PREG_SPLIT_DELIM_CAPTURE);
  86. // Prefix the option key names with the appopriate sigil (- or --)
  87. $option = array();
  88. foreach(explode('|', $keys) as $part)
  89. {
  90. $option[] = (strlen($part) == 1) ? "-{$part}" : "--{$part}";
  91. }
  92. rsort($option);
  93. $option = implode('|', $option);
  94. // Expand simple validator expressions like '-s' into meaningful representations, in that case '[string]'
  95. if ($validator)
  96. {
  97. $validator = strtr($validator, $legend);
  98. $validator = ($validator[0] == '<') ? "{$validator}>" : "{$validator}]";
  99. $option = "{$option} {$validator}";
  100. }
  101. // If it's optional, surround it in square brackets
  102. if ($required)
  103. {
  104. return $option;
  105. }
  106. return "[{$option}]";
  107. }
  108. /**
  109. * Parse a single item from the $optional or $required options arrays into a key we can pass to the Getopt object
  110. * to retreive a specific value. If an option-key has aliases, eg, v|vbr|verbose it'll return the first "longopt"
  111. * it encounters (vbr in this case) but that's fine b/c Getopt handles aliases how you'd expect it to.
  112. * @param $option
  113. * @return null
  114. */
  115. public static function key($option)
  116. {
  117. $option = substr($option, 0, -2);
  118. foreach(explode('|', $option) as $part)
  119. {
  120. if (strlen($part) > 1)
  121. {
  122. return $part;
  123. }
  124. }
  125. return null;
  126. }
  127. /**
  128. * Prints a Usage block
  129. * @param null $message
  130. * @param null $writer
  131. */
  132. public function usage($message = null, StreamWriter $writer = null)
  133. {
  134. $writer = $writer ?: Application::instance()->write;
  135. $message = ($message) ?: $this->description;
  136. $usage = $this->toArray();
  137. if (!$usage['usage'] && !$usage['options'] && !$this->instructions)
  138. {
  139. return;
  140. }
  141. if ($message) $writer($message, '<notice>')->eol();
  142. $writer('Usage: ' . $usage['usage'])->eol();
  143. $writer->column($usage['options'])->eol();
  144. if ($this->instructions)
  145. {
  146. foreach($this->instructions as $header => $value)
  147. {
  148. if (is_string($header))
  149. {
  150. $writer($header, '<header>')->eol();
  151. }
  152. $writer($value)->eol();
  153. }
  154. }
  155. }
  156. /**
  157. * Return a named option.
  158. * @param $key
  159. * @return mixed
  160. */
  161. public function option($key)
  162. {
  163. return $this->cli->getOption($key);
  164. }
  165. /**
  166. * Retrieve a named argument. Only arguments listed in the $arguments property can be accessed this way. For undefined
  167. * arguments, call $this->$cli->getRemainingArgs(). This will match defined args to input args positionally.
  168. * @example The first input arg is matched to the first key in the $arguments array.
  169. * @param $name
  170. */
  171. public function argument($name)
  172. {
  173. $args = $this->cli->getRemainingArgs();
  174. $arg_count = count($args);
  175. $definition = $this->arguments;
  176. $def_count = count($definition);
  177. if ($arg_count < 1)
  178. {
  179. return null;
  180. }
  181. if ($def_count > $arg_count)
  182. {
  183. $definition = array_slice($definition, 0, $arg_count, true);
  184. }
  185. if ($def_count < $arg_count)
  186. {
  187. $args = array_slice($args, 0, $def_count, true);
  188. }
  189. $named_args = array_combine(array_keys($definition), array_values($args));
  190. return @ ($named_args[$name]) ?: null;
  191. }
  192. /**
  193. * Parse the $command and $options properties into a sensible usage message. Returns an associative array, with keys:
  194. * usage = A one-line usage statement
  195. * options = An array of detailed options information including any available text description
  196. * @return Array
  197. */
  198. public function toArray()
  199. {
  200. // The "short" format is displayed in the top-line "Usage: foo [-v|--veee <blah>]" block. The "long" format is
  201. // displayed in the 1-line-per-option help page for the command.
  202. $short_to_long = function($string) {
  203. $string = preg_replace('/^\[([^]]+)\]/', '$1', $string);
  204. $string = str_replace('|', ', ', $string);
  205. return " $string";
  206. };
  207. $arguments = $optional = $required = $combined = array();
  208. foreach($this->arguments as $argument => $description)
  209. {
  210. $string = sprintf('[%s]', ucwords($argument));
  211. $optional[] = $string;
  212. $combined[] = array($short_to_long($string), $description);
  213. }
  214. foreach ($this->required as $option => $description)
  215. {
  216. $string = static::humanize($option, true);
  217. $required[] = $string;
  218. $combined[] = array($short_to_long($string), $description);
  219. }
  220. foreach ($this->optional + $this->builtin as $option => $description)
  221. {
  222. $string = static::humanize($option, false);
  223. $optional[] = $string;
  224. $combined[] = array($short_to_long($string), $description);
  225. }
  226. if ($optional = implode(' ', $optional))
  227. {
  228. $optional = "<optional>{$optional}</optional>";
  229. }
  230. return array (
  231. 'usage' => trim(sprintf('%s %s %s', $this->command, implode(' ', $required), $optional)),
  232. 'options' => $combined
  233. );
  234. }
  235. /**
  236. * Create a new Getopt object and validate the existence of any options defined in the $required property.
  237. * @param null $argv An array or string of commandline arguments. If not provided, Getopt will default to using $_SERVER['argv']
  238. */
  239. protected function setup_cli($argv = null)
  240. {
  241. // If a $cli param was set, treat as dependency injection and use it.
  242. if (!$this->cli)
  243. {
  244. $this->cli = new Getopt(null);
  245. $this->cli->addRules($this->required + $this->optional + $this->builtin);
  246. $this->cli->setOption(Getopt::CONFIG_NUMERIC_FLAGS, true); // Allow flags like -100, eg $ tail -100 output.log
  247. $this->cli->setOption(Getopt::CONFIG_CUMULATIVE_PARAMETERS, true); // Allow array params, eg $ foo -s "key1=val1" -s "key2=val2"
  248. $this->cli->setOption(Getopt::CONFIG_FREEFORM_FLAGS, true); // Allow flags that have not been predefined in the $optional and $required array
  249. }
  250. // If an $argv param was passed, use it. The first value in the array is treated as the command and ignored.
  251. if (!empty($argv))
  252. {
  253. //$argv = (is_array($argv)) ? $argv : explode(' ', $argv);
  254. if (!is_array($argv))
  255. {
  256. // Split the string on a space, maintaining single and double quoted substrings
  257. // For simplicity, this regex includes the quotation marks in the matched string.
  258. // Far more expressive to remove them afterwards than to add a lot of complexity to the regex.
  259. $pattern = "/('.*?'|\".*?\"|\S+)/";
  260. preg_match_all($pattern, $argv, $matches);
  261. $argv = $matches[0];
  262. // Now do the removal of vestigal quote marks. Only strip once.
  263. // For example turn "'hi mom'" into 'hi mom' (not plain old: hi mom)
  264. foreach($argv as &$arg)
  265. {
  266. $orig = $arg;
  267. foreach(array('"', "'") as $mark)
  268. {
  269. $pattern = sprintf('/%1$s(.*)%1$s/', $mark);
  270. $arg = preg_replace($pattern, '$1', $arg);
  271. if ($arg != $orig) break;
  272. }
  273. }
  274. }
  275. $this->cli->setArguments(array_slice($argv, 1));
  276. }
  277. foreach(array_keys($this->required) as $option)
  278. {
  279. $key = $this->key($option);
  280. if (empty($key) || $this->cli->getOption($key) === false)
  281. {
  282. $this->usage("<error>Required Parameter Missing: {$key}</error>");
  283. }
  284. }
  285. }
  286. }