Completed
Push — dev ( f00774...96698a )
by James Ekow Abaka
04:56
created

ArgumentParser::parseCommand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 3
crap 2
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
    private $options = [];
27
28
    /**
29
     * @var HelpMessageGenerator
30
     */
31
    private $helpGenerator;
32
33 8
    public function __construct($helpWriter = null)
34
    {
35 8
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
36 8
    }
37
38
    /**
39
     * Add a value to the available possible options for later parsing.
40
     *
41
     * @param $key
42
     * @param $value
43
     * @throws OptionExistsException
44
     */
45 7
    private function addToOptionCache($key, $value)
46
    {
47 7
        if(!isset($value[$key])) {
48 1
            return;
49
        }
50
        $cacheKey = isset($value['command']) ? "${value['command']}:${value[$key]}" : $value[$key];
51
        if (!isset($this->optionsCache[$cacheKey])) {
52 7
            $this->optionsCache[$cacheKey] = $value;
53
        } else {
54 2
            throw new OptionExistsException(
55 2
                "An argument option with $key {$value[$key]} already exists"
56 2
                . (isset($value['command']) ? " for command ${value['command']}." : '.')
57
            );
58
        }
59 7
    }
60
61
    /**
62
     * Add an option to be parsed.
63
     * Arguments are presented as a structured array with the following possible keys.
64
     *
65
     *  name: The name of the option prefixed with a double dash --
66
     *  short_name: A shorter single character option prefixed with a single dash -
67
     *  type: Required for all options that take values. An option specified without a type is considered to be a
68
     *        boolean flag.
69
     *  help: A help message for the option
70
     *
71
     * @param $option
72
     * @throws OptionExistsException
73
     * @throws InvalidArgumentDescriptionException
74
     * @throws UnknownCommandException
75
     */
76
    public function addOption($option)
77
    {
78 8
        if (!isset($option['name'])) {
79 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
80
        }
81 7
        if(isset($option['command']) && !isset($this->commands[$option['command']])) {
82
            throw new UnknownCommandException("The command {$option['command']} is unknown");
83
        }
84 7
        $this->options[] = $option;
85 7
        $this->addToOptionCache('name', $option);
86 7
        $this->addToOptionCache('short_name', $option);
87 7
    }
88
89
    /**
90
     * @param $arguments
91
     * @param $argPointer
92
     * @return mixed
93
     * @throws InvalidValueException
94
     */
95
    private function getNextValueOrFail($arguments, &$argPointer, $name)
96
    {
97 3
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
98 1
            $argPointer++;
99 1
            return $arguments[$argPointer];
100
        } else {
101 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
102
        }
103
    }
104
105
    /**
106
     * Parse a long argument that is prefixed with a double dash "--"
107
     *
108
     * @param $arguments
109
     * @param $argPointer
110
     * @return array
111
     * @throws InvalidValueException
112
     */
113
    private function parseLongArgument($arguments, &$argPointer)
114
    {
115 3
        $string = substr($arguments[$argPointer], 2);
116 3
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
117 3
        $name = $matches['name'];
118 3
        $option = $this->optionsCache[$name];
119 3
        $value = true;
120
121 3
        if (isset($option['type'])) {
122 2
            if ($matches['equal'] === '=') {
123 1
                $value = $matches['value'];
124
            } else {
125 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
126
            }
127
        }
128
129 2
        return [$name, $this->castType($value, $option['type'] ?? null)];
130
    }
131
132
    /**
133
     * Parse a short argument that is prefixed with a single dash '-'
134
     *
135
     * @param $arguments
136
     * @param $argPointer
137
     * @return array
138
     * @throws InvalidValueException
139
     */
140
    public function parseShortArgument($arguments, &$argPointer)
141
    {
142 2
        $argument = $arguments[$argPointer];
143 2
        $key = substr($argument, 1, 1);
144 2
        $option = $this->optionsCache[$key];
145 2
        $value = true;
146
147 2
        if (isset($option['type'])) {
148 2
            if (substr($argument, 2) != "") {
149 1
                $value = substr($argument, 2);
150
            } else {
151 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
152
            }
153
        }
154
155 1
        return [$option['name'], $this->castType($value, $option['type'] ?? null)];
156
    }
157
158
    private function castType($value, $type)
159
    {
160
        switch ($type) {
161 2
            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...
162
                return (int)$value;
163 2
            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...
164
                return (float)$value;
165
            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...
166 2
                return $value;
167
        }
168
    }
169
170
    /**
171
     * @param $arguments
172
     * @param $argPointer
173
     * @param $output
174
     * @throws InvalidValueException
175
     */
176
    private function parseArgumentArray($arguments, &$argPointer, &$output)
177
    {
178 5
        $numArguments = count($arguments);
179 5
        for (; $argPointer < $numArguments; $argPointer++) {
180 4
            $arg = $arguments[$argPointer];
181 4
            if (substr($arg, 0, 2) == "--") {
182 3
                $argument = $this->parseLongArgument($arguments, $argPointer);
183 2
                $output[$argument[0]] = $argument[1];
184 2
            } else if ($arg[0] == '-') {
185 2
                $argument = $this->parseShortArgument($arguments, $argPointer);
186 1
                $output[$argument[0]] = $argument[1];
187
            } else {
188 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
189
            }
190
        }
191 3
    }
192
193
    private function maybeShowHelp($name, $output)
194
    {
195 3
        if (isset($output['help']) && $output['help']) {
196 1
            $this->helpGenerator->generate($name, $this->optionsCache, $this->description, $this->footer);
197
        }
198 3
    }
199
200
    public function parseCommand($arguments, &$argPointer, &$output)
201
    {
202 5
        if(isset($this->commands[$arguments[$argPointer]])) {
203 1
            $output["__command"] = $arguments[$argPointer];
204 1
            $argPointer++;
205
        }
206 5
    }
207
208
    /**
209
     * Parses command line arguments and return a structured array of options and their associated values.
210
     *
211
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
212
     * @return array
213
     * @throws InvalidValueException
214
     */
215
    public function parse($arguments = null)
216
    {
217 5
        global $argv;
218 5
        $arguments = $arguments ?? $argv;
219 5
        $argPointer = 1;
220 5
        $parsed = [];
221 5
        $this->parseCommand($arguments, $argPointer, $parsed);
222 5
        $this->parseArgumentArray($arguments, $argPointer, $parsed);
223 3
        $this->maybeShowHelp($arguments[0], $parsed);
224 3
        return $parsed;
225
    }
226
227
    /**
228
     * @param $name
229
     * @param null $description
230
     * @param null $footer
231
     * @throws InvalidArgumentDescriptionException
232
     * @throws OptionExistsException
233
     * @throws UnknownCommandException
234
     */
235
    public function enableHelp($name, $description = null, $footer = null)
236
    {
237 1
        $this->name = $name;
238 1
        $this->description = $description;
239 1
        $this->footer = $footer;
240
241 1
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "get help on how to use this app $name"]);
242 1
    }
243
244
    /**
245
     * @param $command
246
     * @throws CommandExistsException
247
     * @throws InvalidArgumentDescriptionException
248
     */
249
    public function addCommand($command)
250
    {
251 1
        if(!isset($command['name'])) {
252
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
253
        }
254 1
        if(isset($this->commands[$command['name']])) {
255
            throw new CommandExistsException("Command ${command['name']} already exists.");
256
        }
257 1
        $this->commands[$command['name']] = $command;
258 1
    }
259
}
260