Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
112 / 112
96.77% covered (success)
96.77%
90 / 93
15.65% covered (danger)
15.65%
23 / 147
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Runner
100.00% covered (success)
100.00%
112 / 112
96.77% covered (success)
96.77%
90 / 93
15.65% covered (danger)
15.65%
23 / 147
100.00% covered (success)
100.00%
10 / 10
1049.98
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
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
 orderCommands
100.00% covered (success)
100.00%
22 / 22
95.24% covered (success)
95.24%
20 / 21
1.39% covered (danger)
1.39%
1 / 72
100.00% covered (success)
100.00%
1 / 1
53.99
 showHelp
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
12 / 12
5.56% covered (danger)
5.56%
1 / 18
100.00% covered (success)
100.00%
1 / 1
26.06
 showCommands
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
14 / 14
0.00% covered (danger)
0.00%
0 / 24
100.00% covered (success)
100.00%
1 / 1
6
 run
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
17 / 17
90.00% covered (success)
90.00%
9 / 10
100.00% covered (success)
100.00%
1 / 1
9.08
 echoGroup
100.00% covered (success)
100.00%
2 / 2
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
 echoCommand
100.00% covered (success)
100.00%
5 / 5
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 showAmbiguousMessage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 getCommand
100.00% covered (success)
100.00%
11 / 11
92.31% covered (success)
92.31%
12 / 13
41.67% covered (danger)
41.67%
5 / 12
100.00% covered (success)
100.00%
1 / 1
13.15
 runCommand
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Cli;
6
7use Throwable;
8use ValueError;
9
10/**
11 * @api
12 */
13final class Runner
14{
15    protected const AMBIGUOUS = 1;
16    protected const NOTFOUND = 2;
17
18    // The commands ordered by group and name
19    private array $toc = [];
20
21    // The commands indexed by name only
22    private array $list = [];
23    private Output $output;
24    private int $longestName = 0;
25
26    public function __construct(
27        Commands $commands,
28        string $output = 'php://output',
29        string $errorOutput = 'php://stderr',
30        private bool $debug = false,
31    ) {
32        $this->output = new Output($output, $errorOutput);
33        $this->orderCommands($commands);
34    }
35
36    private function orderCommands(Commands $commands): void
37    {
38        $groups = [];
39
40        foreach ($commands->get() as $command) {
41            $name = strtolower($command->name());
42            $prefix = $command->prefix();
43
44            if (!array_key_exists($prefix, $groups)) {
45                $group = $command->group();
46                $group = $group === '' ? 'General' : $group;
47                $groups[$prefix] = [
48                    'title' => $prefix === '' ? 'General' : $group,
49                    'commands' => [],
50                ];
51            }
52
53            $groups[$prefix]['commands'][$name] = $command;
54
55            $this->list[$name][] = $command;
56
57            $len = strlen($prefix . ':' . $command->name());
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
59        }
60
61        $this->longestName = max($this->longestName, strlen('commands'));
62
63        ksort($groups);
64
65        foreach ($groups as $name => $group) {
66            $commands = $group['commands'];
67            ksort($commands);
68            $group['commands'] = $commands;
69            $this->toc[$name] = $group;
70        }
71    }
72
73    public function showHelp(): int
74    {
75        $script = $_SERVER['argv'][0] ?? '';
76        $this->output->echo($this->output->color('Usage:', 'brown') . "\n");
77        $this->output->echo("  php {$script} [prefix:]command [arguments]\n\n");
78        $this->output->echo("Prefixes are optional if the command is unambiguous.\n\n");
79        $this->output->echo("Available commands:\n");
80        $this->echoGroup('General');
81        $this->echoCommand('', 'commands', 'Lists all available commands');
82        $this->echoCommand('', 'help', 'Displays this overview');
83
84        foreach ($this->toc['']['commands'] ?? [] as $name => $command) {
85            $this->echoCommand($command->prefix(), $name, $command->description());
86        }
87
88        foreach ($this->toc as $prefix => $group) {
89            if ($prefix === '') {
90                continue;
91            }
92
93            $this->echoGroup($group['title']);
94
95            foreach ($group['commands'] as $name => $command) {
96                $this->echoCommand($command->prefix(), $name, $command->description());
97            }
98        }
99
100        return 0;
101    }
102
103    /**
104     * Displays a list of all available commands.
105     *
106     * With and without namespace/group. If a command appears in more than
107     * one namespace, e. g. foo:cmd and bar:cmd, only the namespaced ones
108     * will be displayed.
109     */
110    public function showCommands(): int
111    {
112        $list = [];
113
114        foreach ($this->toc as $group) {
115            foreach ($group['commands'] as $command) {
116                $prefix = $command->prefix();
117
118                if ($prefix) {
119                    $key = "{$prefix}:" . $command->name();
120                    $list[$key] = ($list[$key] ?? 0) + 1;
121                }
122
123                $name = $command->name();
124                $list[$name] = ($list[$name] ?? 0) + 1;
125            }
126        }
127
128        ksort($list);
129
130        foreach ($list as $name => $count) {
131            if ($count === 1) {
132                $this->output->echo("{$name}\n");
133            }
134        }
135
136        return 0;
137    }
138
139    public function run(): int
140    {
141        try {
142            $argv = $_SERVER['argv'] ?? [];
143            $arg = $argv[1] ?? null;
144
145            if ($arg === null) {
146                return $this->showHelp();
147            }
148
149            $cmd = strtolower($arg);
150            $isHelpCall = false;
151
152            if ($cmd === 'help') {
153                $isHelpCall = true;
154                $arg = $argv[2] ?? null;
155
156                if ($arg === null) {
157                    return $this->showHelp();
158                }
159
160                $cmd = strtolower($arg);
161            }
162
163            if ($cmd === 'commands') {
164                return $this->showCommands();
165            }
166
167            $args = new Args(array_slice($argv, offset: 2));
168
169            try {
170                return $this->runCommand($this->getCommand($cmd), $isHelpCall, $args);
171            } catch (ValueError $e) {
172                if ($e->getCode() === self::AMBIGUOUS) {
173                    return $this->showAmbiguousMessage($cmd);
174                }
175
176                throw $e;
177            }
178        } catch (Throwable $e) {
179            $this->output->echoErr("Error while running command '");
180            $this->output->echoErr($_SERVER['argv'][1] ?? '<no command given>');
181            $this->output->echoErr("':\n\n" . $e->getMessage() . "\n");
182
183            if ($this->debug) {
184                $this->output->echolnErr("\nTraceback:", 'yellow');
185                $this->output->echolnErr($e->getTraceAsString());
186            }
187
188            return 1;
189        }
190    }
191
192    private function echoGroup(string $title): void
193    {
194        $g = $this->output->color($title, 'brown');
195        $this->output->echo("\n{$g}\n");
196    }
197
198    private function echoCommand(string $prefix, string $name, string $desc): void
199    {
200        $prefix = $prefix ? $prefix . ':' : '';
201        $plain = $prefix . $name;
202        $colored = $prefix . $this->output->color($name, 'green');
203
204        // Pad on the visible length so columns align whether or not
205        // color escapes are present.
206        $pad = str_repeat(' ', max(2, $this->longestName + 2 - strlen($plain)));
207        $this->output->echoln("  {$colored}{$pad}{$desc}");
208    }
209
210    private function showAmbiguousMessage(string $cmd): int
211    {
212        $this->output->echoErr("Ambiguous command. Please add the group name:\n\n");
213        asort($this->list[$cmd]);
214
215        foreach ($this->list[$cmd] as $command) {
216            $prefix = $this->output->color($command->prefix(), 'brown');
217            $name = strtolower($command->name());
218            $this->output->echolnErr("  {$prefix}:{$name}");
219        }
220
221        return 1;
222    }
223
224    private function getCommand(string $cmd): Command
225    {
226        if (array_key_exists($cmd, $this->list)) {
227            if (count($this->list[$cmd]) === 1) {
228                return $this->list[$cmd][0];
229            }
230
231            throw new ValueError('Ambiguous command', self::AMBIGUOUS);
232        }
233
234        if (str_contains($cmd, ':')) {
235            /** @var array{0: string, 1: string} $parts */
236            $parts = explode(':', $cmd, limit: 2);
237            $group = $parts[0];
238            $name = $parts[1];
239
240            if (
241                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
242            ) {
243                return $this->toc[$group]['commands'][$name];
244            }
245        }
246
247        throw new ValueError('Command not found', self::NOTFOUND);
248    }
249
250    private function runCommand(Command $command, bool $isHelpCall, Args $args): int
251    {
252        if ($isHelpCall) {
253            $command->output($this->output)->help();
254
255            return 0;
256        }
257
258        return $command->output($this->output)->run($args);
259    }
260}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Runner->__construct
27        Commands $commands,
28        string $output = 'php://output',
29        string $errorOutput = 'php://stderr',
30        private bool $debug = false,
31    ) {
32        $this->output = new Output($output, $errorOutput);
33        $this->orderCommands($commands);
34    }
Runner->echoCommand
198    private function echoCommand(string $prefix, string $name, string $desc): void
199    {
200        $prefix = $prefix ? $prefix . ':' : '';
200        $prefix = $prefix ? $prefix . ':' : '';
200        $prefix = $prefix ? $prefix . ':' : '';
200        $prefix = $prefix ? $prefix . ':' : '';
201        $plain = $prefix . $name;
202        $colored = $prefix . $this->output->color($name, 'green');
203
204        // Pad on the visible length so columns align whether or not
205        // color escapes are present.
206        $pad = str_repeat(' ', max(2, $this->longestName + 2 - strlen($plain)));
206        $pad = str_repeat(' ', max(2, $this->longestName + 2 - strlen($plain)));
206        $pad = str_repeat(' ', max(2, $this->longestName + 2 - strlen($plain)));
206        $pad = str_repeat(' ', max(2, $this->longestName + 2 - strlen($plain)));
207        $this->output->echoln("  {$colored}{$pad}{$desc}");
208    }
Runner->echoGroup
192    private function echoGroup(string $title): void
193    {
194        $g = $this->output->color($title, 'brown');
195        $this->output->echo("\n{$g}\n");
196    }
Runner->getCommand
224    private function getCommand(string $cmd): Command
225    {
226        if (array_key_exists($cmd, $this->list)) {
227            if (count($this->list[$cmd]) === 1) {
228                return $this->list[$cmd][0];
231            throw new ValueError('Ambiguous command', self::AMBIGUOUS);
234        if (str_contains($cmd, ':')) {
234        if (str_contains($cmd, ':')) {
234        if (str_contains($cmd, ':')) {
234        if (str_contains($cmd, ':')) {
236            $parts = explode(':', $cmd, limit: 2);
237            $group = $parts[0];
238            $name = $parts[1];
239
240            if (
241                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
241                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
241                array_key_exists($group, $this->toc) && array_key_exists($name, $this->toc[$group]['commands'])
243                return $this->toc[$group]['commands'][$name];
247        throw new ValueError('Command not found', self::NOTFOUND);
248    }
Runner->orderCommands
36    private function orderCommands(Commands $commands): void
37    {
38        $groups = [];
39
40        foreach ($commands->get() as $command) {
40        foreach ($commands->get() as $command) {
41            $name = strtolower($command->name());
42            $prefix = $command->prefix();
43
44            if (!array_key_exists($prefix, $groups)) {
45                $group = $command->group();
46                $group = $group === '' ? 'General' : $group;
46                $group = $group === '' ? 'General' : $group;
46                $group = $group === '' ? 'General' : $group;
46                $group = $group === '' ? 'General' : $group;
47                $groups[$prefix] = [
48                    'title' => $prefix === '' ? 'General' : $group,
48                    'title' => $prefix === '' ? 'General' : $group,
48                    'title' => $prefix === '' ? 'General' : $group,
48                    'title' => $prefix === '' ? 'General' : $group,
49                    'commands' => [],
50                ];
51            }
52
53            $groups[$prefix]['commands'][$name] = $command;
53            $groups[$prefix]['commands'][$name] = $command;
54
55            $this->list[$name][] = $command;
56
57            $len = strlen($prefix . ':' . $command->name());
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
40        foreach ($commands->get() as $command) {
41            $name = strtolower($command->name());
42            $prefix = $command->prefix();
43
44            if (!array_key_exists($prefix, $groups)) {
45                $group = $command->group();
46                $group = $group === '' ? 'General' : $group;
47                $groups[$prefix] = [
48                    'title' => $prefix === '' ? 'General' : $group,
49                    'commands' => [],
50                ];
51            }
52
53            $groups[$prefix]['commands'][$name] = $command;
54
55            $this->list[$name][] = $command;
56
57            $len = strlen($prefix . ':' . $command->name());
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
40        foreach ($commands->get() as $command) {
41            $name = strtolower($command->name());
42            $prefix = $command->prefix();
43
44            if (!array_key_exists($prefix, $groups)) {
45                $group = $command->group();
46                $group = $group === '' ? 'General' : $group;
47                $groups[$prefix] = [
48                    'title' => $prefix === '' ? 'General' : $group,
49                    'commands' => [],
50                ];
51            }
52
53            $groups[$prefix]['commands'][$name] = $command;
54
55            $this->list[$name][] = $command;
56
57            $len = strlen($prefix . ':' . $command->name());
58            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
59        }
60
61        $this->longestName = max($this->longestName, strlen('commands'));
61        $this->longestName = max($this->longestName, strlen('commands'));
61        $this->longestName = max($this->longestName, strlen('commands'));
61        $this->longestName = max($this->longestName, strlen('commands'));
62
63        ksort($groups);
64
65        foreach ($groups as $name => $group) {
65        foreach ($groups as $name => $group) {
65        foreach ($groups as $name => $group) {
65        foreach ($groups as $name => $group) {
66            $commands = $group['commands'];
67            ksort($commands);
68            $group['commands'] = $commands;
69            $this->toc[$name] = $group;
70        }
71    }
Runner->run
141        try {
142            $argv = $_SERVER['argv'] ?? [];
143            $arg = $argv[1] ?? null;
144
145            if ($arg === null) {
146                return $this->showHelp();
149            $cmd = strtolower($arg);
150            $isHelpCall = false;
151
152            if ($cmd === 'help') {
153                $isHelpCall = true;
154                $arg = $argv[2] ?? null;
155
156                if ($arg === null) {
157                    return $this->showHelp();
160                $cmd = strtolower($arg);
161            }
162
163            if ($cmd === 'commands') {
163            if ($cmd === 'commands') {
164                return $this->showCommands();
167            $args = new Args(array_slice($argv, offset: 2));
168
169            try {
170                return $this->runCommand($this->getCommand($cmd), $isHelpCall, $args);
171            } catch (ValueError $e) {
172                if ($e->getCode() === self::AMBIGUOUS) {
173                    return $this->showAmbiguousMessage($cmd);
176                throw $e;
178        } catch (Throwable $e) {
179            $this->output->echoErr("Error while running command '");
180            $this->output->echoErr($_SERVER['argv'][1] ?? '<no command given>');
181            $this->output->echoErr("':\n\n" . $e->getMessage() . "\n");
182
183            if ($this->debug) {
184                $this->output->echolnErr("\nTraceback:", 'yellow');
185                $this->output->echolnErr($e->getTraceAsString());
186            }
187
188            return 1;
188            return 1;
189        }
190    }
Runner->runCommand
250    private function runCommand(Command $command, bool $isHelpCall, Args $args): int
251    {
252        if ($isHelpCall) {
253            $command->output($this->output)->help();
254
255            return 0;
258        return $command->output($this->output)->run($args);
259    }
Runner->showAmbiguousMessage
210    private function showAmbiguousMessage(string $cmd): int
211    {
212        $this->output->echoErr("Ambiguous command. Please add the group name:\n\n");
213        asort($this->list[$cmd]);
214
215        foreach ($this->list[$cmd] as $command) {
215        foreach ($this->list[$cmd] as $command) {
215        foreach ($this->list[$cmd] as $command) {
216            $prefix = $this->output->color($command->prefix(), 'brown');
215        foreach ($this->list[$cmd] as $command) {
216            $prefix = $this->output->color($command->prefix(), 'brown');
217            $name = strtolower($command->name());
218            $this->output->echolnErr("  {$prefix}:{$name}");
219        }
220
221        return 1;
222    }
Runner->showCommands
112        $list = [];
113
114        foreach ($this->toc as $group) {
114        foreach ($this->toc as $group) {
115            foreach ($group['commands'] as $command) {
115            foreach ($group['commands'] as $command) {
116                $prefix = $command->prefix();
117
118                if ($prefix) {
119                    $key = "{$prefix}:" . $command->name();
120                    $list[$key] = ($list[$key] ?? 0) + 1;
121                }
122
123                $name = $command->name();
115            foreach ($group['commands'] as $command) {
116                $prefix = $command->prefix();
117
118                if ($prefix) {
119                    $key = "{$prefix}:" . $command->name();
120                    $list[$key] = ($list[$key] ?? 0) + 1;
121                }
122
123                $name = $command->name();
114        foreach ($this->toc as $group) {
115            foreach ($group['commands'] as $command) {
114        foreach ($this->toc as $group) {
115            foreach ($group['commands'] as $command) {
116                $prefix = $command->prefix();
117
118                if ($prefix) {
119                    $key = "{$prefix}:" . $command->name();
120                    $list[$key] = ($list[$key] ?? 0) + 1;
121                }
122
123                $name = $command->name();
124                $list[$name] = ($list[$name] ?? 0) + 1;
125            }
126        }
127
128        ksort($list);
129
130        foreach ($list as $name => $count) {
130        foreach ($list as $name => $count) {
130        foreach ($list as $name => $count) {
131            if ($count === 1) {
130        foreach ($list as $name => $count) {
131            if ($count === 1) {
132                $this->output->echo("{$name}\n");
130        foreach ($list as $name => $count) {
130        foreach ($list as $name => $count) {
131            if ($count === 1) {
132                $this->output->echo("{$name}\n");
133            }
134        }
135
136        return 0;
137    }
Runner->showHelp
75        $script = $_SERVER['argv'][0] ?? '';
76        $this->output->echo($this->output->color('Usage:', 'brown') . "\n");
77        $this->output->echo("  php {$script} [prefix:]command [arguments]\n\n");
78        $this->output->echo("Prefixes are optional if the command is unambiguous.\n\n");
79        $this->output->echo("Available commands:\n");
80        $this->echoGroup('General');
81        $this->echoCommand('', 'commands', 'Lists all available commands');
82        $this->echoCommand('', 'help', 'Displays this overview');
83
84        foreach ($this->toc['']['commands'] ?? [] as $name => $command) {
84        foreach ($this->toc['']['commands'] ?? [] as $name => $command) {
84        foreach ($this->toc['']['commands'] ?? [] as $name => $command) {
84        foreach ($this->toc['']['commands'] ?? [] as $name => $command) {
85            $this->echoCommand($command->prefix(), $name, $command->description());
86        }
87
88        foreach ($this->toc as $prefix => $group) {
88        foreach ($this->toc as $prefix => $group) {
88        foreach ($this->toc as $prefix => $group) {
89            if ($prefix === '') {
90                continue;
93            $this->echoGroup($group['title']);
94
95            foreach ($group['commands'] as $name => $command) {
95            foreach ($group['commands'] as $name => $command) {
95            foreach ($group['commands'] as $name => $command) {
88        foreach ($this->toc as $prefix => $group) {
89            if ($prefix === '') {
90                continue;
91            }
92
93            $this->echoGroup($group['title']);
94
95            foreach ($group['commands'] as $name => $command) {
88        foreach ($this->toc as $prefix => $group) {
89            if ($prefix === '') {
90                continue;
91            }
92
93            $this->echoGroup($group['title']);
94
95            foreach ($group['commands'] as $name => $command) {
96                $this->echoCommand($command->prefix(), $name, $command->description());
97            }
98        }
99
100        return 0;
101    }