487 lines
12 KiB
PHP
487 lines
12 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Console class file.
|
||
|
*
|
||
|
* @author Antonio Ramirez <amigo.cobos@gmail.com>
|
||
|
* @author Nofriandi Ramenta <nramenta@gmail.com>
|
||
|
* @link http://www.ramirezcobos.com/
|
||
|
* @link http://www.2amigos.us/
|
||
|
* @link https://github.com/nramenta/clio
|
||
|
* @copyright 2013 2amigOS! Consultation Group LLC
|
||
|
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
|
||
|
*/
|
||
|
namespace Yiinitializr\Cli;
|
||
|
/**
|
||
|
* Console provides a set of useful functions to work on the terminal
|
||
|
*
|
||
|
* @author Antonio Ramirez <amigo.cobos@gmail.com>
|
||
|
* @package Yiinitializr.Cli
|
||
|
* @since 1.0
|
||
|
*/
|
||
|
class Console
|
||
|
{
|
||
|
/**
|
||
|
* Text foreground colors.
|
||
|
*/
|
||
|
protected static $FGCOLOR = array(
|
||
|
'black' => 30,
|
||
|
'red' => 31,
|
||
|
'green' => 32,
|
||
|
'brown' => 33,
|
||
|
'blue' => 34,
|
||
|
'purple' => 35,
|
||
|
'cyan' => 36,
|
||
|
'grey' => 37,
|
||
|
'yellow' => 33,
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Text styling.
|
||
|
*/
|
||
|
protected static $STYLE = array(
|
||
|
'normal' => 0,
|
||
|
'bold' => 1,
|
||
|
'light' => 1,
|
||
|
'underscore' => 4,
|
||
|
'underline' => 4,
|
||
|
'blink' => 5,
|
||
|
'inverse' => 6,
|
||
|
'hidden' => 8,
|
||
|
'concealed' => 8,
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Text background color.
|
||
|
*/
|
||
|
protected static $BGCOLOR = array(
|
||
|
'black' => 40,
|
||
|
'red' => 41,
|
||
|
'green' => 42,
|
||
|
'brown' => 43,
|
||
|
'yellow' => 43,
|
||
|
'blue' => 44,
|
||
|
'purple' => 45,
|
||
|
'cyan' => 46,
|
||
|
'grey' => 47,
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Color specifier conversion table. Taken from PEAR's Console_Color.
|
||
|
*/
|
||
|
protected static $CONVERSIONS = array(
|
||
|
'%y' => array('yellow', null, null),
|
||
|
'%g' => array('green', null, null),
|
||
|
'%b' => array('blue', null, null),
|
||
|
'%r' => array('red', null, null),
|
||
|
'%p' => array('purple', null, null),
|
||
|
'%m' => array('purple', null, null),
|
||
|
'%c' => array('cyan', null, null),
|
||
|
'%w' => array('grey', null, null),
|
||
|
'%k' => array('black', null, null),
|
||
|
'%n' => array('reset', null, null),
|
||
|
'%Y' => array('yellow', 'light', null),
|
||
|
'%G' => array('green', 'light', null),
|
||
|
'%B' => array('blue', 'light', null),
|
||
|
'%R' => array('red', 'light', null),
|
||
|
'%P' => array('purple', 'light', null),
|
||
|
'%M' => array('purple', 'light', null),
|
||
|
'%C' => array('cyan', 'light', null),
|
||
|
'%W' => array('grey', 'light', null),
|
||
|
'%K' => array('black', 'light', null),
|
||
|
'%N' => array('reset', 'light', null),
|
||
|
'%3' => array(null, null, 'yellow'),
|
||
|
'%2' => array(null, null, 'green'),
|
||
|
'%4' => array(null, null, 'blue'),
|
||
|
'%1' => array(null, null, 'red'),
|
||
|
'%5' => array(null, null, 'purple'),
|
||
|
'%6' => array(null, null, 'cyan'),
|
||
|
'%7' => array(null, null, 'grey'),
|
||
|
'%0' => array(null, null, 'black'),
|
||
|
'%F' => array(null, 'blink', null),
|
||
|
'%U' => array(null, 'underline', null),
|
||
|
'%8' => array(null, 'inverse', null),
|
||
|
'%9' => array(null, 'bold', null),
|
||
|
'%_' => array(null, 'bold', null),
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* Create ANSI-control codes for text foreground and background colors, and
|
||
|
* styling.
|
||
|
*
|
||
|
* @param string $fgcolor Text foreground color
|
||
|
* @param string $style Text style
|
||
|
* @param string $bgcolor Text background color
|
||
|
*
|
||
|
* @return string ANSI-control code
|
||
|
*/
|
||
|
public static function color($fgcolor, $style, $bgcolor)
|
||
|
{
|
||
|
$code = array();
|
||
|
if ($fgcolor == 'reset') {
|
||
|
return "\033[0m";
|
||
|
}
|
||
|
if (isset(static::$FGCOLOR[$fgcolor])) {
|
||
|
$code[] = static::$FGCOLOR[$fgcolor];
|
||
|
}
|
||
|
if (isset(static::$STYLE[$style])) {
|
||
|
$code[] = static::$STYLE[$style];
|
||
|
}
|
||
|
if (isset(static::$BGCOLOR[$bgcolor])) {
|
||
|
$code[] = static::$BGCOLOR[$bgcolor];
|
||
|
}
|
||
|
if (empty($code)) {
|
||
|
$code[] = 0;
|
||
|
}
|
||
|
return "\033[" . implode(';', $code) . 'm';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* aken from PEAR's Console_Color:
|
||
|
*
|
||
|
* Converts colorcodes in the format %y (for yellow) into ansi-control
|
||
|
* codes. The conversion table is: ('bold' meaning 'light' on some
|
||
|
* terminals). It's almost the same conversion table irssi uses.
|
||
|
* <pre>
|
||
|
* text text background
|
||
|
* ------------------------------------------------
|
||
|
* %k %K %0 black dark grey black
|
||
|
* %r %R %1 red bold red red
|
||
|
* %g %G %2 green bold green green
|
||
|
* %y %Y %3 yellow bold yellow yellow
|
||
|
* %b %B %4 blue bold blue blue
|
||
|
* %m %M %5 magenta bold magenta magenta
|
||
|
* %p %P magenta (think: purple)
|
||
|
* %c %C %6 cyan bold cyan cyan
|
||
|
* %w %W %7 white bold white white
|
||
|
*
|
||
|
* %F Blinking, Flashing
|
||
|
* %U Underline
|
||
|
* %8 Reverse
|
||
|
* %_,%9 Bold
|
||
|
*
|
||
|
* %n Resets the color
|
||
|
* %% A single %
|
||
|
* </pre>
|
||
|
* First param is the string to convert, second is an optional flag if
|
||
|
* colors should be used. It defaults to true, if set to false, the
|
||
|
* colorcodes will just be removed (And %% will be transformed into %)
|
||
|
*
|
||
|
* @param $text
|
||
|
* @param bool $color
|
||
|
* @return mixed
|
||
|
*/
|
||
|
public static function colorize($text, $color = true)
|
||
|
{
|
||
|
$text = str_replace('%%', '% ', $text);
|
||
|
foreach (static::$CONVERSIONS as $key => $value) {
|
||
|
list($fgcolor, $style, $bgcolor) = $value;
|
||
|
$text = str_replace(
|
||
|
$key,
|
||
|
$color ? static::color($fgcolor, $style, $bgcolor) : '',
|
||
|
$text
|
||
|
);
|
||
|
}
|
||
|
return str_replace('% ', '%', $text);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Strips a string from color specifiers.
|
||
|
*
|
||
|
* @param string $text String to strip
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function decolorize($text)
|
||
|
{
|
||
|
return static::colorize($text, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Strips a string of ansi-control codes.
|
||
|
*
|
||
|
* @param string $text String to strip
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function strip($text)
|
||
|
{
|
||
|
return preg_replace('/\033\[(\d+)(;\d+)*m/', '', $text);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets input from STDIN and returns a string right-trimmed for EOLs.
|
||
|
*
|
||
|
* @param bool $raw If set to true, returns the raw string without trimming
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function stdin($raw = false)
|
||
|
{
|
||
|
return $raw ? fgets(STDIN) : rtrim(fgets(STDIN), PHP_EOL);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prints text to STDOUT.
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param bool $raw
|
||
|
*
|
||
|
* @return int|false Number of bytes printed or false on error
|
||
|
*/
|
||
|
public static function stdout($text, $raw = false)
|
||
|
{
|
||
|
if ($raw) {
|
||
|
return fwrite(STDOUT, $text);
|
||
|
} elseif (extension_loaded('posix') && posix_isatty(STDOUT)) {
|
||
|
return fwrite(STDOUT, static::colorize($text));
|
||
|
} else {
|
||
|
return fwrite(STDOUT, static::decolorize($text));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prints text to STDERR.
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param bool $raw
|
||
|
*
|
||
|
* @return int|false Number of bytes printed or false on error
|
||
|
*/
|
||
|
public static function stderr($text, $raw = false)
|
||
|
{
|
||
|
if ($raw) {
|
||
|
return fwrite(STDERR, $text);
|
||
|
} elseif (extension_loaded('posix') && posix_isatty(STDERR)) {
|
||
|
return fwrite(STDERR, static::colorize($text));
|
||
|
} else {
|
||
|
return fwrite(STDERR, static::decolorize($text));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prints text to STDERR appended with a PHP_EOL.
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param bool $raw
|
||
|
*
|
||
|
* @return int|false Number of bytes printed or false on error
|
||
|
*/
|
||
|
public static function error($text = null, $raw = false)
|
||
|
{
|
||
|
return static::stderr($text . PHP_EOL, $raw);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Asks the user for input. Ends when the user types a PHP_EOL. Optionally
|
||
|
* provide a prompt.
|
||
|
*
|
||
|
* @param string $prompt String prompt (optional)
|
||
|
*
|
||
|
* @return string User input
|
||
|
*/
|
||
|
public static function input($prompt = null)
|
||
|
{
|
||
|
if (isset($prompt)) {
|
||
|
static::stdout($prompt);
|
||
|
}
|
||
|
return static::stdin();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prints text to STDOUT appended with a PHP_EOL.
|
||
|
*
|
||
|
* @param string $text
|
||
|
* @param bool $raw
|
||
|
*
|
||
|
* @return int|false Number of bytes printed or false on error
|
||
|
*/
|
||
|
public static function output($text = null, $raw = false)
|
||
|
{
|
||
|
return static::stdout($text . PHP_EOL, $raw);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Prompts the user for input
|
||
|
*
|
||
|
* @param string $text Prompt string
|
||
|
* @param array $options Set of options
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function prompt($text, $options = array())
|
||
|
{
|
||
|
$options = $options + array(
|
||
|
'required' => false,
|
||
|
'default' => null,
|
||
|
'pattern' => null,
|
||
|
'validator' => null,
|
||
|
'error' => 'Input unacceptable.',
|
||
|
);
|
||
|
|
||
|
top:
|
||
|
if ($options['default']) {
|
||
|
$input = static::input("$text [" . $options['default'] . ']: ');
|
||
|
} else {
|
||
|
$input = static::input("$text: ");
|
||
|
}
|
||
|
|
||
|
if (!strlen($input)) {
|
||
|
if (isset($options['default'])) {
|
||
|
$input = $options['default'];
|
||
|
} elseif ($options['required']) {
|
||
|
static::output($options['error']);
|
||
|
goto top;
|
||
|
}
|
||
|
} elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
|
||
|
static::output($options['error']);
|
||
|
goto top;
|
||
|
} elseif ($options['validator'] &&
|
||
|
!call_user_func_array($options['validator'], array($input, &$error))) {
|
||
|
static::output(isset($error) ? $error : $options['error']);
|
||
|
goto top;
|
||
|
}
|
||
|
|
||
|
return $input;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Asks the user for a simple yes/no confirmation.
|
||
|
*
|
||
|
* @param string $text Prompt string
|
||
|
*
|
||
|
* @return bool Either true or false
|
||
|
*/
|
||
|
public static function confirm($text)
|
||
|
{
|
||
|
top:
|
||
|
$input = strtolower(static::input("$text [y/n]: "));
|
||
|
if (!in_array($input, array('y', 'n'))) goto top;
|
||
|
return $input === 'y' ? true : false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gives the user an option to choose from. Giving '?' as an input will show
|
||
|
* a list of options to choose from and their explanations.
|
||
|
*
|
||
|
* @param string $text Prompt string
|
||
|
* @param array $options Key-value array of options to choose from
|
||
|
*
|
||
|
* @return string An option character the user chose
|
||
|
*/
|
||
|
public static function select($text, $options = array())
|
||
|
{
|
||
|
top:
|
||
|
static::stdout("$text [" . implode(',', array_keys($options)) . ",?]: ");
|
||
|
$input = static::stdin();
|
||
|
if ($input === '?') {
|
||
|
foreach ($options as $key => $value) {
|
||
|
echo " $key - $value\n";
|
||
|
}
|
||
|
echo " ? - Show help\n";
|
||
|
goto top;
|
||
|
} elseif (!in_array($input, array_keys($options))) goto top;
|
||
|
return $input;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute a Closure as another process in the background while showing a
|
||
|
* status update. The status update can be an indefinite spinner or a string
|
||
|
* periodically sent from the background process, depending on whether the
|
||
|
* provided Closure object has a $socket parameter or not. Messaging to the
|
||
|
* main process is done by socket_* functions. The return value is either
|
||
|
* the return value of the background process, or false if the process fork
|
||
|
* failed.
|
||
|
*
|
||
|
* @param callable $callable Closure object
|
||
|
* @return bool|int
|
||
|
* @throws \Exception
|
||
|
*/
|
||
|
public static function work(\Closure $callable)
|
||
|
{
|
||
|
if (!extension_loaded('pcntl')) {
|
||
|
throw new \Exception('pcntl extension required');
|
||
|
}
|
||
|
|
||
|
if (!extension_loaded('sockets')) {
|
||
|
throw new \Exception('sockets extension required');
|
||
|
}
|
||
|
|
||
|
$spinner = array('|', '/', '-', '\\');
|
||
|
$i = 0; $l = count($spinner);
|
||
|
$delay = 100000;
|
||
|
|
||
|
$func = new \ReflectionFunction($callable);
|
||
|
|
||
|
$socket = (bool)$func->getNumberOfParameters();
|
||
|
|
||
|
if ($socket) {
|
||
|
$sockets = array();
|
||
|
if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === false) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$pid = pcntl_fork();
|
||
|
|
||
|
if ($pid > 0) {
|
||
|
$done = false;
|
||
|
$retval = 0;
|
||
|
pcntl_signal(SIGCHLD, function() use ($pid, &$done, &$retval) {
|
||
|
$child_pid = pcntl_waitpid($pid, $status);
|
||
|
if (pcntl_wifexited($status)) {
|
||
|
$retval = pcntl_wexitstatus($status);
|
||
|
}
|
||
|
$done = true;
|
||
|
});
|
||
|
|
||
|
if ($socket) {
|
||
|
$text = '';
|
||
|
while (!$done) {
|
||
|
$r = array($sockets[1]);
|
||
|
$w = null;
|
||
|
$e = null;
|
||
|
if ($status = socket_select($r, $w, $e, 0)) {
|
||
|
$data = socket_read($sockets[1], 4096, PHP_NORMAL_READ);
|
||
|
if ($data === false) {
|
||
|
throw new \Exception(
|
||
|
sprintf(
|
||
|
'socket write error %s',
|
||
|
socket_strerror(socket_last_error($sockets[1]))
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
echo str_repeat(chr(8), strlen($text));
|
||
|
$text = rtrim($data, "\n");
|
||
|
Console::stdout($text);
|
||
|
} else {
|
||
|
pcntl_signal_dispatch();
|
||
|
}
|
||
|
usleep($delay);
|
||
|
}
|
||
|
echo str_repeat(chr(8), strlen($text));
|
||
|
socket_close($sockets[0]);
|
||
|
socket_close($sockets[1]);
|
||
|
} else {
|
||
|
while (!$done) {
|
||
|
pcntl_signal_dispatch();
|
||
|
echo $spinner[$i];
|
||
|
usleep($delay);
|
||
|
echo chr(8);
|
||
|
$i = $i === $l - 1 ? 0 : $i + 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $retval;
|
||
|
} elseif ($pid === 0) {
|
||
|
if ($socket) {
|
||
|
call_user_func($callable, $sockets[0]);
|
||
|
} else {
|
||
|
call_user_func($callable);
|
||
|
}
|
||
|
exit;
|
||
|
} else {
|
||
|
// Unable to fork process.
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|