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