sfCommandApplication.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. /*
  3. * This file is part of the symfony package.
  4. * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. /**
  10. * sfCommandApplication manages the lifecycle of a CLI application.
  11. *
  12. * @package symfony
  13. * @subpackage command
  14. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  15. * @version SVN: $Id: sfCommandApplication.class.php 19217 2009-06-13 09:15:28Z fabien $
  16. */
  17. abstract class sfCommandApplication
  18. {
  19. protected
  20. $commandManager = null,
  21. $trace = false,
  22. $verbose = true,
  23. $dryrun = false,
  24. $nowrite = false,
  25. $name = 'UNKNOWN',
  26. $version = 'UNKNOWN',
  27. $tasks = array(),
  28. $currentTask = null,
  29. $dispatcher = null,
  30. $options = array(),
  31. $formatter = null;
  32. /**
  33. * Constructor.
  34. *
  35. * @param sfEventDispatcher $dispatcher A sfEventDispatcher instance
  36. * @param sfFormatter $formatter A sfFormatter instance
  37. * @param array $options An array of options
  38. */
  39. public function __construct(sfEventDispatcher $dispatcher, sfFormatter $formatter, $options = array())
  40. {
  41. $this->dispatcher = $dispatcher;
  42. $this->formatter = $formatter;
  43. $this->options = $options;
  44. $this->fixCgi();
  45. $this->configure();
  46. $this->registerTasks();
  47. }
  48. /**
  49. * Configures the current command application.
  50. */
  51. abstract public function configure();
  52. /**
  53. * Returns the value of a given option.
  54. *
  55. * @param string $name The option name
  56. *
  57. * @return mixed The option value
  58. */
  59. public function getOption($name)
  60. {
  61. return isset($this->options[$name]) ? $this->options[$name] : null;
  62. }
  63. /**
  64. * Returns the formatter instance.
  65. *
  66. * @return object The formatter instance
  67. */
  68. public function getFormatter()
  69. {
  70. return $this->formatter;
  71. }
  72. /**
  73. * Registers an array of task objects.
  74. *
  75. * If you pass null, this method will register all available tasks.
  76. *
  77. * @param array $tasks An array of tasks
  78. */
  79. public function registerTasks($tasks = null)
  80. {
  81. if (is_null($tasks))
  82. {
  83. $tasks = array();
  84. foreach (get_declared_classes() as $class)
  85. {
  86. $r = new ReflectionClass($class);
  87. if ($r->isSubclassOf('sfTask') && !$r->isAbstract())
  88. {
  89. $tasks[] = new $class($this->dispatcher, $this->formatter);
  90. }
  91. }
  92. }
  93. foreach ($tasks as $task)
  94. {
  95. $this->registerTask($task);
  96. }
  97. }
  98. /**
  99. * Registers a task object.
  100. *
  101. * @param sfTask $task An sfTask object
  102. */
  103. public function registerTask(sfTask $task)
  104. {
  105. if (isset($this->tasks[$task->getFullName()]))
  106. {
  107. throw new sfCommandException(sprintf('The task named "%s" in "%s" task is already registered by the "%s" task.', $task->getFullName(), get_class($task), get_class($this->tasks[$task->getFullName()])));
  108. }
  109. $this->tasks[$task->getFullName()] = $task;
  110. foreach ($task->getAliases() as $alias)
  111. {
  112. if (isset($this->tasks[$alias]))
  113. {
  114. throw new sfCommandException(sprintf('A task named "%s" is already registered.', $alias));
  115. }
  116. $this->tasks[$alias] = $task;
  117. }
  118. }
  119. /**
  120. * Returns all registered tasks.
  121. *
  122. * @return array An array of sfTask objects
  123. */
  124. public function getTasks()
  125. {
  126. return $this->tasks;
  127. }
  128. /**
  129. * Returns a registered task by name or alias.
  130. *
  131. * @param string $name The task name or alias
  132. *
  133. * @return sfTask An sfTask object
  134. */
  135. public function getTask($name)
  136. {
  137. if (!isset($this->tasks[$name]))
  138. {
  139. throw new sfCommandException(sprintf('The task "%s" does not exist.', $name));
  140. }
  141. return $this->tasks[$name];
  142. }
  143. /**
  144. * Runs the current application.
  145. *
  146. * @param mixed $options The command line options
  147. *
  148. * @return integer 0 if everything went fine, or an error code
  149. */
  150. public function run($options = null)
  151. {
  152. $this->handleOptions($options);
  153. $arguments = $this->commandManager->getArgumentValues();
  154. $this->currentTask = $this->getTaskToExecute($arguments['task']);
  155. $ret = $this->currentTask->runFromCLI($this->commandManager, $this->commandOptions);
  156. $this->currentTask = null;
  157. return $ret;
  158. }
  159. /**
  160. * Gets the name of the application.
  161. *
  162. * @return string The application name
  163. */
  164. public function getName()
  165. {
  166. return $this->name;
  167. }
  168. /**
  169. * Sets the application name.
  170. *
  171. * @param string $name The application name
  172. */
  173. public function setName($name)
  174. {
  175. $this->name = $name;
  176. }
  177. /**
  178. * Gets the application version.
  179. *
  180. * @return string The application version
  181. */
  182. public function getVersion()
  183. {
  184. return $this->version;
  185. }
  186. /**
  187. * Sets the application version.
  188. *
  189. * @param string $version The application version
  190. */
  191. public function setVersion($version)
  192. {
  193. $this->version = $version;
  194. }
  195. /**
  196. * Returns the long version of the application.
  197. *
  198. * @return string The long application version
  199. */
  200. public function getLongVersion()
  201. {
  202. return sprintf('%s version %s', $this->getName(), $this->formatter->format($this->getVersion(), 'INFO'))."\n";
  203. }
  204. /**
  205. * Returns whether the application must be verbose.
  206. *
  207. * @return Boolean true if the application must be verbose, false otherwise
  208. */
  209. public function isVerbose()
  210. {
  211. return $this->verbose;
  212. }
  213. /**
  214. * Returns whether the application must activate the trace.
  215. *
  216. * @return Boolean true if the application must activate the trace, false otherwise
  217. */
  218. public function withTrace()
  219. {
  220. return $this->trace;
  221. }
  222. /*
  223. * Returns whether the application must run in dry mode.
  224. *
  225. * @return Boolean true if the application must run in dry mode, false otherwise
  226. */
  227. public function isDryrun()
  228. {
  229. return $this->dryrun;
  230. }
  231. /**
  232. * Outputs a help message for the current application.
  233. */
  234. public function help()
  235. {
  236. $messages = array(
  237. sprintf("%s [options] task_name [arguments]\n", $this->getName()),
  238. "\nAvailable options:\n",
  239. );
  240. foreach ($this->commandManager->getOptionSet()->getOptions() as $option)
  241. {
  242. $messages[] = sprintf(" %-10s (%s) %s\n", $option->getName(), $option->getShortcut(), $option->getHelp());
  243. }
  244. $this->dispatcher->notify(new sfEvent($this, 'command.log', $messages));
  245. }
  246. /**
  247. * Parses and handles command line options.
  248. *
  249. * @param mixed $options The command line options
  250. */
  251. protected function handleOptions($options = null)
  252. {
  253. $argumentSet = new sfCommandArgumentSet(array(
  254. new sfCommandArgument('task', sfCommandArgument::REQUIRED, 'The task to execute'),
  255. ));
  256. $optionSet = new sfCommandOptionSet(array(
  257. new sfCommandOption('--dry-run', '-n', sfCommandOption::PARAMETER_NONE, 'Do a dry run without executing actions.'),
  258. new sfCommandOption('--help', '-H', sfCommandOption::PARAMETER_NONE, 'Display this help message.'),
  259. new sfCommandOption('--quiet', '-q', sfCommandOption::PARAMETER_NONE, 'Do not log messages to standard output.'),
  260. new sfCommandOption('--trace', '-t', sfCommandOption::PARAMETER_NONE, 'Turn on invoke/execute tracing, enable full backtrace.'),
  261. new sfCommandOption('--version', '-V', sfCommandOption::PARAMETER_NONE, 'Display the program version.'),
  262. ));
  263. $this->commandManager = new sfCommandManager($argumentSet, $optionSet);
  264. $this->commandManager->process($options);
  265. foreach ($this->commandManager->getOptionValues() as $opt => $value)
  266. {
  267. if (false === $value)
  268. {
  269. continue;
  270. }
  271. switch ($opt)
  272. {
  273. case 'dry-run':
  274. $this->verbose = true;
  275. $this->nowrite = true;
  276. $this->dryrun = true;
  277. $this->trace = true;
  278. break;
  279. case 'help':
  280. $this->help();
  281. exit();
  282. case 'quiet':
  283. $this->verbose = false;
  284. break;
  285. case 'trace':
  286. $this->trace = true;
  287. $this->verbose = true;
  288. break;
  289. case 'version':
  290. echo $this->getLongVersion();
  291. exit(0);
  292. }
  293. }
  294. $this->commandOptions = $options;
  295. }
  296. /**
  297. * Renders an exception.
  298. *
  299. * @param Exception $e An exception object
  300. */
  301. public function renderException($e)
  302. {
  303. $title = sprintf(' [%s] ', get_class($e));
  304. $len = $this->strlen($title);
  305. $lines = array();
  306. foreach (explode("\n", $e->getMessage()) as $line)
  307. {
  308. $lines[] = sprintf(' %s ', $line);
  309. $len = max($this->strlen($line) + 4, $len);
  310. }
  311. $messages = array(str_repeat(' ', $len));
  312. if ($this->trace)
  313. {
  314. $messages[] = $title.str_repeat(' ', $len - $this->strlen($title));
  315. }
  316. foreach ($lines as $line)
  317. {
  318. $messages[] = $line.str_repeat(' ', $len - $this->strlen($line));
  319. }
  320. $messages[] = str_repeat(' ', $len);
  321. fwrite(STDERR, "\n");
  322. foreach ($messages as $message)
  323. {
  324. fwrite(STDERR, $this->formatter->format($message, 'ERROR', STDERR)."\n");
  325. }
  326. fwrite(STDERR, "\n");
  327. if (!is_null($this->currentTask) && $e instanceof sfCommandArgumentsException)
  328. {
  329. fwrite(STDERR, $this->formatter->format(sprintf($this->currentTask->getSynopsis(), $this->getName()), 'INFO', STDERR)."\n");
  330. fwrite(STDERR, "\n");
  331. }
  332. if ($this->trace)
  333. {
  334. fwrite(STDERR, $this->formatter->format("Exception trace:\n", 'COMMENT'));
  335. // exception related properties
  336. $trace = $e->getTrace();
  337. array_unshift($trace, array(
  338. 'function' => '',
  339. 'file' => $e->getFile() != null ? $e->getFile() : 'n/a',
  340. 'line' => $e->getLine() != null ? $e->getLine() : 'n/a',
  341. 'args' => array(),
  342. ));
  343. for ($i = 0, $count = count($trace); $i < $count; $i++)
  344. {
  345. $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
  346. $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
  347. $function = $trace[$i]['function'];
  348. $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
  349. $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
  350. fwrite(STDERR, sprintf(" %s%s%s at %s:%s\n", $class, $type, $function, $this->formatter->format($file, 'INFO', STDERR), $this->formatter->format($line, 'INFO', STDERR)));
  351. }
  352. fwrite(STDERR, "\n");
  353. }
  354. }
  355. /**
  356. * Gets a task from a task name or a shortcut.
  357. *
  358. * @param string $name The task name or a task shortcut
  359. *
  360. * @return sfTask A sfTask object
  361. */
  362. protected function getTaskToExecute($name)
  363. {
  364. // namespace
  365. if (false !== $pos = strpos($name, ':'))
  366. {
  367. $namespace = substr($name, 0, $pos);
  368. $name = substr($name, $pos + 1);
  369. $namespaces = array();
  370. foreach ($this->tasks as $task)
  371. {
  372. if ($task->getNamespace() && !in_array($task->getNamespace(), $namespaces))
  373. {
  374. $namespaces[] = $task->getNamespace();
  375. }
  376. }
  377. $abbrev = $this->getAbbreviations($namespaces);
  378. if (!isset($abbrev[$namespace]))
  379. {
  380. throw new sfCommandException(sprintf('There are no tasks defined in the "%s" namespace.', $namespace));
  381. }
  382. else if (count($abbrev[$namespace]) > 1)
  383. {
  384. throw new sfCommandException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, implode(', ', $abbrev[$namespace])));
  385. }
  386. else
  387. {
  388. $namespace = $abbrev[$namespace][0];
  389. }
  390. }
  391. else
  392. {
  393. $namespace = '';
  394. }
  395. // name
  396. $tasks = array();
  397. foreach ($this->tasks as $taskName => $task)
  398. {
  399. if ($taskName == $task->getFullName() && $task->getNamespace() == $namespace)
  400. {
  401. $tasks[] = $task->getName();
  402. }
  403. }
  404. $abbrev = $this->getAbbreviations($tasks);
  405. if (isset($abbrev[$name]) && count($abbrev[$name]) == 1)
  406. {
  407. return $this->getTask($namespace ? $namespace.':'.$abbrev[$name][0] : $abbrev[$name][0]);
  408. }
  409. // aliases
  410. $aliases = array();
  411. foreach ($this->tasks as $taskName => $task)
  412. {
  413. if ($taskName == $task->getFullName())
  414. {
  415. foreach ($task->getAliases() as $alias)
  416. {
  417. $aliases[] = $alias;
  418. }
  419. }
  420. }
  421. $abbrev = $this->getAbbreviations($aliases);
  422. if (!isset($abbrev[$name]))
  423. {
  424. throw new sfCommandException(sprintf('Task "%s" is not defined.', $name));
  425. }
  426. else if (count($abbrev[$name]) > 1)
  427. {
  428. throw new sfCommandException(sprintf('Task "%s" is ambiguous (%s).', $name, implode(', ', $abbrev[$name])));
  429. }
  430. else
  431. {
  432. return $this->getTask($abbrev[$name][0]);
  433. }
  434. }
  435. protected function strlen($string)
  436. {
  437. return function_exists('mb_strlen') ? mb_strlen($string) : strlen($string);
  438. }
  439. /**
  440. * Fixes php behavior if using cgi php.
  441. *
  442. * @see http://www.sitepoint.com/article/php-command-line-1/3
  443. */
  444. protected function fixCgi()
  445. {
  446. // handle output buffering
  447. @ob_end_flush();
  448. ob_implicit_flush(true);
  449. // PHP ini settings
  450. set_time_limit(0);
  451. ini_set('track_errors', true);
  452. ini_set('html_errors', false);
  453. ini_set('magic_quotes_runtime', false);
  454. if (false === strpos(PHP_SAPI, 'cgi'))
  455. {
  456. return;
  457. }
  458. // define stream constants
  459. define('STDIN', fopen('php://stdin', 'r'));
  460. define('STDOUT', fopen('php://stdout', 'w'));
  461. define('STDERR', fopen('php://stderr', 'w'));
  462. // change directory
  463. if (isset($_SERVER['PWD']))
  464. {
  465. chdir($_SERVER['PWD']);
  466. }
  467. // close the streams on script termination
  468. register_shutdown_function(create_function('', 'fclose(STDIN); fclose(STDOUT); fclose(STDERR); return true;'));
  469. }
  470. /**
  471. * Returns an array of possible abbreviations given a set of names.
  472. *
  473. * @see Text::Abbrev perl module for the algorithm
  474. */
  475. protected function getAbbreviations($names)
  476. {
  477. $abbrevs = array();
  478. $table = array();
  479. foreach ($names as $name)
  480. {
  481. for ($len = strlen($name) - 1; $len > 0; --$len)
  482. {
  483. $abbrev = substr($name, 0, $len);
  484. if (!array_key_exists($abbrev, $table))
  485. {
  486. $table[$abbrev] = 1;
  487. }
  488. else
  489. {
  490. ++$table[$abbrev];
  491. }
  492. $seen = $table[$abbrev];
  493. if ($seen == 1)
  494. {
  495. // We're the first word so far to have this abbreviation.
  496. $abbrevs[$abbrev] = array($name);
  497. }
  498. else if ($seen == 2)
  499. {
  500. // We're the second word to have this abbreviation, so we can't use it.
  501. // unset($abbrevs[$abbrev]);
  502. $abbrevs[$abbrev][] = $name;
  503. }
  504. else
  505. {
  506. // We're the third word to have this abbreviation, so skip to the next word.
  507. continue;
  508. }
  509. }
  510. }
  511. // Non-abbreviations always get entered, even if they aren't unique
  512. foreach ($names as $name)
  513. {
  514. $abbrevs[$name] = array($name);
  515. }
  516. return $abbrevs;
  517. }
  518. }