Cli   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Importance

Changes 5
Bugs 2 Features 0
Metric Value
eloc 91
c 5
b 2
f 0
dl 0
loc 245
rs 9.6
wmc 35

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A arg() 0 3 2
A getGlobalArgV() 0 4 1
B getConvertedOptions() 0 59 9
A input() 0 16 3
A verbose() 0 10 5
A quiet() 0 6 3
A run() 0 16 3
A render() 0 3 1
A out() 0 6 2
A progressBar() 0 12 3
A getOpt() 0 7 2
1
<?php
2
3
namespace alkemann\h2l\util;
4
5
/**
6
 * A "Dispatcher" of terminal actions
7
 *
8
 * @package alkemann\h2l\util
9
 */
10
abstract class Cli
11
{
12
    /**
13
     * Replace these in subclass to define name and version
14
     */
15
    public const NAME = "H2Cli";
16
    public const VERSION = "v0.1";
17
18
    /** @var string */
19
    protected string $self;
20
    /** @var string */
21
    protected string $command;
22
    /** @var array<string, mixed> */
23
    protected array $args;
24
    /** @var bool */
25
    protected bool $echo = true;
26
    /** @var string */
27
    protected string $out = '';
28
    /** @var int */
29
    private int $last_index = 0;
30
31
    /**
32
     * @param array<string, string> $map
33
     */
34
    public function __construct(array $map)
35
    {
36
        [$this->self, $this->command, $this->args] = $this->getConvertedOptions($map);
37
    }
38
39
    /**
40
     * @psalm-suppress MissingClosureParamType
41
     * @param array<string|int, mixed> $options_map
42
     * @return array
43
     */
44
    protected function getConvertedOptions(array $options_map): array
45
    {
46
        $argv = $this->getGlobalArgV();
47
        $just_longs = array_filter(
48
            $options_map,
49
            /**
50
             * @param mixed $k
51
             * @return bool
52
             */
53
            static fn($k): bool => is_int($k),
54
            ARRAY_FILTER_USE_KEY
55
        );
56
        $options_map = array_filter(
57
            $options_map,
58
            /**
59
             * @param mixed $k
60
             * @return bool
61
             */
62
            static fn($k): bool => is_string($k),
63
            ARRAY_FILTER_USE_KEY
64
        );
65
        $short_options = join('', array_keys($options_map));
66
        $long_options = array_merge(array_values($options_map), $just_longs);
67
        $args = $this->getOpt($short_options, $long_options);
68
69
        /**
70
         * Map words to characters to only return word arguments
71
         * @var array<string, string> $short_to_long_map
72
         */
73
        $short_to_long_map = [];
74
        foreach ($options_map as $short => $long) {
75
            $short_to_long_map[trim($short, ':')] = trim($long, ':');
76
        }
77
        foreach ($short_to_long_map as $short => $long) {
78
            if (isset($args[$short])) {
79
                $args[$long] = $args[$short];
80
                unset($args[$short]);
81
            }
82
        }
83
84
        // Flip no-value option present to a true value
85
        // A no-value option that is present multiple times converted to a number
86
        foreach ($args as $key => $value) {
87
            if ($value === false) {
88
                $args[$key] = true;
89
            } elseif (is_array($value)) {
90
                $args[$key] = count($value);
91
            }
92
        }
93
94
        // Non present values get false value
95
        foreach (array_values($long_options) as $long) {
96
            $long = trim($long, ':');
97
            if (array_key_exists($long, $args) === false) {
98
                $args[$long] = false;
99
            }
100
        }
101
102
        return [$argv[0], array_pop($argv), $args];
103
    }
104
105
    /**
106
     * @codeCoverageIgnore
107
     * @param string $s
108
     */
109
    protected function out(string $s): void
110
    {
111
        if ($this->echo) {
112
            echo $s . PHP_EOL;
113
        } else {
114
            $this->out .= $s . PHP_EOL;
115
        }
116
    }
117
118
    /**
119
     */
120
    protected function input(): void
121
    {
122
        $this->out("Running Command: [ {$this->command} ] from [ {$this->self} ]");
123
        $flags = join(', ', array_keys(array_filter($this->args, function($val): bool {
124
            return $val === true;
125
        })));
126
        if (empty($flags) === false) {
127
            $this->out("Flags: [ {$flags} ]");
128
        }
129
        $options = urldecode(http_build_query(array_filter($this->args, function($val): bool {
130
            return is_bool($val) === false;
131
        }), '', ", "));
132
        if (empty($options) === false) {
133
            $this->out("Options: [ {$options} ]");
134
        }
135
        $this->out("");
136
    }
137
138
    /**
139
     * @return string
140
     */
141
    public function render(): string
142
    {
143
        return $this->out;
144
    }
145
146
    /**
147
     * @param bool $echo
148
     * @return Cli
149
     * @throws \Exception for unknown commands
150
     */
151
    public function run(bool $echo = false): self
152
    {
153
        $this->echo = $echo;
154
        $this->out = '';
155
        $this->out(static::NAME . " " . static::VERSION);
156
        $this->out("");
157
158
        $command_method = "command_{$this->command}";
159
        if (method_exists($this, $command_method) === false) {
160
            throw new \Exception("Command {$this->command} does not exist");
161
        }
162
        if ($this->verbose()) {
163
            $this->input();
164
        }
165
        $this->$command_method();
166
        return $this;
167
    }
168
169
    /**
170
     * @param int $level
171
     * @return bool
172
     */
173
    public function verbose(int $level = 1): bool
174
    {
175
        if (array_key_exists('verbose', $this->args)) {
176
            if (is_bool($this->args['verbose'])) {
177
                return $level === 1 && $this->args['verbose'];
178
            } elseif (is_numeric($this->args['verbose'])) {
179
                return $this->args['verbose'] >= $level;
180
            }
181
        }
182
        return false;
183
    }
184
185
    /**
186
     * @return bool
187
     */
188
    public function quiet(): bool
189
    {
190
        if (array_key_exists('quiet', $this->args)) {
191
            return $this->args['quiet'] === true || is_numeric($this->args['quiet']);
192
        }
193
        return false;
194
    }
195
196
    /**
197
     * @param string $label
198
     * @return null|string
199
     */
200
    public function arg(string $label): ?string
201
    {
202
        return array_key_exists($label, $this->args) ? $this->args[$label] : null;
203
    }
204
205
    /**
206
     * Wrapper for grabbing the global `$argv` for testing purposes
207
     *
208
     * @codeCoverageIgnore
209
     * @return array<string>
210
     */
211
    protected function getGlobalArgV(): array
212
    {
213
        global $argv;
214
        return $argv;
215
    }
216
217
    /**
218
     * Wrapper for native `getopt` for testing purposes
219
     *
220
     * @codeCoverageIgnore
221
     * @param string $short_options
222
     * @param string[] $long_options
223
     * @return array<string, array<int, mixed>|string|false>
224
     */
225
    protected function getOpt(string $short_options, array $long_options): array
226
    {
227
        $o = getopt($short_options, $long_options, $this->last_index);
228
        if ($o === false) {
229
            return [];
230
        }
231
        return $o;
232
    }
233
234
    /**
235
     * Echo out a progressbar:
236
     *
237
     * [====================================>       ] 70% (140/200)
238
     *
239
     * @codeCoverageIgnore
240
     * @param int $counter
241
     * @param int $total
242
     */
243
    protected function progressBar(int $counter, int $total): void
244
    {
245
        $length = (int) (($counter/$total) * 100);
246
        $active = ($counter === $total) ? '' : '>';
247
        $loadbar = sprintf(
248
            "\r[%-100s] %d%% (%s/%s)",
249
            str_repeat("=", $length) . $active,
250
            $length,
251
            number_format($counter),
252
            number_format($total)
253
        );
254
        echo $loadbar . (($counter === $total) ? PHP_EOL : '');
255
    }
256
}
257