Passed
Push — dev ( 3cf4c5...d72ebb )
by James Ekow Abaka
02:00
created

ArgumentParser::validateOption()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 4
nc 3
nop 1
crap 4
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 11
    public function __construct($helpWriter = null)
39
    {
40 11
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
41 11
    }
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 10
    private function addToOptionCache($key, $value)
51
    {
52 10
        if (!isset($value[$key])) {
53 1
            return;
54
        }
55
        $cacheKey = "${value['command']}${value[$key]}";
56
        if (!isset($this->optionsCache[$cacheKey])) {
57 10
            $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 10
    }
64
65
    /**
66
     * @param $option
67
     * @throws InvalidArgumentDescriptionException
68
     * @throws UnknownCommandException
69
     */
70
    private function validateOption($option)
71
    {
72 11
        if (!isset($option['name'])) {
73 1
            throw new InvalidArgumentDescriptionException("Argument must have a name");
74
        }
75 10
        if (isset($option['command']) && !isset($this->commands[$option['command']])) {
76 1
            throw new UnknownCommandException("The command {$option['command']} is unknown");
77
        }
78 10
    }
79
80
    /**
81
     * Add an option to be parsed.
82
     * Arguments are presented as a structured array with the following possible keys.
83
     *
84
     *  name: The name of the option prefixed with a double dash --
85
     *  short_name: A shorter single character option prefixed with a single dash -
86
     *  type: Required for all options that take values. An option specified without a type is considered to be a
87
     *        boolean flag.
88
     *  help: A help message for the option
89
     *
90
     * @param $option
91
     * @throws OptionExistsException
92
     * @throws InvalidArgumentDescriptionException
93
     * @throws UnknownCommandException
94
     */
95
    public function addOption($option)
96
    {
97 11
        $this->validateOption($option);
98 10
        if (!isset($option['command'])) {
99 10
            $option['command'] = '';
100
        }
101 10
        $this->options[] = $option;
102 10
        $this->addToOptionCache('name', $option);
103 10
        $this->addToOptionCache('short_name', $option);
104 10
    }
105
106
    /**
107
     * @param $arguments
108
     * @param $argPointer
109
     * @return mixed
110
     * @throws InvalidValueException
111
     */
112
    private function getNextValueOrFail($arguments, &$argPointer, $name)
113
    {
114 4
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
115 2
            $argPointer++;
116 2
            return $arguments[$argPointer];
117
        } else {
118 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
119
        }
120
    }
121
122
    /**
123
     * Parse a long argument that is prefixed with a double dash "--"
124
     *
125
     * @param $arguments
126
     * @param $argPointer
127
     * @return array
128
     * @throws InvalidValueException
129
     */
130
    private function parseLongArgument($command, $arguments, &$argPointer)
131
    {
132 4
        $string = substr($arguments[$argPointer], 2);
133 4
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
134 4
        $name = $command . $matches['name'];
135 4
        $option = $this->optionsCache[$name];
136 4
        $value = true;
137
138 4
        if (isset($option['type'])) {
139 3
            if ($matches['equal'] === '=') {
140 1
                $value = $matches['value'];
141
            } else {
142 3
                $value = $this->getNextValueOrFail($arguments, $argPointer, $name);
143
            }
144
        }
145
146 3
        return [$option['name'], $value];
147
    }
148
149
    /**
150
     * Parse a short argument that is prefixed with a single dash '-'
151
     *
152
     * @param $command
153
     * @param $arguments
154
     * @param $argPointer
155
     * @return array
156
     * @throws InvalidValueException
157
     */
158
    public function parseShortArgument($command, $arguments, &$argPointer)
159
    {
160 2
        $argument = $arguments[$argPointer];
161 2
        $key = $command . substr($argument, 1, 1);
162 2
        $option = $this->optionsCache[$key];
163 2
        $value = true;
164
165 2
        if (isset($option['type'])) {
166 2
            if (substr($argument, 2) != "") {
167 1
                $value = substr($argument, 2);
168
            } else {
169 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
170
            }
171
        }
172
173 1
        return [$option['name'], $value];
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 4
        if (!isset($command['name'])) {
262 1
            throw new InvalidArgumentDescriptionException("Command description must contain a name");
263
        }
264 4
        if (isset($this->commands[$command['name']])) {
265 1
            throw new CommandExistsException("Command ${command['name']} already exists.");
266
        }
267 4
        $this->commands[$command['name']] = $command;
268 4
    }
269
}
270