ArgvInput::setTokens()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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 Symfony\Component\Console\Input;
13
14
use Symfony\Component\Console\Exception\RuntimeException;
15
16
/**
17
 * ArgvInput represents an input coming from the CLI arguments.
18
 *
19
 * Usage:
20
 *
21
 *     $input = new ArgvInput();
22
 *
23
 * By default, the `$_SERVER['argv']` array is used for the input values.
24
 *
25
 * This can be overridden by explicitly passing the input values in the constructor:
26
 *
27
 *     $input = new ArgvInput($_SERVER['argv']);
28
 *
29
 * If you pass it yourself, don't forget that the first element of the array
30
 * is the name of the running application.
31
 *
32
 * When passing an argument to the constructor, be sure that it respects
33
 * the same rules as the argv one. It's almost always better to use the
34
 * `StringInput` when you want to provide your own input.
35
 *
36
 * @author Fabien Potencier <[email protected]>
37
 *
38
 * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
39
 * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02
40
 */
41
class ArgvInput extends Input
42
{
43
    private $tokens;
44
    private $parsed;
45
46
    public function __construct(array $argv = null, InputDefinition $definition = null)
47
    {
48
        $argv = $argv ?? $_SERVER['argv'] ?? [];
49
50
        // strip the application name
51
        array_shift($argv);
52
53
        $this->tokens = $argv;
54
55
        parent::__construct($definition);
56
    }
57
58
    protected function setTokens(array $tokens)
59
    {
60
        $this->tokens = $tokens;
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    protected function parse()
67
    {
68
        $parseOptions = true;
69
        $this->parsed = $this->tokens;
70
        while (null !== $token = array_shift($this->parsed)) {
71
            if ($parseOptions && '' == $token) {
72
                $this->parseArgument($token);
73
            } elseif ($parseOptions && '--' == $token) {
74
                $parseOptions = false;
75
            } elseif ($parseOptions && 0 === strpos($token, '--')) {
76
                $this->parseLongOption($token);
77
            } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
78
                $this->parseShortOption($token);
79
            } else {
80
                $this->parseArgument($token);
81
            }
82
        }
83
    }
84
85
    /**
86
     * Parses a short option.
87
     */
88
    private function parseShortOption(string $token)
89
    {
90
        $name = substr($token, 1);
91
92
        if (\strlen($name) > 1) {
93
            if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) {
94
                // an option with a value (with no space)
95
                $this->addShortOption($name[0], substr($name, 1));
96
            } else {
97
                $this->parseShortOptionSet($name);
98
            }
99
        } else {
100
            $this->addShortOption($name, null);
101
        }
102
    }
103
104
    /**
105
     * Parses a short option set.
106
     *
107
     * @throws RuntimeException When option given doesn't exist
108
     */
109
    private function parseShortOptionSet(string $name)
110
    {
111
        $len = \strlen($name);
112
        for ($i = 0; $i < $len; ++$i) {
113
            if (!$this->definition->hasShortcut($name[$i])) {
114
                $encoding = mb_detect_encoding($name, null, true);
115
                throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
116
            }
117
118
            $option = $this->definition->getOptionForShortcut($name[$i]);
119
            if ($option->acceptValue()) {
120
                $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
121
122
                break;
123
            } else {
124
                $this->addLongOption($option->getName(), null);
125
            }
126
        }
127
    }
128
129
    /**
130
     * Parses a long option.
131
     */
132
    private function parseLongOption(string $token)
133
    {
134
        $name = substr($token, 2);
135
136
        if (false !== $pos = strpos($name, '=')) {
137
            if (0 === \strlen($value = substr($name, $pos + 1))) {
138
                array_unshift($this->parsed, $value);
139
            }
140
            $this->addLongOption(substr($name, 0, $pos), $value);
141
        } else {
142
            $this->addLongOption($name, null);
143
        }
144
    }
145
146
    /**
147
     * Parses an argument.
148
     *
149
     * @throws RuntimeException When too many arguments are given
150
     */
151
    private function parseArgument(string $token)
152
    {
153
        $c = \count($this->arguments);
154
155
        // if input is expecting another argument, add it
156
        if ($this->definition->hasArgument($c)) {
157
            $arg = $this->definition->getArgument($c);
158
            $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
159
160
        // if last argument isArray(), append token to last argument
161
        } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
162
            $arg = $this->definition->getArgument($c - 1);
163
            $this->arguments[$arg->getName()][] = $token;
164
165
        // unexpected argument
166
        } else {
167
            $all = $this->definition->getArguments();
168
            $symfonyCommandName = null;
169
            if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) {
170
                $symfonyCommandName = $this->arguments['command'] ?? null;
171
                unset($all[$key]);
172
            }
173
174
            if (\count($all)) {
175
                if ($symfonyCommandName) {
176
                    $message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
177
                } else {
178
                    $message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
179
                }
180
            } elseif ($symfonyCommandName) {
181
                $message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
182
            } else {
183
                $message = sprintf('No arguments expected, got "%s".', $token);
184
            }
185
186
            throw new RuntimeException($message);
187
        }
188
    }
189
190
    /**
191
     * Adds a short option value.
192
     *
193
     * @throws RuntimeException When option given doesn't exist
194
     */
195
    private function addShortOption(string $shortcut, $value)
196
    {
197
        if (!$this->definition->hasShortcut($shortcut)) {
198
            throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
199
        }
200
201
        $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
202
    }
203
204
    /**
205
     * Adds a long option value.
206
     *
207
     * @throws RuntimeException When option given doesn't exist
208
     */
209
    private function addLongOption(string $name, $value)
210
    {
211
        if (!$this->definition->hasOption($name)) {
212
            if (!$this->definition->hasNegation($name)) {
213
                throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
214
            }
215
216
            $optionName = $this->definition->negationToName($name);
217
            if (null !== $value) {
218
                throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
219
            }
220
            $this->options[$optionName] = false;
221
222
            return;
223
        }
224
225
        $option = $this->definition->getOption($name);
226
227
        if (null !== $value && !$option->acceptValue()) {
228
            throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
229
        }
230
231
        if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) {
232
            // if option accepts an optional or mandatory argument
233
            // let's see if there is one provided
234
            $next = array_shift($this->parsed);
235
            if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) {
236
                $value = $next;
237
            } else {
238
                array_unshift($this->parsed, $next);
239
            }
240
        }
241
242
        if (null === $value) {
243
            if ($option->isValueRequired()) {
244
                throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name));
245
            }
246
247
            if (!$option->isArray() && !$option->isValueOptional()) {
248
                $value = true;
249
            }
250
        }
251
252
        if ($option->isArray()) {
253
            $this->options[$name][] = $value;
254
        } else {
255
            $this->options[$name] = $value;
256
        }
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function getFirstArgument()
263
    {
264
        $isOption = false;
265
        foreach ($this->tokens as $i => $token) {
266
            if ($token && '-' === $token[0]) {
267
                if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) {
268
                    continue;
269
                }
270
271
                // If it's a long option, consider that everything after "--" is the option name.
272
                // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
273
                $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1);
274
                if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) {
275
                    // noop
276
                } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) {
277
                    $isOption = true;
278
                }
279
280
                continue;
281
            }
282
283
            if ($isOption) {
284
                $isOption = false;
285
                continue;
286
            }
287
288
            return $token;
289
        }
290
291
        return null;
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297
    public function hasParameterOption($values, bool $onlyParams = false)
298
    {
299
        $values = (array) $values;
300
301
        foreach ($this->tokens as $token) {
302
            if ($onlyParams && '--' === $token) {
303
                return false;
304
            }
305
            foreach ($values as $value) {
306
                // Options with values:
307
                //   For long options, test for '--option=' at beginning
308
                //   For short options, test for '-o' at beginning
309
                $leading = 0 === strpos($value, '--') ? $value.'=' : $value;
310
                if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) {
311
                    return true;
312
                }
313
            }
314
        }
315
316
        return false;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    public function getParameterOption($values, $default = false, bool $onlyParams = false)
323
    {
324
        $values = (array) $values;
325
        $tokens = $this->tokens;
326
327
        while (0 < \count($tokens)) {
328
            $token = array_shift($tokens);
329
            if ($onlyParams && '--' === $token) {
330
                return $default;
331
            }
332
333
            foreach ($values as $value) {
334
                if ($token === $value) {
335
                    return array_shift($tokens);
336
                }
337
                // Options with values:
338
                //   For long options, test for '--option=' at beginning
339
                //   For short options, test for '-o' at beginning
340
                $leading = 0 === strpos($value, '--') ? $value.'=' : $value;
341
                if ('' !== $leading && 0 === strpos($token, $leading)) {
342
                    return substr($token, \strlen($leading));
343
                }
344
            }
345
        }
346
347
        return $default;
348
    }
349
350
    /**
351
     * Returns a stringified representation of the args passed to the command.
352
     *
353
     * @return string
354
     */
355
    public function __toString()
356
    {
357
        $tokens = array_map(function ($token) {
358
            if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
359
                return $match[1].$this->escapeToken($match[2]);
360
            }
361
362
            if ($token && '-' !== $token[0]) {
363
                return $this->escapeToken($token);
364
            }
365
366
            return $token;
367
        }, $this->tokens);
368
369
        return implode(' ', $tokens);
370
    }
371
}
372