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