4 * This file is part of Psy Shell.
6 * (c) 2012-2018 Justin Hileman
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Psy\Command;
14 use JakubOnderka\PhpConsoleHighlighter\Highlighter;
15 use Psy\Configuration;
16 use Psy\ConsoleColorFactory;
17 use Psy\Exception\RuntimeException;
18 use Psy\Formatter\CodeFormatter;
19 use Psy\Formatter\SignatureFormatter;
20 use Psy\Input\CodeArgument;
21 use Psy\Output\ShellOutput;
22 use Symfony\Component\Console\Formatter\OutputFormatter;
23 use Symfony\Component\Console\Input\InputInterface;
24 use Symfony\Component\Console\Input\InputOption;
25 use Symfony\Component\Console\Output\OutputInterface;
28 * Show the code for an object, class, constant, method or property.
30 class ShowCommand extends ReflectingCommand
34 private $lastException;
35 private $lastExceptionIndex;
38 * @param null|string $colorMode (default: null)
40 public function __construct($colorMode = null)
42 $this->colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO;
44 parent::__construct();
50 protected function configure()
55 new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'),
56 new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1),
58 ->setDescription('Show the code for an object, class, constant, method or property.')
61 Show the code for an object, class, constant, method or property, or the context
62 of the last exception.
64 <return>cat --ex</return> defaults to showing the lines surrounding the location of the last
65 exception. Invoking it more than once travels up the exception's stack trace,
66 and providing a number shows the context of the given index of the trace.
69 <return>>>> show \$myObject</return>
70 <return>>>> show Psy\Shell::debug</return>
71 <return>>>> show --ex</return>
72 <return>>>> show --ex 3</return>
80 protected function execute(InputInterface $input, OutputInterface $output)
82 // n.b. As far as I can tell, InputInterface doesn't want to tell me
83 // whether an option with an optional value was actually passed. If you
84 // call `$input->getOption('ex')`, it will return the default, both when
85 // `--ex` is specified with no value, and when `--ex` isn't specified at
88 // So we're doing something sneaky here. If we call `getOptions`, it'll
89 // return the default value when `--ex` is not present, and `null` if
90 // `--ex` is passed with no value. /shrug
91 $opts = $input->getOptions();
93 // Strict comparison to `1` (the default value) here, because `--ex 1`
94 // will come in as `"1"`. Now we can tell the difference between
95 // "no --ex present", because it's the integer 1, "--ex with no value",
96 // because it's `null`, and "--ex 1", because it's the string "1".
97 if ($opts['ex'] !== 1) {
98 if ($input->getArgument('target')) {
99 throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")');
102 return $this->writeExceptionContext($input, $output);
105 if ($input->getArgument('target')) {
106 return $this->writeCodeContext($input, $output);
109 throw new RuntimeException('Not enough arguments (missing: "target")');
112 private function writeCodeContext(InputInterface $input, OutputInterface $output)
114 list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'));
116 // Set some magic local variables
117 $this->setCommandScopeVariables($reflector);
120 $output->page(CodeFormatter::format($reflector, $this->colorMode), ShellOutput::OUTPUT_RAW);
121 } catch (RuntimeException $e) {
122 $output->writeln(SignatureFormatter::format($reflector));
127 private function writeExceptionContext(InputInterface $input, OutputInterface $output)
129 $exception = $this->context->getLastException();
130 if ($exception !== $this->lastException) {
131 $this->lastException = null;
132 $this->lastExceptionIndex = null;
135 $opts = $input->getOptions();
136 if ($opts['ex'] === null) {
137 if ($this->lastException && $this->lastExceptionIndex !== null) {
138 $index = $this->lastExceptionIndex + 1;
143 $index = \max(0, \intval($input->getOption('ex')) - 1);
146 $trace = $exception->getTrace();
147 \array_unshift($trace, [
148 'file' => $exception->getFile(),
149 'line' => $exception->getLine(),
152 if ($index >= \count($trace)) {
156 $this->lastException = $exception;
157 $this->lastExceptionIndex = $index;
159 $output->writeln($this->getApplication()->formatException($exception));
160 $output->writeln('--');
161 $this->writeTraceLine($output, $trace, $index);
162 $this->writeTraceCodeSnippet($output, $trace, $index);
164 $this->setCommandScopeVariablesFromContext($trace[$index]);
167 private function writeTraceLine(OutputInterface $output, array $trace, $index)
169 $file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a';
170 $line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a';
172 $output->writeln(\sprintf(
173 'From <info>%s:%d</info> at <strong>level %d</strong> of backtrace (of %d).',
174 OutputFormatter::escape($file),
175 OutputFormatter::escape($line),
181 private function replaceCwd($file)
183 if ($cwd = \getcwd()) {
184 $cwd = \rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
187 if ($cwd === false) {
190 return \preg_replace('/^' . \preg_quote($cwd, '/') . '/', '', $file);
194 private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index)
196 if (!isset($trace[$index]['file'])) {
200 $file = $trace[$index]['file'];
201 if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
202 list($file, $line) = $fileAndLine;
204 if (!isset($trace[$index]['line'])) {
208 $line = $trace[$index]['line'];
211 if (\is_file($file)) {
212 $code = @\file_get_contents($file);
219 $output->write($this->getHighlighter()->getCodeSnippet($code, $line, 5, 5), ShellOutput::OUTPUT_RAW);
222 private function getHighlighter()
224 if (!$this->highlighter) {
225 $factory = new ConsoleColorFactory($this->colorMode);
226 $this->highlighter = new Highlighter($factory->getConsoleColor());
229 return $this->highlighter;
232 private function setCommandScopeVariablesFromContext(array $context)
236 if (isset($context['class'])) {
237 $vars['__class'] = $context['class'];
238 if (isset($context['function'])) {
239 $vars['__method'] = $context['function'];
243 $refl = new \ReflectionClass($context['class']);
244 if ($namespace = $refl->getNamespaceName()) {
245 $vars['__namespace'] = $namespace;
247 } catch (\Exception $e) {
250 } elseif (isset($context['function'])) {
251 $vars['__function'] = $context['function'];
254 $refl = new \ReflectionFunction($context['function']);
255 if ($namespace = $refl->getNamespaceName()) {
256 $vars['__namespace'] = $namespace;
258 } catch (\Exception $e) {
263 if (isset($context['file'])) {
264 $file = $context['file'];
265 if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
266 list($file, $line) = $fileAndLine;
267 } elseif (isset($context['line'])) {
268 $line = $context['line'];
271 if (\is_file($file)) {
272 $vars['__file'] = $file;
274 $vars['__line'] = $line;
276 $vars['__dir'] = \dirname($file);
280 $this->context->setCommandScopeVariables($vars);
283 private function extractEvalFileAndLine($file)
285 if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
286 return [$matches[1], $matches[2]];