Skip to content

Commit 54cb375

Browse files
authored
Merge pull request #50 from adhocore/develop
Develop
2 parents 6358778 + 62cbfd0 commit 54cb375

File tree

11 files changed

+255
-102
lines changed

11 files changed

+255
-102
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Framework agnostic Command Line Interface utilities and helpers for PHP. Build C
1111

1212
- Command line application made easy
1313
- Inspired by nodejs [commander](https://github.com/tj/commander.js) (thanks tj)
14+
- Zero dependency.
1415
- For PHP7 and for good
1516

1617
[![Screen Preview](https://i.imgur.com/qIYg9Zn.gif "Preview from adhocore/phalcon-ext which uses this cli package")](https://github.com/adhocore/phalcon-ext/tree/master/example/cli)
@@ -499,6 +500,22 @@ $reader->read('abc', 'trim');
499500
// Read at most first 5 chars
500501
// (if ENTER is pressed before 5 chars then further read is aborted)
501502
$reader->read('', 'trim', 5);
503+
504+
// Read but dont echo back the input
505+
$reader->readHidden($default, $callback);
506+
507+
// Read from piped stream (or STDIN) if available without waiting
508+
$reader->readPiped();
509+
510+
// Pass in a callback for if STDIN is empty
511+
// The callback recieves $reader instance and MUST return string
512+
$reader->readPiped(function ($reader) {
513+
// Wait to read a line!
514+
return $reader->read();
515+
516+
// Wait to read multi lines (until Ctrl+D pressed)
517+
return $reader->readAll();
518+
});
502519
```
503520

504521
#### Exceptions
@@ -579,3 +596,7 @@ appname subcommand <tab> # autocompletes options for subcommand (phint init <tab
579596
## License
580597

581598
> &copy; 2018, [Jitendra Adhikari](https://github.com/adhocore) | [MIT](./LICENSE)
599+
600+
### Credits
601+
602+
This project is release managed by [please](https://github.com/adhocore/please).

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,9 @@
2929
},
3030
"require-dev": {
3131
"phpunit/phpunit": "^6.0"
32+
},
33+
"scripts": {
34+
"test": "vendor/bin/phpunit",
35+
"test:cov": "vendor/bin/phpunit --coverage-text --coverage-clover coverage.xml --coverage-html vendor/cov"
3236
}
3337
}

src/Helper/OutputHelper.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,40 @@ protected function showHelp(string $for, array $items, string $header = '', stri
191191
}
192192
}
193193

194+
/**
195+
* Show usage examples of a Command.
196+
*
197+
* It replaces $0 with actual command name and properly pads ` ## ` segments.
198+
*
199+
* @param string|null $usage Usage description.
200+
*
201+
* @return self
202+
*/
203+
public function showUsage(string $usage = null): self
204+
{
205+
$usage = \str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage);
206+
207+
if (\strpos($usage, ' ## ') === false) {
208+
$this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol();
209+
210+
return $this;
211+
}
212+
213+
$lines = \explode("\n", \str_replace(['<eol>', '<eol/>', '</eol>', "\r\n"], "\n", $usage));
214+
foreach ($lines as &$pos) {
215+
$pos = \strpos(\preg_replace('~<.*?>~', '', $pos), ' ##');
216+
}
217+
218+
$maxlen = \max($lines) + 4;
219+
$usage = \preg_replace_callback('~ ## ~', function () use (&$lines, $maxlen) {
220+
return \str_pad('# ', $maxlen - \array_shift($lines), ' ', \STR_PAD_LEFT);
221+
}, $usage);
222+
223+
$this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol();
224+
225+
return $this;
226+
}
227+
194228
/**
195229
* Sort items by name. As a side effect sets max length of all names.
196230
*

src/Helper/Shell.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,11 @@ protected function checkTimeout()
152152

153153
throw new RuntimeException('Timeout occurred, process terminated.');
154154
}
155+
// @codeCoverageIgnoreStart
155156
}
156157

158+
// @codeCoverageIgnoreEnd
159+
157160
public function setOptions(string $cwd = null, array $env = null, float $timeout = null, array $otherOptions = []): self
158161
{
159162
$this->cwd = $cwd;

src/Input/Command.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,8 @@ public function showHelp()
343343

344344
$helper
345345
->showArgumentsHelp($this->allArguments())
346-
->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
347-
348-
if ($this->_usage) {
349-
$io->eol();
350-
$io->boldGreen('Usage Examples:', true)->colors($this->_usage)->eol();
351-
}
346+
->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...')
347+
->showUsage($this->_usage);
352348

353349
return $this->emit('_exit', 0);
354350
}

src/Input/Reader.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,51 @@ public function read($default = null, callable $fn = null)
5353
return $fn ? $fn($in) : $in;
5454
}
5555

56+
/**
57+
* Same like read but it reads all the lines.
58+
*
59+
* @codeCoverageIgnore
60+
*
61+
* @param callable|null $fn The validator/sanitizer callback.
62+
*
63+
* @return string
64+
*/
65+
public function readAll(callable $fn = null): string
66+
{
67+
$in = \stream_get_contents($this->stream);
68+
69+
return $fn ? $fn($in) : $in;
70+
}
71+
72+
/**
73+
* Read content piped to the stream without waiting.
74+
*
75+
* @codeCoverageIgnore
76+
*
77+
* @param callable|null $fn The callback to execute if stream is empty.
78+
*
79+
* @return string
80+
*/
81+
public function readPiped(callable $fn = null): string
82+
{
83+
$stdin = '';
84+
$read = [$this->stream];
85+
$write = [];
86+
$exept = [];
87+
88+
if (\stream_select($read, $write, $exept, 0) === 1) {
89+
while ($line = \fgets($this->stream)) {
90+
$stdin .= $line;
91+
}
92+
}
93+
94+
if ('' === $stdin) {
95+
return $fn ? $fn($this) : '';
96+
}
97+
98+
return $stdin;
99+
}
100+
56101
/**
57102
* Read a line from configured stream (or terminal) but don't echo it back.
58103
*
@@ -86,7 +131,7 @@ public function readHidden($default = null, callable $fn = null)
86131
*
87132
* @return mixed
88133
*/
89-
private function readHiddenWinOS($default = null, callable $fn = null)
134+
protected function readHiddenWinOS($default = null, callable $fn = null)
90135
{
91136
$cmd = 'powershell -Command ' . \implode('; ', \array_filter([
92137
'$pword = Read-Host -AsSecureString',

src/Output/Color.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323
*/
2424
class Color
2525
{
26-
const BLACK = 30;
27-
const RED = 31;
28-
const GREEN = 32;
29-
const YELLOW = 33;
30-
const BLUE = 34;
31-
const PURPLE = 35;
32-
const CYAN = 36;
33-
const WHITE = 37;
26+
const BLACK = 30;
27+
const RED = 31;
28+
const GREEN = 32;
29+
const YELLOW = 33;
30+
const BLUE = 34;
31+
const PURPLE = 35;
32+
const CYAN = 36;
33+
const WHITE = 37;
34+
const GRAY = 47;
35+
const DARKGRAY = 100;
3436

3537
/** @var string Cli format */
3638
protected $format = "\033[:bold:;:fg:;:bg:m:text:\033[0m";
@@ -48,7 +50,7 @@ class Color
4850
*/
4951
public function comment(string $text, array $style = []): string
5052
{
51-
return $this->line($text, ['fg' => static::BLACK, 'bold' => 1] + $style);
53+
return $this->line($text, ['fg' => static::DARKGRAY, 'bold' => 0] + $style);
5254
}
5355

5456
/**

src/Output/Table.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP-CLI package.
5+
*
6+
* (c) Jitendra Adhikari <[email protected]>
7+
* <https://github.com/adhocore>
8+
*
9+
* Licensed under MIT license.
10+
*/
11+
12+
namespace Ahc\Cli\Output;
13+
14+
use Ahc\Cli\Exception\InvalidArgumentException;
15+
use Ahc\Cli\Helper\InflectsString;
16+
17+
class Table
18+
{
19+
use InflectsString;
20+
21+
public function render(array $rows, array $styles = []): string
22+
{
23+
if ([] === $table = $this->normalize($rows)) {
24+
return '';
25+
}
26+
27+
list($head, $rows) = $table;
28+
29+
$styles = $this->normalizeStyles($styles);
30+
$title = $body = $dash = [];
31+
32+
list($start, $end) = $styles['head'];
33+
foreach ($head as $col => $size) {
34+
$dash[] = \str_repeat('-', $size + 2);
35+
$title[] = \str_pad($this->toWords($col), $size, ' ');
36+
}
37+
38+
$title = "|$start " . \implode(" $end|$start ", $title) . " $end|" . \PHP_EOL;
39+
40+
$odd = true;
41+
foreach ($rows as $row) {
42+
$parts = [];
43+
44+
list($start, $end) = $styles[['even', 'odd'][(int) $odd]];
45+
foreach ($head as $col => $size) {
46+
$parts[] = \str_pad($row[$col] ?? '', $size, ' ');
47+
}
48+
49+
$odd = !$odd;
50+
$body[] = "|$start " . \implode(" $end|$start ", $parts) . " $end|";
51+
}
52+
53+
$dash = '+' . \implode('+', $dash) . '+' . \PHP_EOL;
54+
$body = \implode(\PHP_EOL, $body) . \PHP_EOL;
55+
56+
return "$dash$title$dash$body$dash";
57+
}
58+
59+
protected function normalize(array $rows): array
60+
{
61+
$head = \reset($rows);
62+
if (empty($head)) {
63+
return [];
64+
}
65+
66+
if (!\is_array($head)) {
67+
throw new InvalidArgumentException(
68+
\sprintf('Rows must be array of assoc arrays, %s given', \gettype($head))
69+
);
70+
}
71+
72+
$head = \array_fill_keys(\array_keys($head), null);
73+
foreach ($rows as $i => &$row) {
74+
$row = \array_merge($head, $row);
75+
}
76+
77+
foreach ($head as $col => &$value) {
78+
$cols = \array_column($rows, $col);
79+
$span = \array_map('strlen', $cols);
80+
$span[] = \strlen($col);
81+
$value = \max($span);
82+
}
83+
84+
return [$head, $rows];
85+
}
86+
87+
protected function normalizeStyles(array $styles)
88+
{
89+
$default = [
90+
// styleFor => ['styleStartFn', 'end']
91+
'head' => ['', ''],
92+
'odd' => ['', ''],
93+
'even' => ['', ''],
94+
];
95+
96+
foreach ($styles as $for => $style) {
97+
if (isset($default[$for])) {
98+
$default[$for] = ['<' . \trim($style, '<> ') . '>', '</end>'];
99+
}
100+
}
101+
102+
return $default;
103+
}
104+
}

0 commit comments

Comments
 (0)