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

nette / command-line / 20908440499

12 Jan 2026 04:50AM UTC coverage: 96.859% (+2.6%) from 94.231%
20908440499

push

github

dg
added Test class

27 of 27 new or added lines in 1 file covered. (100.0%)

6 existing lines in 2 files now uncovered.

185 of 191 relevant lines covered (96.86%)

0.97 hits per line

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

97.64
/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

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

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

52

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

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

62

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

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

86
                foreach ($defaults as $name => $opt) {
1✔
87
                        $default = $opt[self::Default] ?? null;
1✔
88
                        $type = match (true) {
1✔
89
                                !($opt[self::Argument] ?? true) => ValueType::None,
1✔
90
                                ($opt[self::Optional] ?? false) || $default !== null => ValueType::Optional,
1✔
91
                                default => ValueType::Required,
1✔
92
                        };
93
                        if ($opt[self::RealPath] ?? false) {
1✔
94
                                $opt[self::Normalizer] = ($opt[self::Normalizer] ?? null)
1✔
UNCOV
95
                                        ? fn($value) => self::normalizeRealPath($opt[self::Normalizer]($value))
×
96
                                        : self::normalizeRealPath(...);
1✔
97
                        }
98
                        $this->options[$name] = new Option(
1✔
99
                                name: $name,
1✔
100
                                alias: $aliases[$name] ?? null,
1✔
101
                                type: $type,
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
                ?string $description = null,
123
        ): self
124
        {
125
                $this->options[$name] = new Option(
1✔
126
                        name: $name,
1✔
127
                        alias: $alias,
128
                        type: ValueType::None,
1✔
129
                        repeatable: $repeatable,
130
                        description: $description,
131
                );
132
                return $this;
1✔
133
        }
134

135

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

165

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

193

194
        /**
195
         * Parses command-line arguments and returns associative array of values.
196
         * @param array|null $args  Arguments to parse (defaults to $_SERVER['argv'])
197
         */
198
        public function parse(?array $args = null): array
1✔
199
        {
200
                $args ??= $this->args;
1✔
201

202
                $aliases = $positional = [];
1✔
203
                foreach ($this->options as $opt) {
1✔
204
                        if ($opt->isPositional()) {
1✔
205
                                $positional[] = $opt;
1✔
206
                        } elseif ($opt->alias !== null) {
1✔
207
                                $aliases[$opt->alias] = $opt;
1✔
208
                        }
209
                }
210

211
                $params = [];
1✔
212
                reset($positional);
1✔
213
                $i = 0;
1✔
214
                while ($i < count($args)) {
1✔
215
                        $arg = $args[$i++];
1✔
216
                        if ($arg[0] !== '-') {
1✔
217
                                if (!current($positional)) {
1✔
218
                                        throw new \Exception("Unexpected parameter $arg.");
1✔
219
                                }
220

221
                                $opt = current($positional);
1✔
222
                                $arg = $this->normalizeValue($opt, $arg);
1✔
223
                                if (!$opt->repeatable) {
1✔
224
                                        $params[$opt->name] = $arg;
1✔
225
                                        next($positional);
1✔
226
                                } else {
227
                                        $params[$opt->name][] = $arg;
1✔
228
                                }
229

230
                                continue;
1✔
231
                        }
232

233
                        [$name, $arg] = strpos($arg, '=') ? explode('=', $arg, 2) : [$arg, true];
1✔
234
                        $opt = $aliases[$name] ?? $this->options[$name] ?? null;
1✔
235
                        if (!$opt) {
1✔
236
                                throw new \Exception("Unknown option $name.");
1✔
237
                        }
238

239
                        if ($arg !== true && $opt->type === ValueType::None) {
1✔
240
                                throw new \Exception("Option $opt->name has not argument.");
1✔
241

242
                        } elseif ($arg === true && $opt->type !== ValueType::None) {
1✔
243
                                if (isset($args[$i]) && $args[$i][0] !== '-') {
1✔
244
                                        $arg = $args[$i++];
1✔
245
                                } elseif ($opt->type === ValueType::Required) {
1✔
246
                                        throw new \Exception("Option $opt->name requires argument.");
1✔
247
                                }
248
                        }
249

250
                        $arg = $this->normalizeValue($opt, $arg);
1✔
251

252
                        if (!$opt->repeatable) {
1✔
253
                                $params[$opt->name] = $arg;
1✔
254
                        } else {
255
                                $params[$opt->name][] = $arg;
1✔
256
                        }
257
                }
258

259
                foreach ($this->options as $opt) {
1✔
260
                        if (isset($params[$opt->name])) {
1✔
261
                                continue;
1✔
262
                        } elseif ($opt->type !== ValueType::Required) {
1✔
263
                                $params[$opt->name] = $opt->fallback;
1✔
264
                        } elseif ($opt->isPositional()) {
1✔
265
                                throw new \Exception("Missing required argument <$opt->name>.");
1✔
266
                        } else {
267
                                $params[$opt->name] = null;
1✔
268
                        }
269

270
                        if ($opt->repeatable) {
1✔
271
                                $params[$opt->name] = (array) $params[$opt->name];
1✔
272
                        }
273
                }
274

275
                return $params;
1✔
276
        }
277

278

279
        /**
280
         * Prints help text to stdout.
281
         */
282
        public function help(): void
283
        {
284
                echo $this->help;
1✔
285

286
                foreach ($this->options as $opt) {
1✔
287
                        if ($opt->description === null) {
1✔
288
                                continue;
1✔
289
                        }
290

291
                        if ($opt->isPositional()) {
1✔
292
                                echo "\t" . ($opt->type === ValueType::Optional ? "[$opt->name]" : "<$opt->name>");
1✔
293
                        } else {
294
                                echo "\t" . ($opt->alias ? "$opt->alias, " : '') . $opt->name;
1✔
295
                                if ($opt->type !== ValueType::None) {
1✔
296
                                        echo match (true) {
297
                                                (bool) $opt->enum => '<' . implode('|', $opt->enum) . '>',
1✔
298
                                                $opt->type === ValueType::Optional => '[value]',
1✔
299
                                                default => '<value>',
1✔
300
                                        };
301
                                }
302
                        }
303

304
                        echo ($opt->repeatable ? '...' : '')
1✔
305
                                . '  ' . $opt->description
1✔
306
                                . (is_scalar($opt->fallback) ? " (default: $opt->fallback)" : '')
1✔
307
                                . "\n";
1✔
308
                }
309
        }
1✔
310

311

312
        private function normalizeValue(Option $opt, mixed $value): mixed
1✔
313
        {
314
                if ($opt->enum && $value !== true && !in_array($value, $opt->enum, true)) {
1✔
315
                        throw new \Exception("Value of option $opt->name must be " . implode(', or ', $opt->enum) . '.');
1✔
316
                }
317

318
                return $opt->normalizer ? ($opt->normalizer)($value) : $value;
1✔
319
        }
320

321

322
        /**
323
         * Normalizer that resolves path to absolute and validates existence.
324
         */
325
        public static function normalizeRealPath(string $value): string
1✔
326
        {
327
                $path = realpath($value);
1✔
328
                if ($path === false) {
1✔
329
                        throw new \Exception("File path '$value' not found.");
1✔
330
                }
331

332
                return $path;
1✔
333
        }
334

335

336
        /**
337
         * Returns true if no command-line arguments were provided.
338
         */
339
        public function isEmpty(): bool
340
        {
UNCOV
341
                return !$this->args;
×
342
        }
343
}
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