Completed
Push — dev ( 137ae2...762bae )
by James Ekow Abaka
01:17
created

ArgumentParser::addOption()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.0342

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 8
cts 9
cp 0.8889
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 1
crap 5.0342
1
<?php
2
3
namespace clearice\argparser;
4
5
6
/**
7
 * Class ArgumentParser
8
 *
9
 * @package clearice\argparser
10
 */
11
class ArgumentParser
12
{
13
    private $description;
14
15
    private $footer;
16
17
    private $name;
18
19
    private $commands = [];
20
21
    /**
22
     * @var array
23
     */
24
    private $optionsCache = [];
25
26
    /**
27
     * All the possible options for arguments.
28
     * @var array
29
     */
30
    private $options = [];
31
32
    /**
33
     * An instance of the help generator.
34
     * @var HelpMessageGenerator
35
     */
36
    private $helpGenerator;
37
38 8
    public function __construct($helpWriter = null)
39
    {
40 8
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
41 8
    }
42
43
    /**
44
     * Add a value to the available possible options for later parsing.
45
     *
46
     * @param $key
47
     * @param $value
48
     * @throws OptionExistsException
49
     */
50 7
    private function addToOptionCache($key, $value)
51
    {
52 7
        if(!isset($value[$key])) {
53 1
            return;
54
        }
55
        $cacheKey = "${value['command']}${value[$key]}";
56
        if (!isset($this->optionsCache[$cacheKey])) {
57 7
            $this->optionsCache[$cacheKey] = $value;
58
        } else {
59 2
            throw new OptionExistsException(
60 2
                "An argument option with $key {$value['command']} {$value[$key]} already exists."
61
            );
62
        }
63 7
    }
64
65
    /**
66
     * Add an option to be parsed.
67
     * Arguments are presented as a structured array with the following possible keys.
68
     *
69
     *  name: The name of the option prefixed with a double dash --
70
     *  short_name: A shorter single character option prefixed with a single dash -
71
     *  type: Required for all options that take values. An option specified without a type is considered to be a
72
     *        boolean flag.
73
     *  help: A help message for the option
74
     *
75
     * @param $option
76
     * @throws OptionExistsException
77
     * @throws InvalidArgumentDescriptionException
78
     * @throws UnknownCommandException
79
     */
80
    public function addOption($option)
81
    {
82 8
        if (!isset($option['name'])) {
83 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
84
        }
85 7
        if (isset($option['command']) && !isset($this->commands[$option['command']])) {
86
            throw new UnknownCommandException("The command {$option['command']} is unknown");
87 7
        } else if (!isset($option['command'])) {
88 7
            $option['command'] = '';
89
        }
90 7
        $this->options[] = $option;
91 7
        $this->addToOptionCache('name', $option);
92 7
        $this->addToOptionCache('short_name', $option);
93 7
    }
94
95
    /**
96
     * @param $arguments
97
     * @param $argPointer
98
     * @return mixed
99
     * @throws InvalidValueException
100
     */
101
    private function getNextValueOrFail($arguments, &$argPointer, $name)
102
    {
103 4
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
104 2
            $argPointer++;
105 2
            return $arguments[$argPointer];
106
        } else {
107 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
108
        }
109
    }
110
111
    /**
112
     * Parse a long argument that is prefixed with a double dash "--"
113
     *
114
     * @param $arguments
115
     * @param $argPointer
116
     * @return array
117
     * @throws InvalidValueException
118
     */
119
    private function parseLongArgument($command, $arguments, &$argPointer)
120
    {
121 4
        $string = substr($arguments[$argPointer], 2);
122 4
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
123 4
        $name = $command . $matches['name'];
124 4
        $option = $this->optionsCache[$name];
125 4
        $value = true;
126
127 4
        if (isset($option['type'])) {
128 3
            if ($matches['equal'] === '=') {
129 1
                $value = $matches['value'];
130
            } else {
131 3
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
132
            }
133
        }
134
135 3
        return [$option['name'], $this->castType($value, $option['type'] ?? null)];
136
    }
137
138
    /**
139
     * Parse a short argument that is prefixed with a single dash '-'
140
     *
141
     * @param $arguments
142
     * @param $argPointer
143
     * @return array
144
     * @throws InvalidValueException
145
     */
146
    public function parseShortArgument($command, $arguments, &$argPointer)
147
    {
148 2
        $argument = $arguments[$argPointer];
149 2
        $key = $command . substr($argument, 1, 1);
150 2
        $option = $this->optionsCache[$key];
151 2
        $value = true;
152
153 2
        if (isset($option['type'])) {
154 2
            if (substr($argument, 2) != "") {
155 1
                $value = substr($argument, 2);
156
            } else {
157 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
158
            }
159
        }
160
161 1
        return [$option['name'], $this->castType($value, $option['type'] ?? null)];
162
    }
163
164
    private function castType($value, $type)
165
    {
166
        switch ($type) {
167 3
            case 'integer':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
168
                return (int)$value;
169 3
            case 'float':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
170
                return (float)$value;
171
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
172 3
                return $value;
173
        }
174
    }
175
176
    /**
177
     * @param $arguments
178
     * @param $argPointer
179
     * @param $output
180
     * @throws InvalidValueException
181
     */
182
    private function parseArgumentArray($arguments, &$argPointer, &$output)
183
    {
184 5
        $numArguments = count($arguments);
185 5
        $command = $output['__command'] ?? '';
186 5
        for (; $argPointer < $numArguments; $argPointer++) {
187 5
            $arg = $arguments[$argPointer];
188 5
            if (substr($arg, 0, 2) == "--") {
189 4
                $argument = $this->parseLongArgument($command, $arguments, $argPointer);
190 3
                $output[$argument[0]] = $argument[1];
191 2
            } else if ($arg[0] == '-') {
192 2
                $argument = $this->parseShortArgument($command, $arguments, $argPointer);
193 1
                $output[$argument[0]] = $argument[1];
194
            } else {
195 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
196
            }
197
        }
198 3
    }
199
200
    private function maybeShowHelp($name, $output)
201
    {
202 3
        if (isset($output['help']) && $output['help']) {
203 1
            $this->helpGenerator->generate(
204 1
                $name, $output['command'] ?? null,
205 1
                $this->optionsCache, $this->description, $this->footer
206
            );
207
        }
208 3
    }
209
210
    public function parseCommand($arguments, &$argPointer, &$output)
211
    {
212 5
        if (isset($this->commands[$arguments[$argPointer]])) {
213 1
            $output["__command"] = $arguments[$argPointer];
214 1
            $argPointer++;
215
        }
216 5
    }
217
218
    /**
219
     * Parses command line arguments and return a structured array of options and their associated values.
220
     *
221
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
222
     * @return array
223
     * @throws InvalidValueException
224
     */
225
    public function parse($arguments = null)
226
    {
227 5
        global $argv;
228 5
        $arguments = $arguments ?? $argv;
229 5
        $argPointer = 1;
230 5
        $parsed = [];
231 5
        $this->parseCommand($arguments, $argPointer, $parsed);
232 5
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
233 3
        $this->maybeShowHelp($arguments[0], $parsed);
234 3
        return $parsed;
235
    }
236
237
    /**
238
     * @param $name
239
     * @param null $description
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $description is correct as it would always require null to be passed?
Loading history...
240
     * @param null $footer
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $footer is correct as it would always require null to be passed?
Loading history...
241
     * @throws InvalidArgumentDescriptionException
242
     * @throws OptionExistsException
243
     * @throws UnknownCommandException
244
     */
245
    public function enableHelp($name, $description = null, $footer = null)
246
    {
247 1
        $this->name = $name;
248 1
        $this->description = $description;
249 1
        $this->footer = $footer;
250
251 1
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]);
252 1
    }
253
254
    /**
255
     * @param $command
256
     * @throws CommandExistsException
257
     * @throws InvalidArgumentDescriptionException
258
     */
259
    public function addCommand($command)
260
    {
261 1
        if (!isset($command['name'])) {
262
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
263
        }
264 1
        if (isset($this->commands[$command['name']])) {
265
            throw new CommandExistsException("Command ${command['name']} already exists.");
266
        }
267 1
        $this->commands[$command['name']] = $command;
268 1
    }
269
}
270