Chris@13: colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO; Chris@13: Chris@13: parent::__construct(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * {@inheritdoc} Chris@13: */ Chris@13: protected function configure() Chris@13: { Chris@13: $this Chris@13: ->setName('show') Chris@13: ->setDefinition([ Chris@13: new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'), Chris@13: new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1), Chris@13: ]) Chris@13: ->setDescription('Show the code for an object, class, constant, method or property.') Chris@13: ->setHelp( Chris@13: <<cat --ex defaults to showing the lines surrounding the location of the last Chris@13: exception. Invoking it more than once travels up the exception's stack trace, Chris@13: and providing a number shows the context of the given index of the trace. Chris@13: Chris@13: e.g. Chris@13: >>> show \$myObject Chris@13: >>> show Psy\Shell::debug Chris@13: >>> show --ex Chris@13: >>> show --ex 3 Chris@13: HELP Chris@13: ); Chris@13: } Chris@13: Chris@13: /** Chris@13: * {@inheritdoc} Chris@13: */ Chris@13: protected function execute(InputInterface $input, OutputInterface $output) Chris@13: { Chris@13: // n.b. As far as I can tell, InputInterface doesn't want to tell me Chris@13: // whether an option with an optional value was actually passed. If you Chris@13: // call `$input->getOption('ex')`, it will return the default, both when Chris@13: // `--ex` is specified with no value, and when `--ex` isn't specified at Chris@13: // all. Chris@13: // Chris@13: // So we're doing something sneaky here. If we call `getOptions`, it'll Chris@13: // return the default value when `--ex` is not present, and `null` if Chris@13: // `--ex` is passed with no value. /shrug Chris@13: $opts = $input->getOptions(); Chris@13: Chris@13: // Strict comparison to `1` (the default value) here, because `--ex 1` Chris@13: // will come in as `"1"`. Now we can tell the difference between Chris@13: // "no --ex present", because it's the integer 1, "--ex with no value", Chris@13: // because it's `null`, and "--ex 1", because it's the string "1". Chris@13: if ($opts['ex'] !== 1) { Chris@13: if ($input->getArgument('target')) { Chris@13: throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")'); Chris@13: } Chris@13: Chris@13: return $this->writeExceptionContext($input, $output); Chris@13: } Chris@13: Chris@13: if ($input->getArgument('target')) { Chris@13: return $this->writeCodeContext($input, $output); Chris@13: } Chris@13: Chris@13: throw new RuntimeException('Not enough arguments (missing: "target")'); Chris@13: } Chris@13: Chris@13: private function writeCodeContext(InputInterface $input, OutputInterface $output) Chris@13: { Chris@13: list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target')); Chris@13: Chris@13: // Set some magic local variables Chris@13: $this->setCommandScopeVariables($reflector); Chris@13: Chris@13: try { Chris@13: $output->page(CodeFormatter::format($reflector, $this->colorMode), ShellOutput::OUTPUT_RAW); Chris@13: } catch (RuntimeException $e) { Chris@13: $output->writeln(SignatureFormatter::format($reflector)); Chris@13: throw $e; Chris@13: } Chris@13: } Chris@13: Chris@13: private function writeExceptionContext(InputInterface $input, OutputInterface $output) Chris@13: { Chris@13: $exception = $this->context->getLastException(); Chris@13: if ($exception !== $this->lastException) { Chris@13: $this->lastException = null; Chris@13: $this->lastExceptionIndex = null; Chris@13: } Chris@13: Chris@13: $opts = $input->getOptions(); Chris@13: if ($opts['ex'] === null) { Chris@13: if ($this->lastException && $this->lastExceptionIndex !== null) { Chris@13: $index = $this->lastExceptionIndex + 1; Chris@13: } else { Chris@13: $index = 0; Chris@13: } Chris@13: } else { Chris@17: $index = \max(0, \intval($input->getOption('ex')) - 1); Chris@13: } Chris@13: Chris@13: $trace = $exception->getTrace(); Chris@17: \array_unshift($trace, [ Chris@13: 'file' => $exception->getFile(), Chris@13: 'line' => $exception->getLine(), Chris@13: ]); Chris@13: Chris@17: if ($index >= \count($trace)) { Chris@13: $index = 0; Chris@13: } Chris@13: Chris@13: $this->lastException = $exception; Chris@13: $this->lastExceptionIndex = $index; Chris@13: Chris@13: $output->writeln($this->getApplication()->formatException($exception)); Chris@13: $output->writeln('--'); Chris@13: $this->writeTraceLine($output, $trace, $index); Chris@13: $this->writeTraceCodeSnippet($output, $trace, $index); Chris@13: Chris@13: $this->setCommandScopeVariablesFromContext($trace[$index]); Chris@13: } Chris@13: Chris@13: private function writeTraceLine(OutputInterface $output, array $trace, $index) Chris@13: { Chris@13: $file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a'; Chris@13: $line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a'; Chris@13: Chris@17: $output->writeln(\sprintf( Chris@13: 'From %s:%d at level %d of backtrace (of %d).', Chris@13: OutputFormatter::escape($file), Chris@13: OutputFormatter::escape($line), Chris@13: $index + 1, Chris@17: \count($trace) Chris@13: )); Chris@13: } Chris@13: Chris@13: private function replaceCwd($file) Chris@13: { Chris@17: if ($cwd = \getcwd()) { Chris@17: $cwd = \rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; Chris@13: } Chris@13: Chris@13: if ($cwd === false) { Chris@13: return $file; Chris@13: } else { Chris@17: return \preg_replace('/^' . \preg_quote($cwd, '/') . '/', '', $file); Chris@13: } Chris@13: } Chris@13: Chris@13: private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index) Chris@13: { Chris@13: if (!isset($trace[$index]['file'])) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $file = $trace[$index]['file']; Chris@13: if ($fileAndLine = $this->extractEvalFileAndLine($file)) { Chris@13: list($file, $line) = $fileAndLine; Chris@13: } else { Chris@13: if (!isset($trace[$index]['line'])) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $line = $trace[$index]['line']; Chris@13: } Chris@13: Chris@17: if (\is_file($file)) { Chris@17: $code = @\file_get_contents($file); Chris@13: } Chris@13: Chris@13: if (empty($code)) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $output->write($this->getHighlighter()->getCodeSnippet($code, $line, 5, 5), ShellOutput::OUTPUT_RAW); Chris@13: } Chris@13: Chris@13: private function getHighlighter() Chris@13: { Chris@13: if (!$this->highlighter) { Chris@13: $factory = new ConsoleColorFactory($this->colorMode); Chris@13: $this->highlighter = new Highlighter($factory->getConsoleColor()); Chris@13: } Chris@13: Chris@13: return $this->highlighter; Chris@13: } Chris@13: Chris@13: private function setCommandScopeVariablesFromContext(array $context) Chris@13: { Chris@13: $vars = []; Chris@13: Chris@13: if (isset($context['class'])) { Chris@13: $vars['__class'] = $context['class']; Chris@13: if (isset($context['function'])) { Chris@13: $vars['__method'] = $context['function']; Chris@13: } Chris@13: Chris@13: try { Chris@13: $refl = new \ReflectionClass($context['class']); Chris@13: if ($namespace = $refl->getNamespaceName()) { Chris@13: $vars['__namespace'] = $namespace; Chris@13: } Chris@13: } catch (\Exception $e) { Chris@13: // oh well Chris@13: } Chris@13: } elseif (isset($context['function'])) { Chris@13: $vars['__function'] = $context['function']; Chris@13: Chris@13: try { Chris@13: $refl = new \ReflectionFunction($context['function']); Chris@13: if ($namespace = $refl->getNamespaceName()) { Chris@13: $vars['__namespace'] = $namespace; Chris@13: } Chris@13: } catch (\Exception $e) { Chris@13: // oh well Chris@13: } Chris@13: } Chris@13: Chris@13: if (isset($context['file'])) { Chris@13: $file = $context['file']; Chris@13: if ($fileAndLine = $this->extractEvalFileAndLine($file)) { Chris@13: list($file, $line) = $fileAndLine; Chris@13: } elseif (isset($context['line'])) { Chris@13: $line = $context['line']; Chris@13: } Chris@13: Chris@17: if (\is_file($file)) { Chris@13: $vars['__file'] = $file; Chris@13: if (isset($line)) { Chris@13: $vars['__line'] = $line; Chris@13: } Chris@17: $vars['__dir'] = \dirname($file); Chris@13: } Chris@13: } Chris@13: Chris@13: $this->context->setCommandScopeVariables($vars); Chris@13: } Chris@13: Chris@13: private function extractEvalFileAndLine($file) Chris@13: { Chris@17: if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) { Chris@13: return [$matches[1], $matches[2]]; Chris@13: } Chris@13: } Chris@13: }