• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Utils / 29770052588

20 Jul 2026 06:55PM UTC coverage: 92.619% (-0.1%) from 92.722%
29770052588

push

github

web-flow
Merge pull request #57 from JBZoo/release/8.0

8.0.0 — PHP 8.3+ floor & lock-step major

15 of 16 new or added lines in 7 files covered. (93.75%)

106 existing lines in 13 files now uncovered.

1669 of 1802 relevant lines covered (92.62%)

41.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.21
/src/Cli.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Utils.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Utils
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Utils;
18

19
use Symfony\Component\Process\Exception\ProcessFailedException;
20
use Symfony\Component\Process\Process;
21

22
/**
23
 * @psalm-suppress UnusedClass
24
 */
25
final class Cli
26
{
27
    public const STDIN         = 0;
28
    public const STDOUT        = 1;
29
    public const STDERR        = 2;
30
    public const DEFAULT_WIDTH = 80;
31

32
    /**
33
     * Is command line mode.
34
     */
35
    public static function check(): bool
36
    {
37
        return \PHP_SAPI === 'cli' && \defined('STDOUT') && \defined('STDERR');
18✔
38
    }
39

40
    /**
41
     * Print line to std out.
42
     */
43
    public static function out(string $message, bool $addEol = true): bool
44
    {
45
        if ($addEol) {
6✔
46
            $message .= \PHP_EOL;
6✔
47
        }
48

49
        $stdout = \fopen('php://stdout', 'w');
6✔
50
        if ($stdout !== false && self::check()) {
6✔
51
            \fwrite($stdout, $message);
6✔
52

53
            return true;
6✔
54
        }
55

56
        /** @phan-suppress-next-line PhanPluginRemoveDebugEcho */
UNCOV
57
        echo $message;
×
58

UNCOV
59
        return false;
×
60
    }
61

62
    /**
63
     * Print line to std error.
64
     */
65
    public static function err(string $message, bool $addEol = true): bool
66
    {
67
        if ($addEol) {
6✔
68
            $message .= \PHP_EOL;
6✔
69
        }
70

71
        $stderr = \fopen('php://stderr', 'w');
6✔
72
        if ($stderr !== false && self::check()) {
6✔
73
            \fwrite($stderr, $message);
6✔
74

75
            return true;
6✔
76
        }
77

78
        /** @phan-suppress-next-line PhanPluginRemoveDebugEcho */
UNCOV
79
        echo $message;
×
80

UNCOV
81
        return false;
×
82
    }
83

84
    /**
85
     * Execute cli commands.
86
     */
87
    public static function exec(string $command, array $args = [], ?string $cwd = null, bool $verbose = false): string
88
    {
89
        if (!\class_exists(Process::class)) {
12✔
UNCOV
90
            throw new Exception('Symfony/Process package required for Cli::exec() method');
×
91
        }
92

93
        $realCommand = self::build($command, $args);
12✔
94

95
        if (!isStrEmpty($cwd)) {
12✔
UNCOV
96
            $realCwd = \realpath((string)$cwd);
×
UNCOV
97
            $cwd     = $realCwd === false ? null : $realCwd;
×
98
        } else {
99
            $cwd = null;
12✔
100
        }
101

102
        if ($verbose) {
12✔
UNCOV
103
            self::out("Process: {$realCommand}");
×
UNCOV
104
            self::out("CWD: {$cwd}");
×
105
        }
106

107
        try {
108
            // @phpstan-ignore-next-line
109
            if (\method_exists(Process::class, 'fromShellCommandline')) {
12✔
110
                $process = Process::fromShellCommandline($realCommand, $cwd, null, null, 3600);
12✔
111
            } else {
UNCOV
112
                $process = new Process([$realCommand], $cwd, null, null, 3600);
×
113
            }
114

115
            $process->run();
12✔
UNCOV
116
        } catch (\Exception $exception) {
×
UNCOV
117
            throw new Exception($exception->getMessage(), (int)$exception->getCode(), $exception);
×
118
        }
119

120
        // executes after the command finishes
121
        if (!$process->isSuccessful()) {
12✔
122
            throw new ProcessFailedException($process);
6✔
123
        }
124

125
        return $process->getOutput();
6✔
126
    }
127

128
    /**
129
     * Build params for cli options.
130
     */
131
    public static function build(string $command, array $args = []): string
132
    {
133
        $stringArgs  = [];
18✔
134
        $realCommand = $command;
18✔
135

136
        if (\count($args) > 0) {
18✔
137
            foreach ($args as $key => $value) {
12✔
138
                $value = \trim((string)$value);
12✔
139
                $key   = \trim((string)$key);
12✔
140

141
                if (!\str_starts_with($key, '-')) {
12✔
142
                    $key = \strlen($key) === 1 ? '-' . $key : '--' . $key;
12✔
143
                }
144

145
                if (!isStrEmpty($value)) {
12✔
146
                    $stringArgs[] = $key . '="' . \addcslashes($value, '"') . '"';
6✔
147
                } else {
148
                    $stringArgs[] = $key;
12✔
149
                }
150
            }
151
        }
152

153
        if (\count($stringArgs) > 0) {
18✔
154
            $realCommand = $command . ' ' . \implode(' ', $stringArgs);
12✔
155
        }
156

157
        return $realCommand;
18✔
158
    }
159

160
    /**
161
     * Returns true if STDOUT supports colorization.
162
     * This code has been copied and adapted from \Symfony\Component\Console\Output\OutputStream.
163
     */
164
    public static function hasColorSupport(): bool
165
    {
UNCOV
166
        if (!self::check()) {
×
UNCOV
167
            return false;
×
168
        }
169

170
        // @codeCoverageIgnoreStart
171
        if (Sys::isWin()) {
172
            return Env::bool('ANSICON') || Env::string('ConEmuANSI') === 'ON' || Env::string('TERM') === 'xterm';
173
        }
174
        // @codeCoverageIgnoreEnd
175

UNCOV
176
        return self::isInteractive(\STDOUT);
×
177
    }
178

179
    /**
180
     * Returns the number of columns of the terminal.
181
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
182
     */
183
    public static function getNumberOfColumns(): int
184
    {
185
        // @codeCoverageIgnoreStart
186
        if (Sys::isWin()) {
187
            $columns = self::DEFAULT_WIDTH;
188

189
            if (\preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', Env::string('ANSICON'), $matches) > 0) {
190
                $columns = $matches[1];
191
            } elseif (\function_exists('proc_open')) {
192
                $process = \proc_open(
193
                    'mode CON',
194
                    [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
195
                    $pipes,
196
                    null,
197
                    null,
198
                    ['suppress_errors' => true],
199
                );
200

201
                if (\is_resource($process)) {
202
                    $info = (string)\stream_get_contents($pipes[1]);
203

204
                    \fclose($pipes[1]);
205
                    \fclose($pipes[2]);
206
                    \proc_close($process);
207

208
                    if (\preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches) > 0) {
209
                        $columns = $matches[2];
210
                    }
211
                }
212
            }
213

214
            return (int)$columns - 1;
215
        }
216
        // @codeCoverageIgnoreEnd
217

218
        if (!self::isInteractive(self::STDIN)) {
12✔
219
            return self::DEFAULT_WIDTH;
12✔
220
        }
221

222
        /** @psalm-suppress ForbiddenCode */
UNCOV
223
        if ((\preg_match('#\d+ (\d+)#', (string)\shell_exec('stty size'), $match) === 1) && (int)$match[1] > 0) {
×
UNCOV
224
            return (int)$match[1];
×
225
        }
226

227
        /** @psalm-suppress ForbiddenCode */
UNCOV
228
        if ((\preg_match('#columns = (\d+);#', (string)\shell_exec('stty'), $match) === 1) && (int)$match[1] > 0) {
×
229
            return (int)$match[1];
×
230
        }
231

UNCOV
232
        return Env::int('COLUMNS', self::DEFAULT_WIDTH);
×
233
    }
234

235
    /**
236
     * Returns if the file descriptor is an interactive terminal or not.
237
     * @param int|resource $fileDescriptor
238
     */
239
    public static function isInteractive($fileDescriptor = self::STDOUT): bool
240
    {
241
        return \function_exists('posix_isatty') && @\posix_isatty($fileDescriptor);
12✔
242
    }
243
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc