Completed
Push — dev ( 07a84c...79d7ba )
by James Ekow Abaka
01:15
created

ArgumentParser::fillInDefaults()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 3
nc 3
nop 1
crap 5
1
<?php
2
3
namespace clearice\argparser;
4
use clearice\utils\ProgramControl;
5
6
7
/**
8
 * Class ArgumentParser
9
 *
10
 * @package clearice\argparser
11
 */
12
class ArgumentParser
13
{
14
    private $description;
15
16
    private $footer;
17
18
    private $name;
19
20
    private $commands = [];
21
22
    /**
23
     * @var array
24
     */
25
    private $optionsCache = [];
26
27
    /**
28
     * All the possible options for arguments.
29
     * @var array
30
     */
31
    private $options = [];
32
33
    /**
34
     * An instance of the help generator.
35
     * @var HelpMessageGenerator
36
     */
37
    private $helpGenerator;
38
39
    private $validator;
40
41
    private $programControl;
42
43
    private $helpEnabled = false;
44
45 14
    public function __construct($helpWriter = null, $programControl = null)
46
    {
47 14
        $this->helpGenerator = $helpWriter ?? new HelpMessageGenerator();
48 14
        $this->programControl = $programControl ?? new ProgramControl();
49 14
        $this->validator = new Validator();
50 14
    }
51
52
    /**
53
     * Add a value to the available possible options for later parsing.
54
     *
55
     * @param string $key
56
     * @param array $value
57
     * @throws OptionExistsException
58
     */
59 13
    private function addToOptionCache(string $key, array $value) : void
60
    {
61 13
        if (!isset($value[$key])) {
62 2
            return;
63
        }
64
        $cacheKey = "${value['command']}${value[$key]}";
65
        if (!isset($this->optionsCache[$cacheKey])) {
66 13
            $this->optionsCache[$cacheKey] = $value;
67
        } else {
68 2
            throw new OptionExistsException(
69 2
                "An argument option with $key {$value['command']} {$value[$key]} already exists."
70
            );
71
        }
72 13
    }
73
74
    /**
75
     * @param string $key
76
     * @param string $name
77
     * @return mixed
78
     * @throws InvalidArgumentException
79
     */
80
    private function retrieveOptionFromCache(string $key, string $name)
81
    {
82 7
        if(!isset($this->optionsCache[$key])) {
83
            throw new InvalidArgumentException("Unknown option '$name'. Please run with `--help` for more information on valid options.");
84
        }
85 7
        return $this->optionsCache[$key];
86
    }
87
88
    /**
89
     * Add an option to be parsed.
90
     * Arguments are presented as a structured array with the following possible keys.
91
     *
92
     *  name: The name of the option prefixed with a double dash --
93
     *  short_name: A shorter single character option prefixed with a single dash -
94
     *  type: Required for all options that take values. An option specified without a type is considered to be a
95
     *        boolean flag.
96
     *  repeats: A boolean value that states whether the option can be repeated or not. Repeatable options are returned
97
     *        as arrays.
98
     *  default: A default value for the option.
99
     *  help: A help message for the option
100
     *
101
     * @param array $option
102
     * @throws OptionExistsException
103
     * @throws InvalidArgumentDescriptionException
104
     * @throws UnknownCommandException
105
     */
106
    public function addOption(array $option): void
107
    {
108 14
        $this->validator->validateOption($option, $this->commands);
109 13
        $option['command'] = $option['command'] ?? '';
110 13
        $option['repeats'] = $option['repeats'] ?? false;
111 13
        $this->options[] = $option;
112 13
        $this->addToOptionCache('name', $option);
113 13
        $this->addToOptionCache('short_name', $option);
114 13
    }
115
116
    /**
117
     * @param $arguments
118
     * @param $argPointer
119
     * @return mixed
120
     * @throws InvalidValueException
121
     */
122
    private function getNextValueOrFail($arguments, &$argPointer, $name)
123
    {
124 5
        if (isset($arguments[$argPointer + 1]) && $arguments[$argPointer + 1][0] != '-') {
125 3
            $argPointer++;
126 3
            return $arguments[$argPointer];
127
        } else {
128 2
            throw new InvalidValueException("A value must be passed along with argument $name.");
129
        }
130
    }
131
132
    /**
133
     * Parse a long argument that is prefixed with a double dash "--"
134
     *
135
     * @param $arguments
136
     * @param $argPointer
137
     * @throws InvalidValueException
138
     * @throws InvalidArgumentException
139
     */
140
    private function parseLongArgument($command, $arguments, &$argPointer, &$output)
141
    {
142 5
        $string = substr($arguments[$argPointer], 2);
143 5
        preg_match("/(?<name>[a-zA-Z_0-9-]+)(?<equal>=?)(?<value>.*)/", $string, $matches);
144 5
        $key = $command . $matches['name'];
145 5
        $option = $this->retrieveOptionFromCache($key, $matches['name']);
146 5
        $value = true;
147
148 5
        if (isset($option['type'])) {
149 4
            if ($matches['equal'] === '=') {
150 1
                $value = $matches['value'];
151
            } else {
152 4
                $value = $this->getNextValueOrFail($arguments, $argPointer, $matches['name']);
153
            }
154
        }
155
156 4
        $this->assignValue($option, $output, $option['name'], $value);
157 4
    }
158
159
    /**
160
     * Parse a short argument that is prefixed with a single dash '-'
161
     *
162
     * @param $command
163
     * @param $arguments
164
     * @param $argPointer
165
     * @throws InvalidValueException
166
     * @throws InvalidArgumentException
167
     */
168
    private function parseShortArgument($command, $arguments, &$argPointer, &$output)
169
    {
170 3
        $argument = $arguments[$argPointer];
171 3
        $key = $command . substr($argument, 1, 1);
172 3
        $option = $this->retrieveOptionFromCache($key, substr($argument, 1, 1));
173 3
        $value = true;
174
175 3
        if (isset($option['type'])) {
176 3
            if (substr($argument, 2) != "") {
177 2
                $value = substr($argument, 2);
178
            } else {
179 2
                $value = $this->getNextValueOrFail($arguments, $argPointer, $option['name']);
180
            }
181
        }
182
183 2
        $this->assignValue($option, $output, $option['name'], $value);
184 2
    }
185
186
    private function assignValue($option, &$output, $key, $value)
187
    {
188 5
        if($option['repeats']) {
189 1
            $output[$key] = isset($output[$key]) ? array_merge($output[$key], [$value]) : [$value];
190
        } else {
191 4
            $output[$key] = $value;
192
        }
193 5
    }
194
195
    /**
196
     * @param $arguments
197
     * @param $argPointer
198
     * @param $output
199
     * @throws InvalidValueException
200
     * @throws InvalidArgumentException
201
     */
202
    private function parseArgumentArray($arguments, &$argPointer, &$output)
203
    {
204 7
        $numArguments = count($arguments);
205 7
        $command = $output['__command'] ?? '';
206 7
        for (; $argPointer < $numArguments; $argPointer++) {
207 7
            $arg = $arguments[$argPointer];
208 7
            if (substr($arg, 0, 2) == "--") {
209 5
                $this->parseLongArgument($command, $arguments, $argPointer, $output);
210 3
            } else if ($arg[0] == '-') {
211 3
                $this->parseShortArgument($command, $arguments, $argPointer, $output);
212
            } else {
213 1
                $output['__args'] = isset($output['__args']) ? array_merge($output['__args'], [$arg]) : [$arg];
214
            }
215
        }
216 5
    }
217
218
    /**
219
     * @param $output
220
     * @throws HelpMessageRequestedException
221
     */
222
    private function maybeShowHelp($output)
223
    {
224 5
        if ((isset($output['help']) && $output['help'] && $this->helpEnabled)) {
225 1
            print $this->getHelpMessage($output['__command'] ?? null);
226 1
            throw new HelpMessageRequestedException();
227
        }
228 4
    }
229
230
    private function parseCommand($arguments, &$argPointer, &$output)
231
    {
232 7
        if (count($arguments) > 1 && isset($this->commands[$arguments[$argPointer]])) {
233 1
            $output["__command"] = $arguments[$argPointer];
234 1
            $argPointer++;
235
        }
236 7
    }
237
238
    /**
239
     * @param $parsed
240
     * @throws InvalidArgumentException
241
     */
242
    private function fillInDefaults(&$parsed)
243
    {
244 4
        foreach($this->options as $option) {
245 4
            if(!isset($parsed[$option['name']]) && isset($option['default']) && $option['command'] == ($parsed['__command'] ?? "")) {
246 4
                $parsed[$option['name']] = $option['default'];
247
            }
248
        }
249 4
    }
250
251
    /**
252
     * Parses command line arguments and return a structured array of options and their associated values.
253
     *
254
     * @param array $arguments An optional array of arguments that would be parsed instead of those passed to the CLI.
255
     * @return array
256
     * @throws InvalidValueException
257
     */
258
    public function parse($arguments = null)
259
    {
260
        try{
261 7
            global $argv;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
262 7
            $arguments = $arguments ?? $argv;
263 7
            $argPointer = 1;
264 7
            $parsed = [];
265 7
            $this->name = $this->name ?? $arguments[0];
266 7
            $this->parseCommand($arguments, $argPointer, $parsed);
267 7
            $this->parseArgumentArray($arguments, $argPointer, $parsed);
268 5
            $this->maybeShowHelp($parsed);
269 4
            $this->validator->validateArguments($this->options, $parsed);
270 4
            $this->fillInDefaults($parsed);
271 4
            return $parsed;
272 3
        } catch (HelpMessageRequestedException $exception) {
273 1
            $this->programControl->quit();
274 2
        } catch (InvalidArgumentException $exception) {
275
            print $exception->getMessage() . PHP_EOL;
276
            $this->programControl->quit();
277
        }
278 1
    }
279
280
    /**
281
     * Enables help messages so they show automatically.
282
     *
283
     * @param string $name
284
     * @param string $description
285
     * @param string $footer
286
     *
287
     * @throws InvalidArgumentDescriptionException
288
     * @throws OptionExistsException
289
     * @throws UnknownCommandException
290
     */
291
    public function enableHelp(string $description = null, string $footer = null, string $name = null) : void
292
    {
293 2
        $this->name = $name;
294 2
        $this->description = $description;
295 2
        $this->footer = $footer;
296 2
        $this->helpEnabled = true;
297 2
        $this->addOption(['name' => 'help', 'short_name' => 'h', 'help' => "display this help message"]);
298 2
        foreach($this->commands as $command) {
299 1
            $this->addOption(['name' => 'help', 'help' => 'display this help message', 'command' => $command['name']]);
300
        }
301 2
    }
302
303
    public function getHelpMessage($command = '')
304
    {
305 2
        return $this->helpGenerator->generate(
306 2
            $this->name, $command ?? null,
307 2
            ['options' => $this->options, 'commands' => $this->commands],
308 2
            $this->description, $this->footer
309
        );
310
    }
311
312
    /**
313
     * @param $command
314
     * @throws CommandExistsException
315
     * @throws InvalidArgumentDescriptionException
316
     */
317
    public function addCommand($command)
318
    {
319 5
        $this->validator->validateCommand($command, $this->commands);
320 5
        $this->commands[$command['name']] = $command;
321 5
    }
322
}
323