CLI::run()   F
last analyzed

Complexity

Conditions 17
Paths 747

Size

Total Lines 83
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 83
rs 2.4096
c 0
b 0
f 0
cc 17
eloc 51
nc 747
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the ILess
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace ILess;
11
12
use ILess;
13
use ILess\CLI\ANSIColor;
14
use Exception;
15
use InvalidArgumentException;
16
17
/**
18
 * The CLI handler.
19
 */
20
class CLI extends Configurable
21
{
22
    /**
23
     * Maximum line length.
24
     */
25
    const MAX_LINE_LENGTH = 78;
26
27
    /**
28
     * Parsed cli arguments.
29
     *
30
     * @var array
31
     */
32
    protected $cliArguments = [];
33
34
    /**
35
     * Array of default options.
36
     *
37
     * @var array
38
     */
39
    protected $defaultOptions = [
40
        'silent' => false,
41
        'append' => false,
42
        'no_color' => false,
43
    ];
44
45
    /**
46
     * Array of valid options.
47
     *
48
     * @var array
49
     */
50
    protected $validOptions = [
51
        // option name => array(description, array of flags)
52
        'help' => ['Print help (this message) and exit.', ['h']],
53
        'version' => ['Print version number and exit.', ['v']],
54
        'silent' => ['Suppress output of error messages.', ['s']],
55
        'setup_file' => ['Setup file for the parser. Allows to setup custom variables, plugins...', []],
56
        'no_color' => ['Disable colorized output.', []],
57
        'compress' => ['Compress output by removing the whitespace.', ['x']],
58
        'append' => ['Append the generated CSS to the target file?', ['a']],
59
        'no_ie_compat' => ['Disable IE compatibility checks.', []],
60
        'source_map' => ['Outputs an inline sourcemap to the generated CSS (or output to filename.map).', []],
61
        'source_map_url' => ['The complete url and filename put in the less file.', []],
62
        'source_map_base_path' => ['Sets sourcemap base path, defaults to current working directory.', []],
63
        'strict-math' => ['Strict math. Requires brackets.', ['sm']],
64
        'strict-units' => [
65
            'Allows mixed units, e.g. 1px+1em or 1px*1px which have units that cannot be represented.',
66
            ['su'],
67
        ],
68
        'root-path' => [
69
            'Sets rootpath for url rewriting in relative imports and urls. Works with or without the relative-urls option.',
70
            ['rp'],
71
        ],
72
        'relative-urls' => ['Re-writes relative urls to the base less file.', ['ru']],
73
        'url-args' => ['Adds params into url tokens (e.g. 42, cb=42 or a=1&b=2)', []],
74
        'dump_line_numbers' => [
75
            'Outputs filename and line numbers. TYPE can be either \'comments\', which will output the debug info within comments, \'mediaquery\' that will output the information within a fake media query which is compatible with the SASS format, and \'all\' which will do both.',
76
            [],
77
        ],
78
    ];
79
80
    /**
81
     * Array of valid flags.
82
     *
83
     * @var array
84
     */
85
    protected $validFlags = [];
86
87
    /**
88
     * Valid flag.
89
     *
90
     * @var bool
91
     */
92
    protected $isValid = false;
93
94
    /**
95
     * Current script name.
96
     *
97
     * @var string
98
     */
99
    protected $scriptName;
100
101
    /**
102
     * Current directory.
103
     *
104
     * @var string
105
     */
106
    protected $currentDir;
107
108
    /**
109
     * Stdin aliases.
110
     *
111
     * @var array
112
     */
113
    private $stdAliases = [
114
        '−',
115
        '–',
116
        '-',
117
    ];
118
119
    /**
120
     * Constructor.
121
     *
122
     * @param array $cliArguments Array of ILess\CLI arguments ($argv array)
123
     * @param string $currentDir Current directory
124
     */
125
    public function __construct(array $cliArguments, $currentDir = null)
126
    {
127
        $this->scriptName = basename(array_shift($cliArguments));
128
        $this->cliArguments = $this->parseArguments($cliArguments);
129
        $this->currentDir = $currentDir ? $currentDir : getcwd();
130
        parent::__construct($this->convertOptions($this->cliArguments['options']));
131
    }
132
133
    /**
134
     * Setups the ILess\CLI handler.
135
     *
136
     * @throws InvalidArgumentException If there is an error in the arguments
137
     */
138
    protected function setup()
139
    {
140
        // convert flags to options
141
        if ($this->hasFlag('x')) {
142
            $this->setOption('compress', true);
143
        }
144
145
        if ($this->hasFlag('a')) {
146
            $this->setOption('append', true);
147
        }
148
149
        if ($this->hasFlag('s')) {
150
            $this->setOption('silent', true);
151
        }
152
153
        if ($this->hasFlag('v')) {
154
            $this->setOption('version', true);
155
        }
156
157
        if ($this->hasFlag('h')) {
158
            $this->setOption('help', true);
159
        }
160
161
        // the handler is valid when:
162
        // 1) version is requested: --version (option) or -v (flag) is set
163
        // 2) help is requested: --help or -h
164
        // 2) a file to be parsed is present
165
        $this->isValid = count($this->cliArguments['arguments']) || $this->getOption('help') || $this->getOption('version');
166
    }
167
168
    /**
169
     * Converts option names from dash to underscore. Also converts
170
     * less.js command options to ILess valid options.
171
     *
172
     * @param array $options
173
     *
174
     * @return array
175
     */
176
    protected function convertOptions(array $options)
177
    {
178
        $converted = [];
179
        foreach ($options as $option => $value) {
180
            if (strpos($option, '-') !== false) {
181
                $option = str_replace('-', '_', $option);
182
            }
183
184
            switch ($option) {
185
                case 'line_numbers':
186
                    $option = 'dump_line_numbers';
187
                    break;
188
            }
189
190
            $converted[$option] = $value;
191
        }
192
193
        return $converted;
194
    }
195
196
    /**
197
     * Is valid?
198
     *
199
     * @return bool
200
     */
201
    public function isValid()
202
    {
203
        return $this->isValid;
204
    }
205
206
    /**
207
     * Is flag set?
208
     *
209
     * @param string $flag The flag to check
210
     *
211
     * @return bool
212
     */
213
    public function hasFlag($flag)
214
    {
215
        return in_array($flag, $this->cliArguments['flags']);
216
    }
217
218
    /**
219
     * Returns the script name.
220
     *
221
     * @return string
222
     */
223
    public function getScriptName()
224
    {
225
        return $this->scriptName;
226
    }
227
228
    /**
229
     * Returns the current directory.
230
     *
231
     * @return string
232
     */
233
    public function getCurrentDir()
234
    {
235
        return $this->currentDir;
236
    }
237
238
    /**
239
     * Runs the task based on the arguments.
240
     *
241
     * @return int 0 on success, error code on failure
242
     */
243
    public function run()
244
    {
245
        if (!$this->isValid()) {
246
            echo $this->getUsage();
247
248
            // return error
249
            return 1;
250
        } elseif ($this->getOption('version')) {
251
            echo Parser\Core::VERSION . ' [compatible with less.js ' . Parser\Core::LESS_JS_VERSION . ']' . PHP_EOL;
252
253
            return 0;
254
        } elseif ($this->getOption('help')) {
255
            echo $this->getUsage();
256
257
            return 0;
258
        }
259
260
        try {
261
            $toBeParsed = $this->cliArguments['arguments'][0];
262
            $parser = new Parser($this->prepareOptionsForTheParser());
263
264
            $method = null;
265
            // read from stdin
266
            if (in_array($toBeParsed, $this->stdAliases)) {
267
                $toBeParsed = file_get_contents('php://stdin');
268
                $method = 'parseString';
269
            } else {
270
                if (!Util::isPathAbsolute($toBeParsed)) {
271
                    $toBeParsed = sprintf('%s/%s', $this->currentDir, $toBeParsed);
272
                }
273
                $method = 'parseFile';
274
            }
275
276
            // setup file
277
            $setupFile = $this->getOption('setup_file');
278
            if ($setupFile === null) {
279
                $setupFilePath = getcwd() . '/.iless';
280
                if (file_exists($setupFilePath)) {
281
                    if (!is_readable($setupFilePath)) {
282
                        throw new \RuntimeException(sprintf('Setup file "%s" could not be loaded. File does not exist or is not readable.', $setupFile));
283
                    }
284
                    self::loadSetupFile($setupFilePath, $parser);
285
                }
286
            } else {
287
                $setupFilePath = $setupFile;
288
                if (!Util::isPathAbsolute($setupFilePath)) {
289
                    $setupFilePath = getcwd() . '/' . $setupFile;
290
                }
291
                if (!is_readable($setupFilePath)) {
292
                    throw new \RuntimeException(sprintf('Setup file "%s" could not be loaded. File does not exist or is not readable.', $setupFilePath));
293
                }
294
                self::loadSetupFile($setupFilePath, $parser);
295
            }
296
297
            $parser->$method($toBeParsed);
298
299
            $toBeSavedTo = null;
300
            if (isset($this->cliArguments['arguments'][1])) {
301
                $toBeSavedTo = $this->cliArguments['arguments'][1];
302
                if (!Util::isPathAbsolute($toBeSavedTo)) {
303
                    $toBeSavedTo = sprintf('%s/%s', $this->currentDir, $toBeSavedTo);
304
                }
305
            }
306
307
            $css = $parser->getCSS();
308
309
            // where to put the css?
310
            if ($toBeSavedTo) {
311
                // write the result
312
                $this->saveCSS($toBeSavedTo, $css, $this->getOption('append'));
313
            } else {
314
                echo $css;
315
            }
316
        } catch (Exception $e) {
317
            if (!$this->getOption('silent')) {
318
                $this->renderException($e);
319
            }
320
321
            return $e->getCode() ?: 1;
322
        }
323
324
        return 0;
325
    }
326
327
    /**
328
     * Loads setup file.
329
     */
330
    private static function loadSetupFile()
331
    {
332
        // parser will be available as $parser variable, nothing else is declared
333
        $parser = func_get_arg(1);
334
        ob_start();
335
        include func_get_arg(0);
336
        ob_end_clean();
337
    }
338
339
    /**
340
     * Prepares options for the parser.
341
     *
342
     * @return array
343
     */
344
    protected function prepareOptionsForTheParser()
345
    {
346
        $options = [];
347
348
        foreach ($this->getOptions() as $option => $value) {
349
            switch ($option) {
350
                case 'source_map':
351
352
                    $options['source_map'] = true;
353
                    $options['source_map_options'] = [];
354
355
                    if (is_string($value)) {
356
                        $options['source_map_options']['write_to'] = $value;
357
                    }
358
359
                    if ($basePath = $this->getOption('source_map_base_path')) {
360
                        $options['source_map_options']['base_path'] = $basePath;
361
                    }
362
363
                    if ($url = $this->getOption('source_map_url')) {
364
                        $options['source_map_options']['url'] = $url;
365
                    } // same as write to
366
                    elseif (is_string($value)) {
367
                        $options['source_map_options']['url'] = basename($value);
368
                    }
369
370
                    break;
371
372
                // skip options which are processed above or invalid
373
                case 'source_map_base_path':
374
                case 'source_map_url':
375
                case 'silent':
376
                case 'no_color':
377
                case 'append':
378
                case 'setup_file':
379
                    continue 2;
380
381
                case 'no_ie_compat':
382
                    $options['ie_compat'] = false;
383
                    continue 2;
384
385
                // less.js compatibility options
386
                case 'line_numbers':
387
                    $options['dump_line_numbers'] = $value;
388
                    break;
389
390
                default:
391
                    $options[$option] = $value;
392
                    break;
393
            }
394
395
            // all is passed, The context checks if the option is valid
396
            $options[$option] = $value;
397
        }
398
399
        return $options;
400
    }
401
402
    /**
403
     * Saves the generated CSS to a given file.
404
     *
405
     * @param string $targetFile The target file to write to
406
     * @param string $css The css
407
     * @param bool $append Append the CSS?
408
     *
409
     * @return bool|int The number of bytes that were written to the file, or false on failure.
410
     *
411
     * @throws Exception If the file could not be saved
412
     */
413
    protected function saveCss($targetFile, $css, $append = false)
414
    {
415
        if (@file_put_contents($targetFile, $css, $append ? FILE_APPEND | LOCK_EX : LOCK_EX) === false) {
416
            throw new Exception(sprintf('Error while saving the data to "%s".', $targetFile));
417
        }
418
    }
419
420
    /**
421
     * Returns the ILess\CLI usage.
422
     *
423
     * @return string
424
     */
425
    public function getUsage()
426
    {
427
        $options = [];
428
        $max = 0;
429
        foreach ($this->validOptions as $optionName => $properties) {
430
            $optionName = str_replace('_', '-', $optionName);
431
            list($help, $flags) = $properties;
432
            if ($flags) {
433
                $option = sprintf('  -%s, --%s', implode(',-', $flags), $optionName);
434
            } else {
435
                $option = sprintf('  --%s', $optionName);
436
            }
437
438
            // find the largest line
439
            if ((strlen($option) + 2) > $max) {
440
                $max = strlen($option) + 2;
441
            }
442
443
            $options[] = [
444
                $option,
445
                $help,
446
            ];
447
        }
448
449
        $optionsFormatted = [];
450
        foreach ($options as $option) {
451
            list($name, $help) = $option;
452
            // line will be too long
453
            if (strlen($name . $help) + 2 > self::MAX_LINE_LENGTH) {
454
                $help = wordwrap($help, self::MAX_LINE_LENGTH, PHP_EOL . str_repeat(' ', $max + 2));
455
            }
456
            $optionsFormatted[] = sprintf(' %-' . $max . 's %s', $name, $help);
457
        }
458
459
        return strtr('
460
{%signature}
461
462
usage: {%script_name} [option option=parameter ...] source [destination]
463
464
If source is set to `-` (dash or hyphen-minus), input is read from stdin.
465
466
options:
467
{%options}' . PHP_EOL, [
468
            '{%signature}' => $this->getSignature(),
469
            '{%script_name}' => $this->scriptName,
470
            '{%options}' => implode(PHP_EOL, $optionsFormatted),
471
        ]);
472
    }
473
474
    /**
475
     * Returns the signature.
476
     *
477
     * @return string
478
     *
479
     * @link http://patorjk.com/software/taag/#p=display&f=Cyberlarge&t=iless
480
     */
481
    protected function getSignature()
482
    {
483
        return <<<SIGNATURE
484
 _____        _______ _______ _______
485
   |   |      |______ |______ |______
486
 __|__ |_____ |______ ______| ______|
487
SIGNATURE;
488
    }
489
490
    /**
491
     * Renders an exception.
492
     *
493
     * @param Exception $e
494
     */
495
    protected function renderException(Exception $e)
496
    {
497
        $hasColors = $this->detectColors();
498
499
        // excerpt?
500
        if ($e instanceof ILess\Exception\Exception) {
501
            printf("%s: %s\n", $this->scriptName, $hasColors && !$this->getOption('no_color') ?
502
                ANSIColor::colorize($e->toString(false), 'red') : $e->toString(false));
503
504
            if ($excerpt = $e->getExcerpt()) {
505
                $hasColors ?
506
                    printf("%s\n", $excerpt->toTerminal()) :
507
                    printf("%s\n", $excerpt->toText());
508
            }
509
        } else {
510
            printf("%s: %s\n", $this->scriptName,
511
                $hasColors && !$this->getOption('no_color') ? ANSIColor::colorize($e->getMessage(),
512
                    'red') : $e->getMessage());
513
        }
514
    }
515
516
    /**
517
     * Converts the string to plain text.
518
     *
519
     * @return string $string The string
520
     */
521
    protected function toText($string)
522
    {
523
        return strip_tags($string);
524
    }
525
526
    /**
527
     * Does the console support colors?
528
     *
529
     * @return bool
530
     */
531
    protected function detectColors()
532
    {
533
        return getenv('ConEmuANSI') === 'ON' || getenv('ANSICON') !== false ||
534
            (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT));
535
    }
536
537
    /**
538
     * Is silence requested?
539
     *
540
     * @return bool
541
     */
542
    protected function isSilent()
543
    {
544
        return $this->getOption('silent') || in_array('s', $this->cliArguments['flags']);
545
    }
546
547
    /**
548
     * Parses the $argv array to a more useful array.
549
     *
550
     * @param array $args The $argv array
551
     *
552
     * @return array
553
     *
554
     * @link http://php.net/manual/en/features.commandline.php#83843
555
     */
556
    protected function parseArguments($args)
557
    {
558
        $return = [
559
            'arguments' => [],
560
            'flags' => [],
561
            'options' => [],
562
        ];
563
564
        while ($arg = array_shift($args)) {
565
            if (in_array($arg, $this->stdAliases)) {
566
                $return['arguments'][] = $arg;
567
            } // Is it a command? (prefixed with --)
568
            elseif (substr($arg, 0, 2) === '--') {
569
                $value = '';
570
                $command = substr($arg, 2);
571
                // is it the syntax '--option=argument'?
572
                if (strpos($command, '=') !== false) {
573
                    list($command, $value) = explode('=', $command, 2);
574
                }
575
                $return['options'][$command] = !empty($value) ? $this->convertValue($value) : true;
576
            } // Is it a flag or a serial of flags? (prefixed with -)
577
            else {
578
                if (substr($arg, 0, 1) === '-') {
579
                    for ($i = 1; isset($arg[$i]); ++$i) {
580
                        $return['flags'][] = $arg[$i];
581
                    }
582
                } else {
583
                    $return['arguments'][] = $arg;
584
                }
585
            }
586
        }
587
588
        return $return;
589
    }
590
591
    /**
592
     * Converts the value. Parses strings like "false" to boolean false,
593
     * "true" to boolean true.
594
     *
595
     * @param string $value
596
     *
597
     * @return mixed
598
     */
599
    protected function convertValue($value)
600
    {
601
        switch (strtolower($value)) {
602
            case '0':
603
            case 'false':
604
            case 'no':
605
                $value = false;
606
                break;
607
608
            case '1':
609
            case 'true':
610
            case 'yes':
611
                $value = true;
612
                break;
613
        }
614
615
        return $value;
616
    }
617
}
618