Passed
Push — master ( 856100...c79340 )
by Alexander
02:53
created

Cli::getConvertedOptions()   B

Complexity

Conditions 9
Paths 72

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 23
c 1
b 0
f 0
nc 72
nop 1
dl 0
loc 44
rs 8.0555
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 $self;
20
    /** @var string */
21
    protected $command;
22
    /** @var array<string, mixed> */
23
    protected $args;
24
    /** @var bool */
25
    protected $echo = true;
26
    /** @var string */
27
    protected $out = '';
28
    /** @var int */
29
    private $last_index = 0;
30
31
    /**
32
     * @param array<string, string> $map
33
     */
34
    public function __construct(array $map)
35
    {
36
        [$self, $command, $args] = $this->getConvertedOptions($map);
37
        // var_dump(compact('self', 'command', 'args'));
38
        $this->self = $self;
39
        $this->command = $command;
40
        $this->args = $args;
41
    }
42
43
    /**
44
     * @psalm-suppress MissingClosureParamType
45
     * @param array<string, string> $options_map
46
     * @return array
47
     */
48
    protected function getConvertedOptions(array $options_map): array
49
    {
50
        $argv = $this->getGlobalArgV();
51
52
        // Grab arguments from `$argv` using native `getopt`
53
        $int_filter = function($k): bool { return is_int($k); };
54
        $just_longs = array_filter($options_map, $int_filter, ARRAY_FILTER_USE_KEY);
55
        $string_filter = function($k): bool { return is_string($k); };
56
        $options_map = array_filter($options_map, $string_filter, ARRAY_FILTER_USE_KEY);
57
        $short_options = join('', array_keys($options_map));
58
        $long_options = array_merge(array_values($options_map), $just_longs);
59
        $args = $this->getOpt($short_options, $long_options);
60
61
        /**
62
         * Map words to characters to only return word arguments
63
         * @var array<string, string> $short_to_long_map
64
         */
65
        $short_to_long_map = [];
66
        foreach ($options_map as $short => $long) {
67
            $short_to_long_map[trim($short, ':')] = trim($long, ':');
68
        }
69
        foreach ($short_to_long_map as $short => $long) {
70
            if (isset($args[$short])) {
71
                $args[$long] = $args[$short];
72
                unset($args[$short]);
73
            }
74
        }
75
76
        // Flip no-value option present to a true value
77
        // A no-value option that is present multiple times converted to a number
78
        foreach ($args as $key => $value) {
79
            if ($value === false) $args[$key] = true;
80
            elseif (is_array($value)) $args[$key] = count($args[$key]);
81
        }
82
83
        // Non present values get false value
84
        foreach (array_values($long_options) as $long) {
85
            $long = trim($long, ':');
86
            if (array_key_exists($long, $args) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
87
                $args[$long] = false;
88
            }
89
        }
90
91
        return [$argv[0], array_pop($argv), $args];
92
    }
93
94
    /**
95
     * @codeCoverageIgnore
96
     * @param string $s
97
     */
98
    protected function out(string $s): void
99
    {
100
        if ($this->echo)
101
            echo $s . PHP_EOL;
102
        else
103
            $this->out .= $s . PHP_EOL;
104
    }
105
106
    /**
107
     */
108
    protected function input(): void
109
    {
110
        $this->out("Running Command: [ {$this->command} ] from [ {$this->self} ]");
111
        $flags = join(', ', array_keys(array_filter($this->args, function($val): bool {
112
            return $val === true;
113
        })));
114
        if (empty($flags) == false) $this->out("Flags: [ {$flags} ]");
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
115
        $options = urldecode(http_build_query(array_filter($this->args, function($val): bool {
116
            return is_bool($val) === false;
117
        }), '', ", "));
118
        if (empty($options) == false) $this->out("Options: [ {$options} ]");
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
119
        $this->out("");
120
    }
121
122
    /**
123
     * @return string
124
     */
125
    public function render(): string
126
    {
127
        return $this->out;
128
    }
129
130
    /**
131
     * @param bool $echo
132
     * @return self
133
     */
134
    public function run(bool $echo = false): self
135
    {
136
        $this->echo = $echo; $this->out = '';
137
        $this->out(static::NAME . " " . static::VERSION);
138
        $this->out("");
139
140
        $command_method = "command_{$this->command}";
141
        if (method_exists($this, $command_method) === false) {
142
            throw new \Exception("Command {$this->command} does not exist");
143
        }
144
        if ($this->verbose()) $this->input();
145
        $this->$command_method();
146
        return $this;
147
    }
148
149
    /**
150
     * @param int $level
151
     * @return bool
152
     */
153
    public function verbose(int $level = 1): bool
154
    {
155
        if (array_key_exists('verbose', $this->args)) {
156
            if (is_bool($this->args['verbose'])) {
157
                return $level === 1 && $this->args['verbose'];
158
            } elseif (is_numeric($this->args['verbose'])) {
159
                return $this->args['verbose'] >= $level;
160
            }
161
        }
162
        return false;
163
    }
164
165
    /**
166
     * @return bool
167
     */
168
    public function quiet(): bool
169
    {
170
        if (array_key_exists('quiet', $this->args)) {
171
            return $this->args['quiet'] === true || is_numeric($this->args['quiet']);
172
        }
173
        return false;
174
    }
175
176
    /**
177
     * @param string $label
178
     * @return null|string
179
     */
180
    public function arg(string $label): ?string
181
    {
182
        return array_key_exists($label, $this->args) ? $this->args[$label] : null;
183
    }
184
185
    /**
186
     * Wrapper for grabbing the global `$argv` for testing purposes
187
     *
188
     * @codeCoverageIgnore
189
     * @return array
190
     */
191
    protected function getGlobalArgV(): array
192
    {
193
        global $argv;
194
        return $argv;
195
    }
196
197
    /**
198
     * Wrapper for native `getopt` for testing purposes
199
     *
200
     * @codeCoverageIgnore
201
     * @param string $short_options
202
     * @param string[] $long_options
203
     * @return array
204
     */
205
    protected function getOpt(string $short_options, array $long_options): array
206
    {
207
        return getopt($short_options, $long_options, $this->last_index);
208
    }
209
210
    /**
211
     * Echo out a progressbar:
212
     *
213
     * [====================================>       ] 70% (140/200)
214
     *
215
     * @codeCoverageIgnore
216
     * @param int $counter
217
     * @param int $total
218
     */
219
    protected function progressBar(int $counter, int $total): void
220
    {
221
        $length = (int) ( ($counter/$total) * 100 );
222
        $active = ($counter === $total) ? '' : '>';
223
        $loadbar = sprintf(
224
            "\r[%-100s] %d%% (%s/%s)",
225
            str_repeat("=", $length) . $active,
226
            $length,
227
            number_format($counter),
228
            number_format($total)
229
        );
230
        echo $loadbar . (($counter === $total) ? PHP_EOL : '');
231
    }
232
}
233