Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
92.86% covered (success)
92.86%
26 / 28
42.86% covered (danger)
42.86%
12 / 28
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Args
100.00% covered (success)
100.00%
22 / 22
92.86% covered (success)
92.86%
26 / 28
42.86% covered (danger)
42.86%
12 / 28
100.00% covered (success)
100.00%
8 / 8
50.57
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
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
 bare
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
 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 array<string, true> */
27    private array $bare = [];
28
29    /** @var list<string> */
30    private array $positionals = [];
31
32    /**
33     * @param list<string> $tokens
34     */
35    public function __construct(array $tokens = [])
36    {
37        $literal = false;
38
39        foreach ($tokens as $token) {
40            if ($literal || !str_starts_with($token, '-')) {
41                $this->positionals[] = $token;
42
43                continue;
44            }
45
46            // The token is command-line input, not a secret.
47            // @mago-expect lint:no-insecure-comparison
48            if ($token === '--') {
49                $literal = true;
50
51                continue;
52            }
53
54            if (str_contains($token, '=')) {
55                /** @var array{0: string, 1: string} $pair */
56                $pair = explode('=', $token, limit: 2);
57                $this->options[$pair[0]][] = $pair[1];
58
59                continue;
60            }
61
62            $this->options[$token] ??= [];
63            $this->bare[$token] = true;
64        }
65    }
66
67    /**
68     * Whether the option occurred at least once without a value.
69     */
70    public function bare(string $key): bool
71    {
72        return array_key_exists($key, $this->bare);
73    }
74
75    public function has(string $key): bool
76    {
77        return array_key_exists($key, $this->options);
78    }
79
80    /**
81     * The names of all provided options, in first-seen order.
82     *
83     * @return list<string>
84     */
85    public function names(): array
86    {
87        return array_keys($this->options);
88    }
89
90    public function opt(string $key, string $default = ''): string
91    {
92        return $this->options[$key][0] ?? $default;
93    }
94
95    /**
96     * @param list<string> $default
97     * @return list<string>
98     */
99    public function opts(string $key, array $default = []): array
100    {
101        $values = $this->options[$key] ?? [];
102
103        return $values === [] ? $default : $values;
104    }
105
106    public function positional(int $index, ?string $default = null): ?string
107    {
108        return $this->positionals[$index] ?? $default;
109    }
110
111    /**
112     * @return list<string>
113     */
114    public function positionals(): array
115    {
116        return $this->positionals;
117    }
118}