Completed
Push — dev ( 7759a2...f00774 )
by James Ekow Abaka
01:34
created

ArgumentParser::parse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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