Completed
Push — dev ( 8e5bf0...137ae2 )
by James Ekow Abaka
02:19
created

ArgumentParser::addCommand()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.576

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
ccs 3
cts 5
cp 0.6
rs 9.6666
cc 3
eloc 5
nc 3
nop 1
crap 3.576
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 7
    public function __construct($helpWriter = null)
39
    {
40 7
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
41 7
    }
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 6
    private function addToOptionCache($key, $value)
51
    {
52 6
        if(!isset($value[$key])) {
53 1
            return;
54
        }
55
        $cacheKey = isset($value['command']) ? "${value['command']}:${value[$key]}" : $value[$key];
56
        if (!isset($this->optionsCache[$cacheKey])) {
57 6
            $this->optionsCache[$cacheKey] = $value;
58
        } else {
59 2
            throw new OptionExistsException(
60 2
                "An argument option with $key {$value[$key]} already exists"
61 2
                . (isset($value['command']) ? " for command ${value['command']}." : '.')
62
            );
63
        }
64 6
    }
65
66
    /**
67
     * Add an option to be parsed.
68
     * Arguments are presented as a structured array with the following possible keys.
69
     *
70
     *  name: The name of the option prefixed with a double dash --
71
     *  short_name: A shorter single character option prefixed with a single dash -
72
     *  type: Required for all options that take values. An option specified without a type is considered to be a
73
     *        boolean flag.
74
     *  help: A help message for the option
75
     *
76
     * @param $option
77
     * @throws OptionExistsException
78
     * @throws InvalidArgumentDescriptionException
79
     * @throws UnknownCommandException
80
     */
81
    public function addOption($option)
82
    {
83 7
        if (!isset($option['name'])) {
84 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
85
        }
86 6
        if(isset($option['command']) && !isset($this->commands[$option['command']])) {
87
            throw new UnknownCommandException("The command {$option['command']} is unknown");
88
        }
89 6
        $this->options[] = $option;
90 6
        $this->addToOptionCache('name', $option);
91 6
        $this->addToOptionCache('short_name', $option);
92 6
    }
93
94
    /**
95
     * @param $arguments
96
     * @param $argPointer
97
     * @return mixed
98
     * @throws InvalidValueException
99
     */
100
    private function getNextValueOrFail($arguments, &$argPointer, $name)
101
    {
102 3
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
103 1
            $argPointer++;
104 1
            return $arguments[$argPointer];
105
        } else {
106 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
107
        }
108
    }
109
110
    /**
111
     * Parse a long argument that is prefixed with a double dash "--"
112
     *
113
     * @param $arguments
114
     * @param $argPointer
115
     * @return array
116
     * @throws InvalidValueException
117
     */
118
    private function parseLongArgument($arguments, &$argPointer)
119
    {
120 2
        $string = substr($arguments[$argPointer], 2);
121 2
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
122 2
        $name = $matches['name'];
123 2
        $option = $this->optionsCache[$name];
124 2
        $value = true;
125
126 2
        if (isset($option['type'])) {
127 2
            if ($matches['equal'] === '=') {
128 1
                $value = $matches['value'];
129
            } else {
130 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
131
            }
132
        }
133
134 1
        return [$name, $this->castType($value, $option['type'] ?? null)];
135
    }
136
137
    /**
138
     * Parse a short argument that is prefixed with a single dash '-'
139
     *
140
     * @param $arguments
141
     * @param $argPointer
142
     * @return array
143
     * @throws InvalidValueException
144
     */
145
    public function parseShortArgument($arguments, &$argPointer)
146
    {
147 2
        $argument = $arguments[$argPointer];
148 2
        $key = substr($argument, 1, 1);
149 2
        $option = $this->optionsCache[$key];
150 2
        $value = true;
151
152 2
        if (isset($option['type'])) {
153 2
            if (substr($argument, 2) != "") {
154 1
                $value = substr($argument, 2);
155
            } else {
156 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
157
            }
158
        }
159
160 1
        return [$option['name'], $this->castType($value, $option['type'] ?? null)];
161
    }
162
163
    private function castType($value, $type)
164
    {
165
        switch ($type) {
166 1
            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...
167
                return (int)$value;
168 1
            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...
169
                return (float)$value;
170
            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...
171 1
                return $value;
172
        }
173
    }
174
175
    /**
176
     * @param $arguments
177
     * @param $argPointer
178
     * @param $output
179
     * @throws InvalidValueException
180
     */
181
    private function parseArgumentArray($arguments, &$argPointer, &$output)
182
    {
183 4
        $numArguments = count($arguments);
184 4
        for (; $argPointer < $numArguments; $argPointer++) {
185 3
            $arg = $arguments[$argPointer];
186 3
            if (substr($arg, 0, 2) == "--") {
187 2
                $argument = $this->parseLongArgument($arguments, $argPointer);
188 1
                $output[$argument[0]] = $argument[1];
189 2
            } else if ($arg[0] == '-') {
190 2
                $argument = $this->parseShortArgument($arguments, $argPointer);
191 1
                $output[$argument[0]] = $argument[1];
192
            } else {
193 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
194
            }
195
        }
196 2
    }
197
198
    private function maybeShowHelp($name, $output)
199
    {
200 2
        if (isset($output['help']) && $output['help']) {
201
            $this->helpGenerator->generate($name, $this->optionsCache, $this->description, $this->footer);
0 ignored issues
show
Bug introduced by
The call to clearice\argparser\HelpM...geGenerator::generate() has too few arguments starting with footer. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

201
            $this->helpGenerator->/** @scrutinizer ignore-call */ 
202
                                  generate($name, $this->optionsCache, $this->description, $this->footer);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
202
        }
203 2
    }
204
205
    public function parseCommand($arguments, &$argPointer, &$output)
206
    {
207 4
        if(isset($this->commands[$arguments[$argPointer]])) {
208 1
            $output["__command"] = $arguments[$argPointer];
209 1
            $argPointer++;
210
        }
211 4
    }
212
213
    /**
214
     * Parses command line arguments and return a structured array of options and their associated values.
215
     *
216
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
217
     * @return array
218
     * @throws InvalidValueException
219
     */
220
    public function parse($arguments = null)
221
    {
222 4
        global $argv;
223 4
        $arguments = $arguments ?? $argv;
224 4
        $argPointer = 1;
225 4
        $parsed = [];
226 4
        $this->parseCommand($arguments, $argPointer, $parsed);
227 4
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
228 2
        $this->maybeShowHelp($arguments[0], $parsed);
229 2
        return $parsed;
230
    }
231
232
    /**
233
     * @param $name
234
     * @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...
235
     * @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...
236
     * @throws InvalidArgumentDescriptionException
237
     * @throws OptionExistsException
238
     * @throws UnknownCommandException
239
     */
240
    public function enableHelp($name, $description = null, $footer = null)
241
    {
242
        $this->name = $name;
243
        $this->description = $description;
244
        $this->footer = $footer;
245
246
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]);
247
    }
248
249
    /**
250
     * @param $command
251
     * @throws CommandExistsException
252
     * @throws InvalidArgumentDescriptionException
253
     */
254
    public function addCommand($command)
255
    {
256 1
        if(!isset($command['name'])) {
257
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
258
        }
259 1
        if(isset($this->commands[$command['name']])) {
260
            throw new CommandExistsException("Command ${command['name']} already exists.");
261
        }
262 1
        $this->commands[$command['name']] = $command;
263 1
    }
264
}
265