Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
20 / 20
92.59% covered (success)
92.59%
25 / 27
40.74% covered (danger)
40.74%
11 / 27
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Args
100.00% covered (success)
100.00%
20 / 20
92.59% covered (success)
92.59%
25 / 27
40.74% covered (danger)
40.74%
11 / 27
100.00% covered (success)
100.00%
7 / 7
48.17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
88.89% covered (warning)
88.89%
16 / 18
20.00% covered (danger)
20.00%
4 / 20
100.00% covered (success)
100.00%
1 / 1
24.43
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 names
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 opt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 opts
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 positional
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 positionals
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Console;
6
7/**
8 * Parses a command's argument tokens into options and positionals.
9 *
10 * Options use `--key=value` and repeat: `--tag=a --tag=b`. A dashed token
11 * without `=`, such as `--force` or `-h`, is a boolean flag. Every other
12 * token is a positional. Unlike PHP's native `getopt`, this reads an
13 * explicit token list, so the command name never interferes with parsing.
14 *
15 * A positional cannot start with `-`; such a token is read as a flag —
16 * unless it follows a `--` separator: the first `--` ends option parsing
17 * and every later token is a positional, dashed or not.
18 *
19 * @api
20 */
21final class Args
22{
23    /** @var array<string, list<string>> */
24    private array $options = [];
25
26    /** @var list<string> */
27    private array $positionals = [];
28
29    /**
30     * @param list<string> $tokens
31     */
32    public function __construct(array $tokens = [])
33    {
34        $literal = false;
35
36        foreach ($tokens as $token) {
37            if ($literal || !str_starts_with($token, '-')) {
38                $this->positionals[] = $token;
39
40                continue;
41            }
42
43            // The token is command-line input, not a secret.
44            // @mago-expect lint:no-insecure-comparison
45            if ($token === '--') {
46                $literal = true;
47
48                continue;
49            }
50
51            if (str_contains($token, '=')) {
52                /** @var array{0: string, 1: string} $pair */
53                $pair = explode('=', $token, limit: 2);
54                $this->options[$pair[0]][] = $pair[1];
55
56                continue;
57            }
58
59            $this->options[$token] ??= [];
60        }
61    }
62
63    public function has(string $key): bool
64    {
65        return array_key_exists($key, $this->options);
66    }
67
68    /**
69     * The names of all provided options, in first-seen order.
70     *
71     * @return list<string>
72     */
73    public function names(): array
74    {
75        // PHP turns numeric keys like '-5' into integers.
76        return array_map(strval(...), array_keys($this->options));
77    }
78
79    public function opt(string $key, string $default = ''): string
80    {
81        return $this->options[$key][0] ?? $default;
82    }
83
84    /**
85     * @param list<string> $default
86     * @return list<string>
87     */
88    public function opts(string $key, array $default = []): array
89    {
90        $values = $this->options[$key] ?? [];
91
92        return $values === [] ? $default : $values;
93    }
94
95    public function positional(int $index, ?string $default = null): ?string
96    {
97        return $this->positionals[$index] ?? $default;
98    }
99
100    /**
101     * @return list<string>
102     */
103    public function positionals(): array
104    {
105        return $this->positionals;
106    }
107}