Chris@0: runtimeDir = $runtimeDir; Chris@0: } Chris@0: Chris@0: protected function configure() Chris@0: { Chris@0: $this Chris@0: ->setName('edit') Chris@0: ->setDefinition([ Chris@0: new InputArgument('file', InputArgument::OPTIONAL, 'The file to open for editing. If this is not given, edits a temporary file.', null), Chris@0: new InputOption( Chris@0: 'exec', Chris@0: 'e', Chris@0: InputOption::VALUE_NONE, Chris@0: 'Execute the file content after editing. This is the default when a file name argument is not given.', Chris@0: null Chris@0: ), Chris@0: new InputOption( Chris@0: 'no-exec', Chris@0: 'E', Chris@0: InputOption::VALUE_NONE, Chris@0: 'Do not execute the file content after editing. This is the default when a file name argument is given.', Chris@0: null Chris@0: ), Chris@0: ]) Chris@0: ->setDescription('Open an external editor. Afterwards, get produced code in input buffer.') Chris@0: ->setHelp('Set the EDITOR environment variable to something you\'d like to use.'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param InputInterface $input Chris@0: * @param OutputInterface $output Chris@0: * Chris@0: * @throws \InvalidArgumentException when both exec and no-exec flags are given or if a given variable is not found in the current context Chris@0: * @throws \UnexpectedValueException if file_get_contents on the edited file returns false instead of a string Chris@0: */ Chris@0: protected function execute(InputInterface $input, OutputInterface $output) Chris@0: { Chris@0: if ($input->getOption('exec') && Chris@0: $input->getOption('no-exec')) { Chris@0: throw new \InvalidArgumentException('The --exec and --no-exec flags are mutually exclusive'); Chris@0: } Chris@0: Chris@0: $filePath = $this->extractFilePath($input->getArgument('file')); Chris@0: Chris@0: $execute = $this->shouldExecuteFile( Chris@0: $input->getOption('exec'), Chris@0: $input->getOption('no-exec'), Chris@0: $filePath Chris@0: ); Chris@0: Chris@0: $shouldRemoveFile = false; Chris@0: Chris@0: if ($filePath === null) { Chris@4: $filePath = \tempnam($this->runtimeDir, 'psysh-edit-command'); Chris@0: $shouldRemoveFile = true; Chris@0: } Chris@0: Chris@0: $editedContent = $this->editFile($filePath, $shouldRemoveFile); Chris@0: Chris@0: if ($execute) { Chris@0: $this->getApplication()->addInput($editedContent); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $execOption Chris@0: * @param bool $noExecOption Chris@0: * @param string|null $filePath Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function shouldExecuteFile($execOption, $noExecOption, $filePath) Chris@0: { Chris@0: if ($execOption) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: if ($noExecOption) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // By default, code that is edited is executed if there was no given input file path Chris@0: return $filePath === null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string|null $fileArgument Chris@0: * Chris@0: * @return string|null The file path to edit, null if the input was null, or the value of the referenced variable Chris@0: * Chris@0: * @throws \InvalidArgumentException If the variable is not found in the current context Chris@0: */ Chris@0: private function extractFilePath($fileArgument) Chris@0: { Chris@0: // If the file argument was a variable, get it from the context Chris@0: if ($fileArgument !== null && Chris@4: \strlen($fileArgument) > 0 && Chris@0: $fileArgument[0] === '$') { Chris@4: $fileArgument = $this->context->get(\preg_replace('/^\$/', '', $fileArgument)); Chris@0: } Chris@0: Chris@0: return $fileArgument; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $filePath Chris@0: * @param string $shouldRemoveFile Chris@0: * Chris@0: * @return string Chris@0: * Chris@0: * @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string Chris@0: */ Chris@0: private function editFile($filePath, $shouldRemoveFile) Chris@0: { Chris@4: $escapedFilePath = \escapeshellarg($filePath); Chris@0: Chris@0: $pipes = []; Chris@4: $proc = \proc_open((\getenv('EDITOR') ?: 'nano') . " {$escapedFilePath}", [STDIN, STDOUT, STDERR], $pipes); Chris@4: \proc_close($proc); Chris@0: Chris@4: $editedContent = @\file_get_contents($filePath); Chris@0: Chris@0: if ($shouldRemoveFile) { Chris@4: @\unlink($filePath); Chris@0: } Chris@0: Chris@0: if ($editedContent === false) { Chris@0: throw new \UnexpectedValueException("Reading {$filePath} returned false"); Chris@0: } Chris@0: Chris@0: return $editedContent; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the Context reference. Chris@0: * Chris@0: * @param Context $context Chris@0: */ Chris@0: public function setContext(Context $context) Chris@0: { Chris@0: $this->context = $context; Chris@0: } Chris@0: }