• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / command-line / 20911282381

12 Jan 2026 07:25AM UTC coverage: 96.951% (+0.5%) from 96.429%
20911282381

push

github

dg
improved readme.md

159 of 164 relevant lines covered (96.95%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.06
/src/CommandLine/Parser.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\CommandLine;
11

12

13
/**
14
 * Stupid command line arguments parser.
15
 */
16
class Parser
17
{
18
        public const
19
                Argument = 'argument',
20
                Optional = 'optional',
21
                Repeatable = 'repeatable',
22
                Enum = 'enum',
23
                RealPath = 'realpath',
24
                Normalizer = 'normalizer',
25
                Default = 'default';
26

27
        #[\Deprecated('use Parser::Argument')]
28
        public const ARGUMENT = self::Argument;
29

30
        #[\Deprecated('use Parser::Optional')]
31
        public const OPTIONAL = self::Optional;
32

33
        #[\Deprecated('use Parser::Repeatable')]
34
        public const REPEATABLE = self::Repeatable;
35

36
        #[\Deprecated('use Parser::Enum')]
37
        public const ENUM = self::Enum;
38

39
        #[\Deprecated('use Parser::RealPath')]
40
        public const REALPATH = self::RealPath;
41

42
        #[\Deprecated('use Parser::Default')]
43
        public const VALUE = self::Default;
44
        private const OptionPresent = true;
45

46
        /** @var array<string, Option> */
47
        private array $options = [];
48
        private string $help = '';
49

50
        /** @var string[] */
51
        private array $args;
52

53

54
        public function __construct(string $help = '', array $defaults = [])
1✔
55
        {
56
                $this->args = isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : [];
1✔
57

58
                if ($help || $defaults) {
1✔
59
                        $this->addFromHelp($help, $defaults);
1✔
60
                }
61
        }
1✔
62

63

64
        /**
65
         * Extracts option definitions from formatted help text.
66
         */
67
        public function addFromHelp(string $help, array $defaults = []): static
1✔
68
        {
69
                preg_match_all('#^[ \t]+(--?\w.*?)(?:  .*\(default: (.*)\)|  |\r|$)#m', $help, $lines, PREG_SET_ORDER);
1✔
70
                foreach ($lines as $line) {
1✔
71
                        preg_match_all('#(--?\w[\w-]*)(?:[= ](<.*?>|\[.*?]|\w+)(\.{0,3}))?[ ,|]*#A', $line[1], $m);
1✔
72
                        if (!count($m[0]) || count($m[0]) > 2 || implode('', $m[0]) !== $line[1]) {
1✔
73
                                throw new \InvalidArgumentException("Unable to parse '$line[1]'.");
×
74
                        }
75

76
                        $name = end($m[1]);
1✔
77
                        $defaults[$name] = ($defaults[$name] ?? []) + [
1✔
78
                                self::Argument => (bool) end($m[2]),
1✔
79
                                self::Optional => isset($line[2]) || (str_starts_with(end($m[2]), '[')),
1✔
80
                                self::Repeatable => (bool) end($m[3]),
1✔
81
                                self::Enum => count($enums = explode('|', trim(end($m[2]), '<[]>'))) > 1 ? $enums : null,
1✔
82
                                self::Default => $line[2] ?? null,
1✔
83
                        ];
84
                        $aliases[$name] = $name !== $m[1][0] ? $m[1][0] : null;
1✔
85
                }
86

87
                foreach ($defaults as $name => $opt) {
1✔
88
                        $default = $opt[self::Default] ?? null;
1✔
89
                        if ($opt[self::RealPath] ?? false) {
1✔
90
                                $opt[self::Normalizer] = ($opt[self::Normalizer] ?? null)
1✔
91
                                        ? fn($value) => self::normalizeRealPath($opt[self::Normalizer]($value))
×
92
                                        : self::normalizeRealPath(...);
1✔
93
                        }
94
                        $this->options[$name] = new Option(
1✔
95
                                name: $name,
1✔
96
                                alias: $aliases[$name] ?? null,
1✔
97
                                type: match (true) {
1✔
98
                                        !($opt[self::Argument] ?? true) => ValueType::None,
1✔
99
                                        ($opt[self::Optional] ?? false) || $default !== null => ValueType::Optional,
1✔
100
                                        default => ValueType::Required,
1✔
101
                                },
102
                                repeatable: (bool) ($opt[self::Repeatable] ?? null),
1✔
103
                                fallback: $default,
104
                                normalizer: $opt[self::Normalizer] ?? null,
1✔
105
                                enum: $opt[self::Enum] ?? null,
1✔
106
                        );
107
                }
108

109
                $this->help .= $help;
1✔
110
                return $this;
1✔
111
        }
112

113

114
        /**
115
         * Adds a switch (flag without value), e.g. --foo or -f.
116
         * Parses as true when used, null when not.
117
         */
118
        public function addSwitch(
1✔
119
                string $name,
120
                ?string $alias = null,
121
                bool $repeatable = false,
122
        ): static
123
        {
124
                $this->options[$name] = new Option(
1✔
125
                        name: $name,
1✔
126
                        alias: $alias,
127
                        type: ValueType::None,
1✔
128
                        repeatable: $repeatable,
129
                );
130
                return $this;
1✔
131
        }
132

133

134
        /**
135
         * Adds an option with value, e.g. --foo json or -f json.
136
         * @param bool $optionalValue  If true, value can be omitted (--foo parses as true)
137
         * @param mixed $fallback      Parsed value when option is not used at all
138
         */
139
        public function addOption(
1✔
140
                string $name,
141
                ?string $alias = null,
142
                bool $optionalValue = false,
143
                mixed $fallback = null,
144
                ?array $enum = null,
145
                bool $repeatable = false,
146
                ?\Closure $normalizer = null,
147
        ): static
148
        {
149
                $this->options[$name] = new Option(
1✔
150
                        name: $name,
1✔
151
                        alias: $alias,
152
                        type: $optionalValue ? ValueType::Optional : ValueType::Required,
1✔
153
                        fallback: $fallback,
154
                        repeatable: $repeatable,
155
                        enum: $enum,
156
                        normalizer: $normalizer,
157
                );
158
                return $this;
1✔
159
        }
160

161

162
        /**
163
         * Adds a positional argument, e.g. <foo> or [foo].
164
         * @param bool $optional   If true, argument can be omitted
165
         * @param mixed $fallback  Parsed value when argument is not provided
166
         */
167
        public function addArgument(
1✔
168
                string $name,
169
                bool $optional = false,
170
                mixed $fallback = null,
171
                ?array $enum = null,
172
                bool $repeatable = false,
173
                ?\Closure $normalizer = null,
174
        ): static
175
        {
176
                $this->options[$name] = new Option(
1✔
177
                        name: $name,
1✔
178
                        type: $optional ? ValueType::Optional : ValueType::Required,
1✔
179
                        fallback: $fallback,
180
                        repeatable: $repeatable,
181
                        enum: $enum,
182
                        normalizer: $normalizer,
183
                );
184
                return $this;
1✔
185
        }
186

187

188
        /**
189
         * Parses command-line arguments and returns associative array of values.
190
         * @param array|null $args  Arguments to parse (defaults to $_SERVER['argv'])
191
         */
192
        public function parse(?array $args = null): array
1✔
193
        {
194
                $args ??= $this->args;
1✔
195

196
                $aliases = $positional = [];
1✔
197
                foreach ($this->options as $opt) {
1✔
198
                        if ($opt->positional) {
1✔
199
                                $positional[] = $opt;
1✔
200
                        } elseif ($opt->alias !== null) {
1✔
201
                                $aliases[$opt->alias] = $opt;
1✔
202
                        }
203
                }
204

205
                $params = [];
1✔
206
                reset($positional);
1✔
207
                $i = 0;
1✔
208
                while ($i < count($args)) {
1✔
209
                        $arg = $args[$i++];
1✔
210
                        if ($arg[0] !== '-') {
1✔
211
                                if (!current($positional)) {
1✔
212
                                        throw new \Exception("Unexpected parameter $arg.");
1✔
213
                                }
214

215
                                $opt = current($positional);
1✔
216
                                $arg = $this->normalizeValue($opt, $arg);
1✔
217
                                if (!$opt->repeatable) {
1✔
218
                                        $params[$opt->name] = $arg;
1✔
219
                                        next($positional);
1✔
220
                                } else {
221
                                        $params[$opt->name][] = $arg;
1✔
222
                                }
223

224
                                continue;
1✔
225
                        }
226

227
                        [$name, $arg] = strpos($arg, '=') ? explode('=', $arg, 2) : [$arg, self::OptionPresent];
1✔
228
                        $opt = $aliases[$name] ?? $this->options[$name] ?? null;
1✔
229
                        if (!$opt) {
1✔
230
                                throw new \Exception("Unknown option $name.");
1✔
231
                        }
232

233
                        if ($arg !== self::OptionPresent && $opt->type === ValueType::None) {
1✔
234
                                throw new \Exception("Option $opt->name has not argument.");
1✔
235

236
                        } elseif ($arg === self::OptionPresent && $opt->type !== ValueType::None) {
1✔
237
                                if (isset($args[$i]) && $args[$i][0] !== '-') {
1✔
238
                                        $arg = $args[$i++];
1✔
239
                                } elseif ($opt->type === ValueType::Required) {
1✔
240
                                        throw new \Exception("Option $opt->name requires argument.");
1✔
241
                                }
242
                        }
243

244
                        $arg = $this->normalizeValue($opt, $arg);
1✔
245

246
                        if (!$opt->repeatable) {
1✔
247
                                $params[$opt->name] = $arg;
1✔
248
                        } else {
249
                                $params[$opt->name][] = $arg;
1✔
250
                        }
251
                }
252

253
                foreach ($this->options as $opt) {
1✔
254
                        if (isset($params[$opt->name])) {
1✔
255
                                continue;
1✔
256
                        } elseif ($opt->type !== ValueType::Required) {
1✔
257
                                $params[$opt->name] = $opt->fallback;
1✔
258
                        } elseif ($opt->positional) {
1✔
259
                                throw new \Exception("Missing required argument <$opt->name>.");
1✔
260
                        } else {
261
                                $params[$opt->name] = null;
1✔
262
                        }
263

264
                        if ($opt->repeatable) {
1✔
265
                                $params[$opt->name] = (array) $params[$opt->name];
1✔
266
                        }
267
                }
268

269
                return $params;
1✔
270
        }
271

272

273
        /**
274
         * Parses only specified options, ignoring everything else.
275
         * No validation, no exceptions. Useful for early-exit options like --help.
276
         * @param  string[]  $names  Option names to parse (e.g., ['--help', '--version'])
277
         * @return array<string, mixed>  Parsed values (null if option not used)
278
         */
279
        public function parseOnly(array $names, ?array $args = null): array
1✔
280
        {
281
                $args ??= $this->args;
1✔
282
                $lookup = [];
1✔
283
                foreach ($names as $name) {
1✔
284
                        $opt = $this->options[$name] ?? null;
1✔
285
                        if ($opt) {
1✔
286
                                $lookup[$name] = $opt;
1✔
287
                                if ($opt->alias !== null) {
1✔
288
                                        $lookup[$opt->alias] = $opt;
1✔
289
                                }
290
                        }
291
                }
292

293
                $params = array_fill_keys($names, null);
1✔
294
                $i = 0;
1✔
295
                while ($i < count($args)) {
1✔
296
                        $arg = $args[$i++];
1✔
297
                        if ($arg[0] !== '-') {
1✔
298
                                continue;
1✔
299
                        }
300

301
                        [$name, $value] = strpos($arg, '=') ? explode('=', $arg, 2) : [$arg, self::OptionPresent];
1✔
302
                        $opt = $lookup[$name] ?? null;
1✔
303
                        if (!$opt) {
1✔
304
                                continue;
1✔
305
                        }
306

307
                        if ($value === self::OptionPresent && $opt->type !== ValueType::None) {
1✔
308
                                if (isset($args[$i]) && $args[$i][0] !== '-') {
1✔
309
                                        $value = $args[$i++];
1✔
310
                                }
311
                        }
312

313
                        $params[$opt->name] = $value;
1✔
314
                }
315

316
                return $params;
1✔
317
        }
318

319

320
        /**
321
         * Prints help text to stdout.
322
         */
323
        public function help(): void
324
        {
325
                echo $this->help;
×
326
        }
327

328

329
        private function normalizeValue(Option $opt, mixed $value): mixed
1✔
330
        {
331
                if ($opt->enum && $value !== self::OptionPresent && !in_array($value, $opt->enum, strict: true)) {
1✔
332
                        throw new \Exception("Value of option $opt->name must be " . implode(', or ', $opt->enum) . '.');
1✔
333
                }
334

335
                return $opt->normalizer ? ($opt->normalizer)($value) : $value;
1✔
336
        }
337

338

339
        /**
340
         * Normalizer that resolves path to absolute and validates existence.
341
         */
342
        public static function normalizeRealPath(string $value): string
1✔
343
        {
344
                $path = realpath($value);
1✔
345
                if ($path === false) {
1✔
346
                        throw new \Exception("File path '$value' not found.");
1✔
347
                }
348

349
                return $path;
1✔
350
        }
351

352

353
        /**
354
         * Returns true if no command-line arguments were provided.
355
         */
356
        public function isEmpty(): bool
357
        {
358
                return !$this->args;
×
359
        }
360
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc