Completed
Push — master ( ffac26...9b96f1 )
by Michał
02:32
created

Argv::resolveShortOption()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 22
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 7
nc 3
nop 2
1
<?php namespace nyx\console\input\formats\parsers;
2
3
// Internal dependencies
4
use nyx\console\input\formats\tokens;
5
use nyx\console\input\formats\interfaces;
6
use nyx\console\input\parameter\values;
7
use nyx\console\input\parameter\definitions;
8
use nyx\console\input;
9
10
/**
11
 * Argv to Collections Lexer
12
 *
13
 * Takes an Argv Tokens instance and populates the given Arguments and Options collections based on the
14
 * data contained in it.
15
 *
16
 * @version     0.1.0
17
 * @author      Michal Chojnacki <[email protected]>
18
 * @copyright   2012-2017 Nyx Dev Team
19
 * @link        https://github.com/unyx/nyx
20
 */
21
class Argv implements interfaces\Parser
22
{
23
    /**
24
     * Fill the given Input Arguments and Options based on their definitions and the Argv input given.
25
     *
26
     * @param   values\Arguments    $arguments  The Input Arguments to fill.
27
     * @param   values\Options      $options    The Input Options to fill.
28
     * @param   tokens\Argv         $input      The Argv tokens to be parsed.
29
     */
30
    public function parse(interfaces\Tokens $input, values\Arguments $arguments, values\Options $options) : void
31
    {
32
        $isParsingOptions = true;
33
34
        // Grab all tokens. Can't iterate directly over the Collection as we need to easily determine
35
        // which tokens are left to be processed (thus array_shift() below).
36
        $input = $input->all();
37
38
        // Loop through the input given.
39
        while (null !== $token = array_shift($input)) {
40
41
            if ($isParsingOptions) {
42
                // When '--' is present as a token, it forces us to treat
43
                // everything following it as arguments.
44
                if ('--' === $token) {
45
                    $isParsingOptions = false;
46
                    continue;
47
                }
48
49
                // Full options (token beginning with two hyphens).
50
                if (0 === strpos($token, '--')) {
51
                    $this->parseLongOption($token, $options, $input);
52
                    continue;
53
                }
54
55
                // Shortcuts (token beginning with exactly one hyphen).
56
                if ('-' === $token[0]) {
57
                    $this->parseShortOption($token, $options, $input);
58
                    continue;
59
                }
60
            }
61
62
            // In any other case treat the token as an argument.
63
            $arguments->push($token);
64
        }
65
    }
66
67
    /**
68
     * Parses the given token as a long option and adds it to the Options set.
69
     *
70
     * @param   string          $token      The token to parse.
71
     * @param   values\Options  $options    The collection of Options being filled.
72
     * @param   array&          $input      A reference to the argv input array.
0 ignored issues
show
Documentation introduced by
The doc-type array& could not be parsed: Unknown type name "array&" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
73
     */
74
    protected function parseLongOption(string $token, values\Options $options, array& $input) : void
75
    {
76
        // Remove the two starting hyphens.
77
        $name  = substr($token, 2);
78
        $value = null;
79
80
        // If the token contains a value for the option, we need to split the token accordingly.
81
        if (false !== $pos = strpos($name, '=')) {
82
            $value = substr($name, $pos + 1);
83
            $name  = substr($name, 0, $pos);
84
        }
85
86
        // If the option didn't have a value assigned using the 'equals' sign, the value may be contained in
87
        // the next token, *if* the Option accepts values to begin with.
88
        // Note: It's possible that no Definition for the given Option even exists, but we're deferring
89
        // Exception throwing to parameters\Options::set() to avoid some duplicate code.
90
        /* @var input\Option $definition */
91
        if (!isset($value) && $definition = $options->definitions()->get($name) and $definition->hasValue()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
92
            // $input[0][0] is the first character of the first token in the $input array.
93
            // Ensure the token does not begin with a hyphen, which would indicate it's another Option,
94
            // and not a value for the Option we are processing.
95
            if (isset($input[0]) && '-' !== $input[0][0]) {
96
                $value = array_shift($input);
97
            }
98
        }
99
100
        $options->set($name, $value);
101
    }
102
103
    /**
104
     * Parses the given token as a short option and adds it to the Options set.
105
     *
106
     * @param   string          $token      The token to parse.
107
     * @param   values\Options  $options    The collection of Options being filled.
108
     * @param   array&          $input      A reference to the argv input array.
0 ignored issues
show
Documentation introduced by
The doc-type array& could not be parsed: Unknown type name "array&" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
109
     */
110
    protected function parseShortOption($token, values\Options $options, array& $input) : void
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...
111
    {
112
        // Remove the starting hyphen.
113
        // If it's just a hyphen and nothing else, ignore it since it's most likely a mistype.
114
        if (empty($shortcut = substr($token, 1))) {
115
            return;
116
        }
117
118
        foreach ($this->resolveShortOption($shortcut, $options) as $name => $value) {
119
            $options->set($name, $value);
120
        }
121
    }
122
123
    /**
124
     * Takes a short option or short option set (without the starting hyphen) and resolves it to full options
125
     * and their values, depending on the Definitions given.
126
     *
127
     * @param   string          $shortcut       The short option(s) to resolve.
128
     * @param   values\Options  $options        The Input Options being filled.
129
     * @return  array                           A map of full option $names => $values.
130
     */
131
    private function resolveShortOption(string $shortcut, values\Options $options) : array
132
    {
133
        /* @var $definitions definitions\Options */
134
        $definitions = $options->definitions();
135
136
        // We can return right here if the shortcut is a single character.
137
        if (1 === $length = strlen($shortcut)) {
138
            return [$definitions->getByShortcut($shortcut)->getName() => null];
139
        }
140
141
        // We have more than one character. However, if the first character points to an option that accepts values,
142
        // we will treat all characters afterwards as a value for said option.
143
        if ($definition = $definitions->getByShortcut($shortcut[0]) and $definition->hasValue()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
144
            // First, remove the shortcut from the string to leave us only with the value. Also, if the actual value
145
            // starts with "=", we're going to remove that character (ie. the two first characters instead of just the
146
            // shortcut) to cover bad syntax.
147
            return [$definition->getName() => substr($shortcut, strpos($shortcut, '=') === 1 ? 2 : 1)];
148
        }
149
150
        // At this point consider the whole string as a set of different options.
151
        return $this->resolveShortOptionSet($shortcut, $definitions);
152
    }
153
154
    /**
155
     * Takes a set of short options contained in a string and resolves them to full options and their
156
     * values, depending on the Definitions given.
157
     *
158
     * @param   string                  $shortcut       The short option(s) to resolve.
159
     * @param   definitions\Options     $definitions    The Options Definition.
160
     * @return  array                                   An array of full option $names => $values.
161
     */
162
    private function resolveShortOptionSet(string $shortcut, definitions\Options $definitions) : array
163
    {
164
        $length = strlen($shortcut);
165
        $result = [];
166
167
        // Loop through the string, character by character.
168
        for ($i = 0; $i < $length; $i++) {
169
            $definition = $definitions->getByShortcut($shortcut[$i]);
170
171
            // The last shortcut in a set may have a value appended afterwards.
172
            if ($definition->hasValue()) {
173
                $result[$definition->getName()] = $i === $length - 1 ? null : substr($shortcut, $i + 1);
174
                break;
175
            }
176
177
            $result[$definition->getName()] = true;
178
        }
179
180
        return $result;
181
    }
182
}
183