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