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 11505 2008-09-13 09:22:23Z 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. public function run($options = null)
  149. {
  150. $this->handleOptions($options);
  151. $arguments = $this->commandManager->getArgumentValues();
  152. $this->currentTask = $this->getTaskToExecute($arguments['task']);
  153. $ret = $this->currentTask->runFromCLI($this->commandManager, $this->commandOptions);
  154. $this->currentTask = null;
  155. return $ret;
  156. }
  157. /**
  158. * Gets the name of the application.
  159. *
  160. * @return string The application name
  161. */
  162. public function getName()
  163. {
  164. return $this->name;
  165. }
  166. /**
  167. * Sets the application name.
  168. *
  169. * @param string $name The application name
  170. */
  171. public function setName($name)
  172. {
  173. $this->name = $name;
  174. }
  175. /**
  176. * Gets the application version.
  177. *
  178. * @return string The application version
  179. */
  180. public function getVersion()
  181. {
  182. return $this->version;
  183. }
  184. /**
  185. * Sets the application version.
  186. *
  187. * @param string $version The application version
  188. */
  189. public function setVersion($version)
  190. {
  191. $this->version = $version;
  192. }
  193. /**
  194. * Returns the long version of the application.
  195. *
  196. * @return string The long application version
  197. */
  198. public function getLongVersion()
  199. {
  200. return sprintf('%s version %s', $this->getName(), $this->formatter->format($this->getVersion(), 'INFO'))."\n";
  201. }
  202. /**
  203. * Returns whether the application must be verbose.
  204. *
  205. * @return Boolean true if the application must be verbose, false otherwise
  206. */
  207. public function isVerbose()
  208. {
  209. return $this->verbose;
  210. }
  211. /**
  212. * Returns whether the application must activate the trace.
  213. *
  214. * @return Boolean true if the application must activate the trace, false otherwise
  215. */
  216. public function withTrace()
  217. {
  218. return $this->trace;
  219. }
  220. /*
  221. * Returns whether the application must run in dry mode.
  222. *
  223. * @return Boolean true if the application must run in dry mode, false otherwise
  224. */
  225. public function isDryrun()
  226. {
  227. return $this->dryrun;
  228. }
  229. /**
  230. * Outputs a help message for the current application.
  231. */
  232. public function help()
  233. {
  234. $messages = array(
  235. $this->formatter->format('Usage:', 'COMMENT'),
  236. sprintf(" %s [options] task_name [arguments]\n", $this->getName()),
  237. $this->formatter->format('Options:', 'COMMENT'),
  238. );
  239. foreach ($this->commandManager->getOptionSet()->getOptions() as $option)
  240. {
  241. $messages[] = sprintf(' %-24s %s %s', $this->formatter->format('--'.$option->getName(), 'INFO'), $this->formatter->format('-'.$option->getShortcut(), 'INFO'), $option->getHelp());
  242. }
  243. $this->dispatcher->notify(new sfEvent($this, 'command.log', $messages));
  244. }
  245. /**
  246. * Parses and handles command line options.
  247. *
  248. * @param mixed $options The command line options
  249. */
  250. protected function handleOptions($options = null)
  251. {
  252. $argumentSet = new sfCommandArgumentSet(array(
  253. new sfCommandArgument('task', sfCommandArgument::REQUIRED, 'The task to execute'),
  254. ));
  255. $optionSet = new sfCommandOptionSet(array(
  256. new sfCommandOption('--dry-run', '-n', sfCommandOption::PARAMETER_NONE, 'Do a dry run without executing actions.'),
  257. new sfCommandOption('--help', '-H', sfCommandOption::PARAMETER_NONE, 'Display this help message.'),
  258. new sfCommandOption('--quiet', '-q', sfCommandOption::PARAMETER_NONE, 'Do not log messages to standard output.'),
  259. new sfCommandOption('--trace', '-t', sfCommandOption::PARAMETER_NONE, 'Turn on invoke/execute tracing, enable full backtrace.'),
  260. new sfCommandOption('--version', '-V', sfCommandOption::PARAMETER_NONE, 'Display the program version.'),
  261. ));
  262. $this->commandManager = new sfCommandManager($argumentSet, $optionSet);
  263. $this->commandManager->process($options);
  264. foreach ($this->commandManager->getOptionValues() as $opt => $value)
  265. {
  266. if (false === $value)
  267. {
  268. continue;
  269. }
  270. switch ($opt)
  271. {
  272. case 'dry-run':
  273. $this->verbose = true;
  274. $this->nowrite = true;
  275. $this->dryrun = true;
  276. $this->trace = true;
  277. break;
  278. case 'help':
  279. $this->help();
  280. exit();
  281. case 'quiet':
  282. $this->verbose = false;
  283. break;
  284. case 'trace':
  285. $this->trace = true;
  286. $this->verbose = true;
  287. break;
  288. case 'version':
  289. echo $this->getLongVersion();
  290. exit(0);
  291. }
  292. }
  293. $this->commandOptions = $options;
  294. }
  295. /**
  296. * Renders an exception.
  297. *
  298. * @param Exception $e An exception object
  299. */
  300. public function renderException($e)
  301. {
  302. $title = sprintf(' [%s] ', get_class($e));
  303. $len = $this->strlen($title);
  304. $lines = array();
  305. foreach (explode("\n", $e->getMessage()) as $line)
  306. {
  307. $lines[] = sprintf(' %s ', $line);
  308. $len = max($this->strlen($line) + 4, $len);
  309. }
  310. $messages = array(str_repeat(' ', $len));
  311. if ($this->trace)
  312. {
  313. $messages[] = $title.str_repeat(' ', $len - $this->strlen($title));
  314. }
  315. foreach ($lines as $line)
  316. {
  317. $messages[] = $line.str_repeat(' ', $len - $this->strlen($line));
  318. }
  319. $messages[] = str_repeat(' ', $len);
  320. fwrite(STDERR, "\n");
  321. foreach ($messages as $message)
  322. {
  323. fwrite(STDERR, $this->formatter->format($message, 'ERROR', STDERR)."\n");
  324. }
  325. fwrite(STDERR, "\n");
  326. if (!is_null($this->currentTask) && $e instanceof sfCommandArgumentsException)
  327. {
  328. fwrite(STDERR, $this->formatter->format(sprintf($this->currentTask->getSynopsis(), $this->getName()), 'INFO', STDERR)."\n");
  329. fwrite(STDERR, "\n");
  330. }
  331. if ($this->trace)
  332. {
  333. fwrite(STDERR, $this->formatter->format("Exception trace:\n", 'COMMENT'));
  334. // exception related properties
  335. $trace = $e->getTrace();
  336. array_unshift($trace, array(
  337. 'function' => '',
  338. 'file' => $e->getFile() != null ? $e->getFile() : 'n/a',
  339. 'line' => $e->getLine() != null ? $e->getLine() : 'n/a',
  340. 'args' => array(),
  341. ));
  342. for ($i = 0, $count = count($trace); $i < $count; $i++)
  343. {
  344. $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
  345. $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
  346. $function = $trace[$i]['function'];
  347. $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
  348. $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
  349. 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)));
  350. }
  351. fwrite(STDERR, "\n");
  352. }
  353. }
  354. /**
  355. * Gets a task from a task name or a shortcut.
  356. *
  357. * @param string $name The task name or a task shortcut
  358. *
  359. * @return sfTask A sfTask object
  360. */
  361. protected function getTaskToExecute($name)
  362. {
  363. // namespace
  364. if (false !== $pos = strpos($name, ':'))
  365. {
  366. $namespace = substr($name, 0, $pos);
  367. $name = substr($name, $pos + 1);
  368. $namespaces = array();
  369. foreach ($this->tasks as $task)
  370. {
  371. if ($task->getNamespace() && !in_array($task->getNamespace(), $namespaces))
  372. {
  373. $namespaces[] = $task->getNamespace();
  374. }
  375. }
  376. $abbrev = $this->getAbbreviations($namespaces);
  377. if (!isset($abbrev[$namespace]))
  378. {
  379. throw new sfCommandException(sprintf('There are no tasks defined in the "%s" namespace.', $namespace));
  380. }
  381. else if (count($abbrev[$namespace]) > 1)
  382. {
  383. throw new sfCommandException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, implode(', ', $abbrev[$namespace])));
  384. }
  385. else
  386. {
  387. $namespace = $abbrev[$namespace][0];
  388. }
  389. }
  390. else
  391. {
  392. $namespace = '';
  393. }
  394. // name
  395. $tasks = array();
  396. foreach ($this->tasks as $taskName => $task)
  397. {
  398. if ($taskName == $task->getFullName() && $task->getNamespace() == $namespace)
  399. {
  400. $tasks[] = $task->getName();
  401. }
  402. }
  403. $abbrev = $this->getAbbreviations($tasks);
  404. if (isset($abbrev[$name]) && count($abbrev[$name]) == 1)
  405. {
  406. return $this->getTask($namespace ? $namespace.':'.$abbrev[$name][0] : $abbrev[$name][0]);
  407. }
  408. // aliases
  409. $aliases = array();
  410. foreach ($this->tasks as $taskName => $task)
  411. {
  412. if ($taskName == $task->getFullName())
  413. {
  414. foreach ($task->getAliases() as $alias)
  415. {
  416. $aliases[] = $alias;
  417. }
  418. }
  419. }
  420. $abbrev = $this->getAbbreviations($aliases);
  421. $fullName = $namespace ? $namespace.':'.$name : $name;
  422. if (!isset($abbrev[$fullName]))
  423. {
  424. throw new sfCommandException(sprintf('Task "%s" is not defined.', $fullName));
  425. }
  426. else if (count($abbrev[$fullName]) > 1)
  427. {
  428. throw new sfCommandException(sprintf('Task "%s" is ambiguous (%s).', $fullName, implode(', ', $abbrev[$fullName])));
  429. }
  430. else
  431. {
  432. return $this->getTask($abbrev[$fullName][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. }