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

nette / command-line / 22289667037

23 Feb 2026 01:20AM UTC coverage: 96.226%. Remained the same
22289667037

push

github

dg
removed deprecated stuff

153 of 159 relevant lines covered (96.23%)

0.96 hits per line

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

97.06
/src/CommandLine/Parser.php
1
<?php declare(strict_types=1);
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
namespace Nette\CommandLine;
9

10

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

25
        private const OptionPresent = true;
26

27
        /** @var array<string, Option> */
28
        private array $options = [];
29
        private string $help = '';
30

31
        /** @var list<string> */
32
        private array $args;
33

34

35
        /** @param array<string, array<string, mixed>> $defaults */
36
        public function __construct(string $help = '', array $defaults = [])
1✔
37
        {
38
                $this->args = array_values(array_map(strval(...), isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : []));
1✔
39

40
                if ($help || $defaults) {
1✔
41
                        $this->addFromHelp($help, $defaults);
1✔
42
                }
43
        }
1✔
44

45

46
        /**
47
         * Extracts option definitions from formatted help text.
48
         * @param array<string, array<string, mixed>> $defaults
49
         */
50
        public function addFromHelp(string $help, array $defaults = []): static
1✔
51
        {
52
                preg_match_all('#^[ \t]+(--?\w.*?)(?:  .*\(default: (.*)\)|  |\r|$)#m', $help, $lines, PREG_SET_ORDER);
1✔
53
                foreach ($lines as $line) {
1✔
54
                        preg_match_all('#(--?\w[\w-]*)(?:[= ](<.*?>|\[.*?]|\w+)(\.{0,3}))?[ ,|]*#A', $line[1], $m);
1✔
55
                        if (!count($m[0]) || count($m[0]) > 2 || implode('', $m[0]) !== $line[1]) {
1✔
56
                                throw new \InvalidArgumentException("Unable to parse '$line[1]'.");
×
57
                        }
58

59
                        $name = (string) end($m[1]);
1✔
60
                        $defaults[$name] = ($defaults[$name] ?? []) + [
1✔
61
                                self::Argument => (bool) end($m[2]),
1✔
62
                                self::Optional => isset($line[2]) || (str_starts_with((string) end($m[2]), '[')),
1✔
63
                                self::Repeatable => (bool) end($m[3]),
1✔
64
                                self::Enum => count($enums = explode('|', trim((string) end($m[2]), '<[]>'))) > 1 ? $enums : null,
1✔
65
                                self::Default => $line[2] ?? null,
1✔
66
                        ];
67
                        $aliases[$name] = $name !== $m[1][0] ? $m[1][0] : null;
1✔
68
                }
69

70
                foreach ($defaults as $name => $opt) {
1✔
71
                        $default = $opt[self::Default] ?? null;
1✔
72
                        if ($opt[self::RealPath] ?? false) {
1✔
73
                                $opt[self::Normalizer] = ($opt[self::Normalizer] ?? null)
1✔
74
                                        ? fn($value) => self::normalizeRealPath($opt[self::Normalizer]($value))
×
75
                                        : self::normalizeRealPath(...);
1✔
76
                        }
77
                        $this->options[$name] = new Option(
1✔
78
                                name: $name,
1✔
79
                                alias: $aliases[$name] ?? null,
1✔
80
                                type: match (true) {
1✔
81
                                        !($opt[self::Argument] ?? true) => ValueType::None,
1✔
82
                                        ($opt[self::Optional] ?? false) || $default !== null => ValueType::Optional,
1✔
83
                                        default => ValueType::Required,
1✔
84
                                },
85
                                repeatable: (bool) ($opt[self::Repeatable] ?? null),
1✔
86
                                fallback: $default,
87
                                normalizer: $opt[self::Normalizer] ?? null,
1✔
88
                                enum: $opt[self::Enum] ?? null,
1✔
89
                        );
90
                }
91

92
                $this->help .= $help;
1✔
93
                return $this;
1✔
94
        }
95

96

97
        /**
98
         * Adds a switch (flag without value), e.g. --foo or -f.
99
         * Parses as true when used, null when not.
100
         */
101
        public function addSwitch(
1✔
102
                string $name,
103
                ?string $alias = null,
104
                bool $repeatable = false,
105
        ): static
106
        {
107
                $this->options[$name] = new Option(
1✔
108
                        name: $name,
1✔
109
                        alias: $alias,
110
                        type: ValueType::None,
1✔
111
                        repeatable: $repeatable,
112
                );
113
                return $this;
1✔
114
        }
115

116

117
        /**
118
         * Adds an option with value, e.g. --foo json or -f json.
119
         * @param bool  $optionalValue  If true, value can be omitted (--foo parses as true)
120
         * @param mixed  $fallback      Parsed value when option is not used at all
121
         * @param ?list<string>  $enum
122
         * @param ?(\Closure(mixed): mixed)  $normalizer
123
         */
124
        public function addOption(
1✔
125
                string $name,
126
                ?string $alias = null,
127
                bool $optionalValue = false,
128
                mixed $fallback = null,
129
                ?array $enum = null,
130
                bool $repeatable = false,
131
                ?\Closure $normalizer = null,
132
        ): static
133
        {
134
                $this->options[$name] = new Option(
1✔
135
                        name: $name,
1✔
136
                        alias: $alias,
137
                        type: $optionalValue ? ValueType::Optional : ValueType::Required,
1✔
138
                        fallback: $fallback,
139
                        repeatable: $repeatable,
140
                        enum: $enum,
141
                        normalizer: $normalizer,
142
                );
143
                return $this;
1✔
144
        }
145

146

147
        /**
148
         * Adds a positional argument, e.g. <foo> or [foo].
149
         * @param bool  $optional   If true, argument can be omitted
150
         * @param mixed  $fallback  Parsed value when argument is not provided
151
         * @param ?list<string>  $enum
152
         * @param ?(\Closure(mixed): mixed)  $normalizer
153
         */
154
        public function addArgument(
1✔
155
                string $name,
156
                bool $optional = false,
157
                mixed $fallback = null,
158
                ?array $enum = null,
159
                bool $repeatable = false,
160
                ?\Closure $normalizer = null,
161
        ): static
162
        {
163
                $this->options[$name] = new Option(
1✔
164
                        name: $name,
1✔
165
                        type: $optional ? ValueType::Optional : ValueType::Required,
1✔
166
                        fallback: $fallback,
167
                        repeatable: $repeatable,
168
                        enum: $enum,
169
                        normalizer: $normalizer,
170
                );
171
                return $this;
1✔
172
        }
173

174

175
        /**
176
         * Parses command-line arguments and returns associative array of values.
177
         * @param ?list<string>  $args  Arguments to parse (defaults to $_SERVER['argv'])
178
         * @return array<string, mixed>
179
         */
180
        public function parse(?array $args = null): array
1✔
181
        {
182
                $args ??= $this->args;
1✔
183

184
                $aliases = $positional = [];
1✔
185
                foreach ($this->options as $opt) {
1✔
186
                        if ($opt->positional) {
1✔
187
                                $positional[] = $opt;
1✔
188
                        } elseif ($opt->alias !== null) {
1✔
189
                                $aliases[$opt->alias] = $opt;
1✔
190
                        }
191
                }
192

193
                $params = [];
1✔
194
                reset($positional);
1✔
195
                $i = 0;
1✔
196
                while ($i < count($args)) {
1✔
197
                        $arg = $args[$i++];
1✔
198
                        if ($arg[0] !== '-') {
1✔
199
                                if (!current($positional)) {
1✔
200
                                        throw new \Exception("Unexpected parameter $arg.");
1✔
201
                                }
202

203
                                $opt = current($positional);
1✔
204
                                $arg = $this->normalizeValue($opt, $arg);
1✔
205
                                if (!$opt->repeatable) {
1✔
206
                                        $params[$opt->name] = $arg;
1✔
207
                                        next($positional);
1✔
208
                                } else {
209
                                        $params[$opt->name][] = $arg;
1✔
210
                                }
211

212
                                continue;
1✔
213
                        }
214

215
                        [$name, $arg] = strpos($arg, '=') ? explode('=', $arg, 2) : [$arg, self::OptionPresent];
1✔
216
                        $opt = $aliases[$name] ?? $this->options[$name] ?? null;
1✔
217
                        if (!$opt) {
1✔
218
                                throw new \Exception("Unknown option $name.");
1✔
219
                        }
220

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

224
                        } elseif ($arg === self::OptionPresent && $opt->type !== ValueType::None) {
1✔
225
                                if (isset($args[$i]) && $args[$i][0] !== '-') {
1✔
226
                                        $arg = $args[$i++];
1✔
227
                                } elseif ($opt->type === ValueType::Required) {
1✔
228
                                        throw new \Exception("Option $opt->name requires argument.");
1✔
229
                                }
230
                        }
231

232
                        $arg = $this->normalizeValue($opt, $arg);
1✔
233

234
                        if (!$opt->repeatable) {
1✔
235
                                $params[$opt->name] = $arg;
1✔
236
                        } else {
237
                                $params[$opt->name][] = $arg;
1✔
238
                        }
239
                }
240

241
                foreach ($this->options as $opt) {
1✔
242
                        if (isset($params[$opt->name])) {
1✔
243
                                continue;
1✔
244
                        } elseif ($opt->type !== ValueType::Required) {
1✔
245
                                $params[$opt->name] = $opt->fallback;
1✔
246
                        } elseif ($opt->positional) {
1✔
247
                                throw new \Exception("Missing required argument <$opt->name>.");
1✔
248
                        } else {
249
                                $params[$opt->name] = null;
1✔
250
                        }
251

252
                        if ($opt->repeatable) {
1✔
253
                                $params[$opt->name] = (array) $params[$opt->name];
1✔
254
                        }
255
                }
256

257
                return $params;
1✔
258
        }
259

260

261
        /**
262
         * Parses only specified options, ignoring everything else.
263
         * No validation, no exceptions. Useful for early-exit options like --help.
264
         * @param  list<string>  $names  Option names to parse (e.g., ['--help', '--version'])
265
         * @param  ?list<string>  $args
266
         * @return array<string, mixed>  Parsed values (null if option not used)
267
         */
268
        public function parseOnly(array $names, ?array $args = null): array
1✔
269
        {
270
                $args ??= $this->args;
1✔
271
                $lookup = [];
1✔
272
                foreach ($names as $name) {
1✔
273
                        $opt = $this->options[$name] ?? null;
1✔
274
                        if ($opt) {
1✔
275
                                $lookup[$name] = $opt;
1✔
276
                                if ($opt->alias !== null) {
1✔
277
                                        $lookup[$opt->alias] = $opt;
1✔
278
                                }
279
                        }
280
                }
281

282
                $params = array_fill_keys($names, null);
1✔
283
                $i = 0;
1✔
284
                while ($i < count($args)) {
1✔
285
                        $arg = $args[$i++];
1✔
286
                        if ($arg[0] !== '-') {
1✔
287
                                continue;
1✔
288
                        }
289

290
                        [$name, $value] = strpos($arg, '=') ? explode('=', $arg, 2) : [$arg, self::OptionPresent];
1✔
291
                        $opt = $lookup[$name] ?? null;
1✔
292
                        if (!$opt) {
1✔
293
                                continue;
1✔
294
                        }
295

296
                        if ($value === self::OptionPresent && $opt->type !== ValueType::None) {
1✔
297
                                if (isset($args[$i]) && $args[$i][0] !== '-') {
1✔
298
                                        $value = $args[$i++];
1✔
299
                                }
300
                        }
301

302
                        $params[$opt->name] = $value;
1✔
303
                }
304

305
                return $params;
1✔
306
        }
307

308

309
        /**
310
         * Prints help text to stdout.
311
         */
312
        public function help(): void
313
        {
314
                echo $this->help;
×
315
        }
316

317

318
        private function normalizeValue(Option $opt, mixed $value): mixed
1✔
319
        {
320
                if ($opt->enum && $value !== self::OptionPresent && !in_array($value, $opt->enum, strict: true)) {
1✔
321
                        throw new \Exception("Value of option $opt->name must be " . implode(', or ', $opt->enum) . '.');
1✔
322
                }
323

324
                return $opt->normalizer ? ($opt->normalizer)($value) : $value;
1✔
325
        }
326

327

328
        /**
329
         * Normalizer that resolves path to absolute and validates existence.
330
         */
331
        public static function normalizeRealPath(string $value): string
1✔
332
        {
333
                $path = realpath($value);
1✔
334
                if ($path === false) {
1✔
335
                        throw new \Exception("File path '$value' not found.");
1✔
336
                }
337

338
                return $path;
1✔
339
        }
340

341

342
        /**
343
         * Returns true if no command-line arguments were provided.
344
         */
345
        public function isEmpty(): bool
346
        {
347
                return !$this->args;
×
348
        }
349
}
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