Development

/branches/1.4/lib/command/sfCommandApplication.class.php

You must first sign up to be able to contribute.

root/branches/1.4/lib/command/sfCommandApplication.class.php

Revision 33151, 16.7 kB (checked in by fabien, 3 years ago)

[1.4] fixed usage of mb_strlen in tasks (closes #9940)

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