Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
200 lars 6
 * (c) 2012-2023 Justin Hileman
148 lars 7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Psy\Command;
13
 
14
use Psy\Context;
15
use Psy\ContextAware;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
 
21
class EditCommand extends Command implements ContextAware
22
{
23
    /**
24
     * @var string
25
     */
26
    private $runtimeDir = '';
27
 
28
    /**
29
     * @var Context
30
     */
31
    private $context;
32
 
33
    /**
34
     * Constructor.
35
     *
36
     * @param string      $runtimeDir The directory to use for temporary files
37
     * @param string|null $name       The name of the command; passing null means it must be set in configure()
38
     *
39
     * @throws \Symfony\Component\Console\Exception\LogicException When the command name is empty
40
     */
41
    public function __construct($runtimeDir, $name = null)
42
    {
43
        parent::__construct($name);
44
 
45
        $this->runtimeDir = $runtimeDir;
46
    }
47
 
48
    protected function configure()
49
    {
50
        $this
51
            ->setName('edit')
52
            ->setDefinition([
53
                new InputArgument('file', InputArgument::OPTIONAL, 'The file to open for editing. If this is not given, edits a temporary file.', null),
54
                new InputOption(
55
                    'exec',
56
                    'e',
57
                    InputOption::VALUE_NONE,
58
                    'Execute the file content after editing. This is the default when a file name argument is not given.',
59
                    null
60
                ),
61
                new InputOption(
62
                    'no-exec',
63
                    'E',
64
                    InputOption::VALUE_NONE,
65
                    'Do not execute the file content after editing. This is the default when a file name argument is given.',
66
                    null
67
                ),
68
            ])
69
            ->setDescription('Open an external editor. Afterwards, get produced code in input buffer.')
70
            ->setHelp('Set the EDITOR environment variable to something you\'d like to use.');
71
    }
72
 
73
    /**
74
     * @param InputInterface  $input
75
     * @param OutputInterface $output
76
     *
77
     * @return int 0 if everything went fine, or an exit code
78
     *
79
     * @throws \InvalidArgumentException when both exec and no-exec flags are given or if a given variable is not found in the current context
80
     * @throws \UnexpectedValueException if file_get_contents on the edited file returns false instead of a string
81
     */
82
    protected function execute(InputInterface $input, OutputInterface $output)
83
    {
84
        if ($input->getOption('exec') &&
85
            $input->getOption('no-exec')) {
86
            throw new \InvalidArgumentException('The --exec and --no-exec flags are mutually exclusive');
87
        }
88
 
89
        $filePath = $this->extractFilePath($input->getArgument('file'));
90
 
91
        $execute = $this->shouldExecuteFile(
92
            $input->getOption('exec'),
93
            $input->getOption('no-exec'),
94
            $filePath
95
        );
96
 
97
        $shouldRemoveFile = false;
98
 
99
        if ($filePath === null) {
100
            $filePath = \tempnam($this->runtimeDir, 'psysh-edit-command');
101
            $shouldRemoveFile = true;
102
        }
103
 
104
        $editedContent = $this->editFile($filePath, $shouldRemoveFile);
105
 
106
        if ($execute) {
107
            $this->getApplication()->addInput($editedContent);
108
        }
109
 
110
        return 0;
111
    }
112
 
113
    /**
114
     * @param bool        $execOption
115
     * @param bool        $noExecOption
116
     * @param string|null $filePath
117
     */
118
    private function shouldExecuteFile(bool $execOption, bool $noExecOption, string $filePath = null): bool
119
    {
120
        if ($execOption) {
121
            return true;
122
        }
123
 
124
        if ($noExecOption) {
125
            return false;
126
        }
127
 
128
        // By default, code that is edited is executed if there was no given input file path
129
        return $filePath === null;
130
    }
131
 
132
    /**
133
     * @param string|null $fileArgument
134
     *
135
     * @return string|null The file path to edit, null if the input was null, or the value of the referenced variable
136
     *
137
     * @throws \InvalidArgumentException If the variable is not found in the current context
138
     */
139
    private function extractFilePath(string $fileArgument = null)
140
    {
141
        // If the file argument was a variable, get it from the context
142
        if ($fileArgument !== null &&
143
            $fileArgument !== '' &&
144
            $fileArgument[0] === '$') {
145
            $fileArgument = $this->context->get(\preg_replace('/^\$/', '', $fileArgument));
146
        }
147
 
148
        return $fileArgument;
149
    }
150
 
151
    /**
152
     * @param string $filePath
153
     * @param bool   $shouldRemoveFile
154
     *
155
     * @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string
156
     */
157
    private function editFile(string $filePath, bool $shouldRemoveFile): string
158
    {
159
        $escapedFilePath = \escapeshellarg($filePath);
160
        $editor = (isset($_SERVER['EDITOR']) && $_SERVER['EDITOR']) ? $_SERVER['EDITOR'] : 'nano';
161
 
162
        $pipes = [];
163
        $proc = \proc_open("{$editor} {$escapedFilePath}", [\STDIN, \STDOUT, \STDERR], $pipes);
164
        \proc_close($proc);
165
 
166
        $editedContent = @\file_get_contents($filePath);
167
 
168
        if ($shouldRemoveFile) {
169
            @\unlink($filePath);
170
        }
171
 
172
        if ($editedContent === false) {
173
            throw new \UnexpectedValueException("Reading {$filePath} returned false");
174
        }
175
 
176
        return $editedContent;
177
    }
178
 
179
    /**
180
     * Set the Context reference.
181
     *
182
     * @param Context $context
183
     */
184
    public function setContext(Context $context)
185
    {
186
        $this->context = $context;
187
    }
188
}