Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
15 / 15
90.91% covered (success)
90.91%
20 / 22
55.56% covered (warning)
55.56%
10 / 18
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Args
100.00% covered (success)
100.00%
15 / 15
90.91% covered (success)
90.91%
20 / 22
55.56% covered (warning)
55.56%
10 / 18
100.00% covered (success)
100.00%
6 / 6
18.78
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
85.71% covered (warning)
85.71%
12 / 14
33.33% covered (danger)
33.33%
4 / 12
100.00% covered (success)
100.00%
1 / 1
8.74
 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
 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 Celemas\Cli;
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 *
17 * @api
18 */
19final class Args
20{
21    /** @var array<string, list<string>> */
22    private array $options = [];
23
24    /** @var list<string> */
25    private array $positionals = [];
26
27    /**
28     * @param list<string> $tokens
29     */
30    public function __construct(array $tokens = [])
31    {
32        foreach ($tokens as $token) {
33            if (!str_starts_with($token, '-')) {
34                $this->positionals[] = $token;
35
36                continue;
37            }
38
39            if (str_contains($token, '=')) {
40                /** @var array{0: string, 1: string} $pair */
41                $pair = explode('=', $token, limit: 2);
42                $this->options[$pair[0]][] = $pair[1];
43
44                continue;
45            }
46
47            $this->options[$token] ??= [];
48        }
49    }
50
51    public function has(string $key): bool
52    {
53        return array_key_exists($key, $this->options);
54    }
55
56    public function opt(string $key, string $default = ''): string
57    {
58        return $this->options[$key][0] ?? $default;
59    }
60
61    /**
62     * @param list<string> $default
63     * @return list<string>
64     */
65    public function opts(string $key, array $default = []): array
66    {
67        $values = $this->options[$key] ?? [];
68
69        return $values === [] ? $default : $values;
70    }
71
72    public function positional(int $index, ?string $default = null): ?string
73    {
74        return $this->positionals[$index] ?? $default;
75    }
76
77    /**
78     * @return list<string>
79     */
80    public function positionals(): array
81    {
82        return $this->positionals;
83    }
84}