Completed
Push — master ( ad0611...abd676 )
by Tom
09:08 queued 04:30
created

ScriptCommand::_prepareShellCommand()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 16
rs 9.2
cc 4
eloc 9
nc 2
nop 1
1
<?php
2
3
namespace N98\Magento\Command;
4
5
use N98\Magento\Command\AbstractMagentoCommand;
6
use N98\Util\BinaryString;
7
use Symfony\Component\Console\Helper\DialogHelper;
8
use Symfony\Component\Console\Input\InputArgument;
9
use Symfony\Component\Console\Input\InputOption;
10
use Symfony\Component\Console\Input\StringInput;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Output\OutputInterface;
13
14
class ScriptCommand extends AbstractMagentoCommand
15
{
16
    /**
17
     * @var array
18
     */
19
    protected $scriptVars = array();
20
21
    /**
22
     * @var string
23
     */
24
    protected $_scriptFilename = '';
25
26
    /**
27
     * @var bool
28
     */
29
    protected $_stopOnError = false;
30
31 View Code Duplication
    protected function configure()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
32
    {
33
        $this
34
            ->setName('script')
35
            ->addArgument('filename', InputArgument::OPTIONAL, 'Script file')
36
            ->addOption('define', 'd', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Defines a variable')
37
            ->addOption('stop-on-error', null, InputOption::VALUE_NONE, 'Stops execution of script on error')
38
            ->setDescription('Runs multiple n98-magerun commands')
39
        ;
40
41
        $help = <<<HELP
42
Example:
43
44
   # Set multiple config
45
   config:set "web/cookie/cookie_domain" example.com
46
47
   # Set with multiline values with "\n"
48
   config:set "general/store_information/address" "First line\nSecond line\nThird line"
49
50
   # This is a comment
51
   cache:flush
52
53
54
Optionally you can work with unix pipes.
55
56
   \$ echo "cache:flush" | n98-magerun-dev script
57
58
   \$ n98-magerun.phar script < filename
59
60
It is even possible to create executable scripts:
61
62
Create file `test.magerun` and make it executable (`chmod +x test.magerun`):
63
64
   #!/usr/bin/env n98-magerun.phar script
65
66
   config:set "web/cookie/cookie_domain" example.com
67
   cache:flush
68
69
   # Run a shell script with "!" as first char
70
   ! ls -l
71
72
   # Register your own variable (only key = value currently supported)
73
   \${my.var}=bar
74
75
   # Let magerun ask for variable value - add a question mark
76
   \${my.var}=?
77
78
   ! echo \${my.var}
79
80
   # Use resolved variables from n98-magerun in shell commands
81
   ! ls -l \${magento.root}/code/local
82
83
Pre-defined variables:
84
85
* \${magento.root}    -> Magento Root-Folder
86
* \${magento.version} -> Magento Version i.e. 1.7.0.2
87
* \${magento.edition} -> Magento Edition -> Community or Enterprise
88
* \${magerun.version} -> Magerun version i.e. 1.66.0
89
* \${php.version}     -> PHP Version
90
* \${script.file}     -> Current script file path
91
* \${script.dir}      -> Current script file dir
92
93
Variables can be passed to a script with "--define (-d)" option.
94
95
Example:
96
97
   $ n98-magerun.phar script -d foo=bar filename
98
99
   # This will register the variable \${foo} with value bar.
100
101
It's possible to define multiple values by passing more than one option.
102
HELP;
103
        $this->setHelp($help);
104
    }
105
106
    /**
107
     * @return bool
108
     */
109
    public function isEnabled()
110
    {
111
        return function_exists('exec');
112
    }
113
114
    protected function execute(InputInterface $input, OutputInterface $output)
115
    {
116
        $this->_scriptFilename = $input->getArgument('filename');
117
        $this->_stopOnError = $input->getOption('stop-on-error');
118
        $this->_initDefines($input);
119
        $script = $this->_getContent($this->_scriptFilename);
120
        $commands = explode("\n", $script);
121
        $this->initScriptVars();
122
123
        foreach ($commands as $commandString) {
124
            $commandString = trim($commandString);
125
            if (empty($commandString)) {
126
                continue;
127
            }
128
            $firstChar = substr($commandString, 0, 1);
129
130
            switch ($firstChar) {
131
132
                // comment
133
                case '#':
134
                    continue;
135
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
136
137
                // set var
138
                case '$':
139
                    $this->registerVariable($output, $commandString);
140
                    break;
141
142
                // run shell script
143
                case '!':
144
                    $this->runShellCommand($output, $commandString);
145
                    break;
146
147
                default:
148
                    $this->runMagerunCommand($input, $output, $commandString);
149
            }
150
        }
151
    }
152
153
    /**
154
     * @param InputInterface $input
155
     * @throws \InvalidArgumentException
156
     */
157
    protected function _initDefines(InputInterface $input)
158
    {
159
        $defines = $input->getOption('define');
160
        if (is_string($defines)) {
161
            $defines = array($defines);
162
        }
163
        if (count($defines) > 0) {
164
            foreach ($defines as $define) {
0 ignored issues
show
Bug introduced by
The expression $defines of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
165
                if (!strstr($define, '=')) {
166
                    throw new \InvalidArgumentException('Invalid define');
167
                }
168
                $parts = BinaryString::trimExplodeEmpty('=', $define);
169
                $variable = $parts[0];
170
                $value = null;
171
                if (isset($parts[1])) {
172
                    $value = $parts[1];
173
                }
174
                $this->scriptVars['${' . $variable. '}'] = $value;
175
            }
176
        }
177
    }
178
179
    /**
180
     * @param string $filename
181
     * @throws \RuntimeException
182
     * @internal param string $input
183
     * @return string
184
     */
185
    protected function _getContent($filename)
186
    {
187
        if ($filename == '-' || empty($filename)) {
188
            $script = @\file_get_contents('php://stdin', 'r');
189
        } else {
190
            $script = @\file_get_contents($filename);
191
        }
192
193
        if (!$script) {
194
            throw new \RuntimeException('Script file was not found');
195
        }
196
197
        return $script;
198
    }
199
200
    /**
201
     * @param OutputInterface $output
202
     * @param string $commandString
203
     * @throws \RuntimeException
204
     * @return void
205
     */
206
    protected function registerVariable(OutputInterface $output, $commandString)
207
    {
208
        if (preg_match('/^(\$\{[a-zA-Z0-9-_.]+\})=(.+)/', $commandString, $matches)) {
209
            if (isset($matches[2]) && $matches[2][0] == '?') {
210
211
                // Variable is already defined
212
                if (isset($this->scriptVars[$matches[1]])) {
213
                    return $this->scriptVars[$matches[1]];
214
                }
215
216
                $dialog = $this->getHelperSet()->get('dialog'); /* @var $dialog DialogHelper */
217
218
                /**
219
                 * Check for select "?["
220
                 */
221
                if (isset($matches[2][1]) && $matches[2][1] == '[') {
222
                    if (preg_match('/\[(.+)\]/', $matches[2], $choiceMatches)) {
223
                        $choices = BinaryString::trimExplodeEmpty(',', $choiceMatches[1]);
224
                        $selectedIndex = $dialog->select(
225
                            $output,
226
                            '<info>Please enter a value for <comment>' . $matches[1] . '</comment>:</info> ',
227
                            $choices
228
                        );
229
                        $this->scriptVars[$matches[1]] = $choices[$selectedIndex];
230
                    } else {
231
                        throw new \RuntimeException('Invalid choices');
232
                    }
233
                } else {
234
                    // normal input
235
                    $this->scriptVars[$matches[1]] = $dialog->askAndValidate(
236
                        $output,
237
                        '<info>Please enter a value for <comment>' . $matches[1] . '</comment>:</info> ',
238
                        function ($value) {
239
                            if ($value == '') {
240
                                throw new \Exception('Please enter a value');
241
                            }
242
243
                            return $value;
244
                        }
245
                    );
246
                }
247
            } else {
248
                $this->scriptVars[$matches[1]] = $this->_replaceScriptVars($matches[2]);
249
            }
250
        }
251
    }
252
253
    /**
254
     * @param InputInterface $input
255
     * @param OutputInterface $output
256
     * @param $commandString
257
     * @throws \RuntimeException
258
     */
259
    protected function runMagerunCommand(InputInterface $input, OutputInterface $output, $commandString)
0 ignored issues
show
Unused Code introduced by
The parameter $input is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
260
    {
261
        $this->getApplication()->setAutoExit(false);
262
        $commandString = $this->_replaceScriptVars($commandString);
263
        $input = new StringInput($commandString);
264
        $exitCode = $this->getApplication()->run($input, $output);
265
        if ($exitCode !== 0 && $this->_stopOnError) {
266
            throw new \RuntimeException('Script stopped with errors');
267
        }
268
    }
269
270
    /**
271
     * @param string $commandString
272
     * @return mixed|string
273
     */
274
    protected function _prepareShellCommand($commandString)
275
    {
276
        $commandString = ltrim($commandString, '!');
277
278
        // @TODO find a better place
279
        if (strstr($commandString, '${magento.root}')
280
            || strstr($commandString, '${magento.version}')
281
            || strstr($commandString, '${magento.edition}')
282
        ) {
283
            $this->initMagento();
284
        }
285
        $this->initScriptVars();
286
        $commandString = $this->_replaceScriptVars($commandString);
287
288
        return $commandString;
289
    }
290
291
    protected function initScriptVars()
292
    {
293
        $rootFolder = $this->getApplication()->getMagentoRootFolder();
294
        if (!empty($rootFolder)) {
295
            $this->scriptVars['${magento.root}'] = $rootFolder;
296
            $this->scriptVars['${magento.version}'] = \Magento\Framework\AppInterface::VERSION;
297
            $this->scriptVars['${magento.edition}'] = 'Community'; // @TODO replace this if EE is available
298
        }
299
300
        $this->scriptVars['${php.version}']     = substr(phpversion(), 0, strpos(phpversion(), '-'));
301
        $this->scriptVars['${magerun.version}'] = $this->getApplication()->getVersion();
302
        $this->scriptVars['${script.file}'] = $this->_scriptFilename;
303
        $this->scriptVars['${script.dir}'] = dirname($this->_scriptFilename);
304
    }
305
306
    /**
307
     * @param OutputInterface $output
308
     * @param string          $commandString
309
     * @internal param $returnValue
310
     */
311
    protected function runShellCommand(OutputInterface $output, $commandString)
312
    {
313
        $commandString = $this->_prepareShellCommand($commandString);
314
        $returnValue = shell_exec($commandString);
315
        if (!empty($returnValue)) {
316
            $output->writeln($returnValue);
317
        }
318
    }
319
320
    /**
321
     * @param $commandString
322
     * @return mixed
323
     */
324
    protected function _replaceScriptVars($commandString)
325
    {
326
        $commandString = str_replace(array_keys($this->scriptVars), $this->scriptVars, $commandString);
327
328
        return $commandString;
329
    }
330
}
331