Runner::showHelp()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
nc 3
nop 0
dl 0
loc 20
ccs 14
cts 14
cp 1
crap 3
rs 9.8333
c 3
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Cli;
6
7
use BadMethodCallException;
8
use Throwable;
9
use ValueError;
10
11
class Runner
12
{
13
    protected const AMBIGUOUS = 1;
14
    protected const NOTFOUND = 2;
15
16
    // The commands ordered by group and name
17
    protected array $toc = [];
18
    // The commands indexed by name only
19
    protected array $list = [];
20
    protected Output $output;
21
    protected int $longestName = 0;
22
23 13
    public function __construct(
24
        Commands $commands,
25
        string $output = 'php://output'
26
    ) {
27 13
        $this->output = new Output($output);
28 13
        $this->orderCommands($commands);
29
    }
30
31 13
    public function orderCommands(Commands $commands): void
32
    {
33 13
        $groups = [];
34
35 13
        foreach ($commands->get() as $command) {
36 13
            $name = strtolower($command->name());
37 13
            $prefix = $command->prefix();
38
39 13
            if (array_key_exists($prefix, $groups)) {
40 13
                $groups[$prefix]['commands'][$name] = $command;
41
            } else {
42 13
                $group = $command->group() ?: 'General';
43 13
                $groups[$prefix] = [
44 13
                    'title' => empty($prefix) ? 'General' : $group,
45 13
                    'commands' => [$name => $command],
46 13
                ];
47
            }
48
49 13
            $this->list[$name][] = $command;
50
51 13
            $len = strlen($prefix . ':' . $command->name());
52 13
            $this->longestName = $len > $this->longestName ? $len : $this->longestName;
53
        }
54
55 13
        ksort($groups);
56
57 13
        foreach ($groups as $name => $group) {
58 13
            $commands = $group['commands'];
59 13
            ksort($commands);
60 13
            $group['commands'] = $commands;
61 13
            $this->toc[$name] = $group;
62
        }
63
    }
64
65 3
    public function showHelp(): int
66
    {
67 3
        $script = $_SERVER['argv'][0] ?? '';
68 3
        $this->output->echo($this->output->color('Usage:', 'brown') . "\n");
69 3
        $this->output->echo("  php {$script} [prefix:]command [arguments]\n\n");
70 3
        $this->output->echo("Prefixes are optional if the command is unambiguous.\n\n");
71 3
        $this->output->echo("Available commands:\n");
72 3
        $this->echoGroup('General');
73 3
        $this->echoCommand('', 'commands', 'Lists all available commands');
74 3
        $this->echoCommand('', 'help', 'Displays this overview');
75
76 3
        foreach ($this->toc as $group) {
77 3
            $this->echoGroup($group['title']);
78
79 3
            foreach ($group['commands'] as $name => $command) {
80 3
                $this->echoCommand($command->prefix(), $name, $command->description());
81
            }
82
        }
83
84 3
        return 0;
85
    }
86
87
    /**
88
     * Displays a list of all available commands.
89
     *
90
     * With and without namespace/group. If a command appears in more than
91
     * one namespace, e. g. foo:cmd and bar:cmd, only the namespaced ones
92
     * will be displayed.
93
     */
94 1
    public function showCommands(): int
95
    {
96 1
        $list = [];
97
98 1
        foreach ($this->toc as $group) {
99 1
            foreach ($group['commands'] as $command) {
100 1
                $prefix = $command->prefix();
101
102 1
                if ($prefix) {
103 1
                    $key = "{$prefix}:" . $command->name();
104 1
                    $list[$key] = ($list[$key] ?? 0) + 1;
105
                }
106
107 1
                $name = $command->name();
108 1
                $list[$name] = ($list[$name] ?? 0) + 1;
109
            }
110
        }
111
112 1
        ksort($list);
113
114 1
        foreach ($list as $name => $count) {
115 1
            if ($count === 1) {
116 1
                $this->output->echo("{$name}\n");
117
            }
118
        }
119
120 1
        return 0;
121
    }
122
123 13
    public function run(): int|string
124
    {
125
        try {
126 13
            if (isset($_SERVER['argv'][1])) {
127 11
                $cmd = strtolower($_SERVER['argv'][1]);
128 11
                $isHelpCall = false;
129
130 11
                if ($cmd === 'help') {
131 3
                    $isHelpCall = true;
132
133 3
                    if (isset($_SERVER['argv'][2])) {
134 2
                        $cmd = strtolower($_SERVER['argv'][2]);
135
                    } else {
136 1
                        return $this->showHelp();
137
                    }
138
                }
139
140 10
                if ($cmd === 'commands') {
141 1
                    return $this->showCommands();
142
                }
143
144
                try {
145 9
                    return $this->runCommand($this->getCommand($cmd), $isHelpCall);
146 5
                } catch (ValueError $e) {
147 3
                    if ($e->getCode() === self::AMBIGUOUS) {
148 1
                        return $this->showAmbiguousMessage($cmd);
149
                    }
150
151 2
                    throw $e;
152
                }
153
154
                echo "Command not found.\n";
0 ignored issues
show
Unused Code introduced by
echo 'Command not found. ' is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
155
156
                return 1;
157
            }
158
159 2
            return $this->showHelp();
160 4
        } catch (Throwable $e) {
161 4
            $this->output->echo("Error while running command '");
162 4
            $this->output->echo($_SERVER['argv'][1] ?? '<no command given>');
163 4
            $this->output->echo("':\n\n" . $e->getMessage() . "\n");
164
165 4
            return 1;
166
        }
167
    }
168
169 3
    protected function echoGroup(string $title): void
170
    {
171 3
        $g = $this->output->color($title, 'brown');
172 3
        $this->output->echo("\n{$g}\n");
173
    }
174
175 3
    protected function echoCommand(string $prefix, string $name, string $desc): void
176
    {
177 3
        $prefix = $prefix ? $prefix . ':' : '';
178 3
        $name = $this->output->color($name, 'green');
179
180
        // The added magic number takes colorization into
181
        // account as it lengthens the string.
182 3
        $prefixedName = str_pad($prefix . $name, $this->longestName + 13);
183 3
        $this->output->echo("  {$prefixedName}{$desc}\n");
184
    }
185
186 1
    protected function showAmbiguousMessage(string $cmd): int
187
    {
188 1
        $this->output->echo("Ambiguous command. Please add the group name:\n\n");
189 1
        asort($this->list[$cmd]);
190
191 1
        foreach ($this->list[$cmd] as $command) {
192 1
            $prefix = $this->output->color($command->prefix(), 'brown');
193 1
            $name = strtolower($command->name());
194 1
            $this->output->echo("  {$prefix}:{$name}\n");
195
        }
196
197 1
        return 1;
198
    }
199
200 9
    protected function getCommand(string $cmd): Command
201
    {
202 9
        if (isset($this->list[$cmd])) {
203 3
            if (count($this->list[$cmd]) === 1) {
204 2
                return $this->list[$cmd][0];
205
            }
206
207 1
            throw new ValueError('Ambiguous command', self::AMBIGUOUS);
208
        } else {
209 6
            if (str_contains($cmd, ':')) {
210 5
                [$group, $name] = explode(':', $cmd);
211
212 5
                if (isset($this->toc[$group]['commands'][$name])) {
213 4
                    return $this->toc[$group]['commands'][$name];
214
                }
215
            }
216
        }
217
218 2
        throw new ValueError('Command not found', self::NOTFOUND);
219
    }
220
221 6
    protected function runCommand(Command $command, bool $isHelpCall): int|string
222
    {
223 6
        if ($isHelpCall) {
224 2
            $command->output($this->output)->help();
225
226 2
            return 0;
227
        }
228
229 4
        return $command->output($this->output)->run();
230
    }
231
}
232