基础代码

This commit is contained in:
2021-02-26 22:23:13 +08:00
parent 7884df52f0
commit a719feebba
2585 changed files with 328263 additions and 0 deletions
+488
View File
@@ -0,0 +1,488 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\arguments\Argument;
use cli\arguments\HelpScreen;
use cli\arguments\InvalidArguments;
use cli\arguments\Lexer;
/**
* Parses command line arguments.
*/
class Arguments implements \ArrayAccess {
protected $_flags = array();
protected $_options = array();
protected $_strict = false;
protected $_input = array();
protected $_invalid = array();
protected $_parsed;
protected $_lexer;
/**
* Initializes the argument parser. If you wish to change the default behaviour
* you may pass an array of options as the first argument. Valid options are
* `'help'` and `'strict'`, each a boolean.
*
* `'help'` is `true` by default, `'strict'` is false by default.
*
* @param array $options An array of options for this parser.
*/
public function __construct($options = array()) {
$options += array(
'strict' => false,
'input' => array_slice($_SERVER['argv'], 1)
);
$this->_input = $options['input'];
$this->setStrict($options['strict']);
if (isset($options['flags'])) {
$this->addFlags($options['flags']);
}
if (isset($options['options'])) {
$this->addOptions($options['options']);
}
}
/**
* Get the list of arguments found by the defined definitions.
*
* @return array
*/
public function getArguments() {
if (!isset($this->_parsed)) {
$this->parse();
}
return $this->_parsed;
}
public function getHelpScreen() {
return new HelpScreen($this);
}
/**
* Encodes the parsed arguments as JSON.
*
* @return string
*/
public function asJSON() {
return json_encode($this->_parsed);
}
/**
* Returns true if a given argument was parsed.
*
* @param mixed $offset An Argument object or the name of the argument.
* @return bool
*/
public function offsetExists($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
return array_key_exists($offset, $this->_parsed);
}
/**
* Get the parsed argument's value.
*
* @param mixed $offset An Argument object or the name of the argument.
* @return mixed
*/
public function offsetGet($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
if (isset($this->_parsed[$offset])) {
return $this->_parsed[$offset];
}
}
/**
* Sets the value of a parsed argument.
*
* @param mixed $offset An Argument object or the name of the argument.
* @param mixed $value The value to set
*/
public function offsetSet($offset, $value) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
$this->_parsed[$offset] = $value;
}
/**
* Unset a parsed argument.
*
* @param mixed $offset An Argument object or the name of the argument.
*/
public function offsetUnset($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
unset($this->_parsed[$offset]);
}
/**
* Adds a flag (boolean argument) to the argument list.
*
* @param mixed $flag A string representing the flag, or an array of strings.
* @param array $settings An array of settings for this flag.
* @setting string description A description to be shown in --help.
* @setting bool default The default value for this flag.
* @setting bool stackable Whether the flag is repeatable to increase the value.
* @setting array aliases Other ways to trigger this flag.
* @return $this
*/
public function addFlag($flag, $settings = array()) {
if (is_string($settings)) {
$settings = array('description' => $settings);
}
if (is_array($flag)) {
$settings['aliases'] = $flag;
$flag = array_shift($settings['aliases']);
}
if (isset($this->_flags[$flag])) {
$this->_warn('flag already exists: ' . $flag);
return $this;
}
$settings += array(
'default' => false,
'stackable' => false,
'description' => null,
'aliases' => array()
);
$this->_flags[$flag] = $settings;
return $this;
}
/**
* Add multiple flags at once. The input array should be keyed with the
* primary flag character, and the values should be the settings array
* used by {addFlag}.
*
* @param array $flags An array of flags to add
* @return $this
*/
public function addFlags($flags) {
foreach ($flags as $flag => $settings) {
if (is_numeric($flag)) {
$this->_warn('No flag character given');
continue;
}
$this->addFlag($flag, $settings);
}
return $this;
}
/**
* Adds an option (string argument) to the argument list.
*
* @param mixed $option A string representing the option, or an array of strings.
* @param array $settings An array of settings for this option.
* @setting string description A description to be shown in --help.
* @setting bool default The default value for this option.
* @setting array aliases Other ways to trigger this option.
* @return $this
*/
public function addOption($option, $settings = array()) {
if (is_string($settings)) {
$settings = array('description' => $settings);
}
if (is_array($option)) {
$settings['aliases'] = $option;
$option = array_shift($settings['aliases']);
}
if (isset($this->_options[$option])) {
$this->_warn('option already exists: ' . $option);
return $this;
}
$settings += array(
'default' => null,
'description' => null,
'aliases' => array()
);
$this->_options[$option] = $settings;
return $this;
}
/**
* Add multiple options at once. The input array should be keyed with the
* primary option string, and the values should be the settings array
* used by {addOption}.
*
* @param array $options An array of options to add
* @return $this
*/
public function addOptions($options) {
foreach ($options as $option => $settings) {
if (is_numeric($option)) {
$this->_warn('No option string given');
continue;
}
$this->addOption($option, $settings);
}
return $this;
}
/**
* Enable or disable strict mode. If strict mode is active any invalid
* arguments found by the parser will throw `cli\arguments\InvalidArguments`.
*
* Even if strict is disabled, invalid arguments are logged and can be
* retrieved with `cli\Arguments::getInvalidArguments()`.
*
* @param bool $strict True to enable, false to disable.
* @return $this
*/
public function setStrict($strict) {
$this->_strict = (bool)$strict;
return $this;
}
/**
* Get the list of invalid arguments the parser found.
*
* @return array
*/
public function getInvalidArguments() {
return $this->_invalid;
}
/**
* Get a flag by primary matcher or any defined aliases.
*
* @param mixed $flag Either a string representing the flag or an
* cli\arguments\Argument object.
* @return array
*/
public function getFlag($flag) {
if ($flag instanceOf Argument) {
$obj = $flag;
$flag = $flag->value;
}
if (isset($this->_flags[$flag])) {
return $this->_flags[$flag];
}
foreach ($this->_flags as $master => $settings) {
if (in_array($flag, (array)$settings['aliases'])) {
if (isset($obj)) {
$obj->key = $master;
}
$cache[$flag] =& $settings;
return $settings;
}
}
}
public function getFlags() {
return $this->_flags;
}
public function hasFlags() {
return !empty($this->_flags);
}
/**
* Returns true if the given argument is defined as a flag.
*
* @param mixed $argument Either a string representing the flag or an
* cli\arguments\Argument object.
* @return bool
*/
public function isFlag($argument) {
return (null !== $this->getFlag($argument));
}
/**
* Returns true if the given flag is stackable.
*
* @param mixed $flag Either a string representing the flag or an
* cli\arguments\Argument object.
* @return bool
*/
public function isStackable($flag) {
$settings = $this->getFlag($flag);
return isset($settings) && (true === $settings['stackable']);
}
/**
* Get an option by primary matcher or any defined aliases.
*
* @param mixed $option Either a string representing the option or an
* cli\arguments\Argument object.
* @return array
*/
public function getOption($option) {
if ($option instanceOf Argument) {
$obj = $option;
$option = $option->value;
}
if (isset($this->_options[$option])) {
return $this->_options[$option];
}
foreach ($this->_options as $master => $settings) {
if (in_array($option, (array)$settings['aliases'])) {
if (isset($obj)) {
$obj->key = $master;
}
return $settings;
}
}
}
public function getOptions() {
return $this->_options;
}
public function hasOptions() {
return !empty($this->_options);
}
/**
* Returns true if the given argument is defined as an option.
*
* @param mixed $argument Either a string representing the option or an
* cli\arguments\Argument object.
* @return bool
*/
public function isOption($argument) {
return (null != $this->getOption($argument));
}
/**
* Parses the argument list with the given options. The returned argument list
* will use either the first long name given or the first name in the list
* if a long name is not given.
*
* @return array
* @throws arguments\InvalidArguments
*/
public function parse() {
$this->_invalid = array();
$this->_parsed = array();
$this->_lexer = new Lexer($this->_input);
$this->_applyDefaults();
foreach ($this->_lexer as $argument) {
if ($this->_parseFlag($argument)) {
continue;
}
if ($this->_parseOption($argument)) {
continue;
}
array_push($this->_invalid, $argument->raw);
}
if ($this->_strict && !empty($this->_invalid)) {
throw new InvalidArguments($this->_invalid);
}
}
/**
* This applies the default values, if any, of all of the
* flags and options, so that if there is a default value
* it will be available.
*/
private function _applyDefaults() {
foreach($this->_flags as $flag => $settings) {
$this[$flag] = $settings['default'];
}
foreach($this->_options as $option => $settings) {
// If the default is 0 we should still let it be set.
if (!empty($settings['default']) || $settings['default'] === 0) {
$this[$option] = $settings['default'];
}
}
}
private function _warn($message) {
trigger_error('[' . __CLASS__ .'] ' . $message, E_USER_WARNING);
}
private function _parseFlag($argument) {
if (!$this->isFlag($argument)) {
return false;
}
if ($this->isStackable($argument)) {
if (!isset($this[$argument])) {
$this[$argument->key] = 0;
}
$this[$argument->key] += 1;
} else {
$this[$argument->key] = true;
}
return true;
}
private function _parseOption($option) {
if (!$this->isOption($option)) {
return false;
}
// Peak ahead to make sure we get a value.
if ($this->_lexer->end() || !$this->_lexer->peek->isValue) {
$optionSettings = $this->getOption($option->key);
if (empty($optionSettings['default']) && $optionSettings !== 0) {
// Oops! Got no value and no default , throw a warning and continue.
$this->_warn('no value given for ' . $option->raw);
$this[$option->key] = null;
} else {
// No value and we have a default, so we set to the default
$this[$option->key] = $optionSettings['default'];
}
return true;
}
// Store as array and join to string after looping for values
$values = array();
// Loop until we find a flag in peak-ahead
foreach ($this->_lexer as $value) {
array_push($values, $value->raw);
if (!$this->_lexer->end() && !$this->_lexer->peek->isValue) {
break;
}
}
$this[$option->key] = join($values, ' ');
return true;
}
}
+279
View File
@@ -0,0 +1,279 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* Change the color of text.
*
* Reference: http://graphcomp.com/info/specs/ansi_col.html#colors
*/
class Colors {
static protected $_colors = array(
'color' => array(
'black' => 30,
'red' => 31,
'green' => 32,
'yellow' => 33,
'blue' => 34,
'magenta' => 35,
'cyan' => 36,
'white' => 37
),
'style' => array(
'bright' => 1,
'dim' => 2,
'underline' => 4,
'blink' => 5,
'reverse' => 7,
'hidden' => 8
),
'background' => array(
'black' => 40,
'red' => 41,
'green' => 42,
'yellow' => 43,
'blue' => 44,
'magenta' => 45,
'cyan' => 46,
'white' => 47
)
);
static protected $_enabled = null;
static protected $_string_cache = array();
static public function enable($force = true) {
self::$_enabled = $force === true ? true : null;
}
static public function disable($force = true) {
self::$_enabled = $force === true ? false : null;
}
/**
* Check if we should colorize output based on local flags and shell type.
*
* Only check the shell type if `Colors::$_enabled` is null and `$colored` is null.
*/
static public function shouldColorize($colored = null) {
return self::$_enabled === true ||
(self::$_enabled !== false &&
($colored === true ||
($colored !== false && Streams::isTty())));
}
/**
* Set the color.
*
* @param string $color The name of the color or style to set.
* @return string
*/
static public function color($color) {
if (!is_array($color)) {
$color = compact('color');
}
$color += array('color' => null, 'style' => null, 'background' => null);
if ($color['color'] == 'reset') {
return "\033[0m";
}
$colors = array();
foreach (array('color', 'style', 'background') as $type) {
$code = $color[$type];
if (isset(self::$_colors[$type][$code])) {
$colors[] = self::$_colors[$type][$code];
}
}
if (empty($colors)) {
$colors[] = 0;
}
return "\033[" . join(';', $colors) . "m";
}
/**
* Colorize a string using helpful string formatters. If the `Streams::$out` points to a TTY coloring will be enabled,
* otherwise disabled. You can control this check with the `$colored` parameter.
*
* @param string $string
* @param boolean $colored Force enable or disable the colorized output. If left as `null` the TTY will control coloring.
* @return string
*/
static public function colorize($string, $colored = null) {
$passed = $string;
if (!self::shouldColorize($colored)) {
$return = self::decolorize( $passed, 2 /*keep_encodings*/ );
self::cacheString($passed, $return);
return $return;
}
$md5 = md5($passed);
if (isset(self::$_string_cache[$md5]['colorized'])) {
return self::$_string_cache[$md5]['colorized'];
}
$string = str_replace('%%', '%¾', $string);
foreach (self::getColors() as $key => $value) {
$string = str_replace($key, self::color($value), $string);
}
$string = str_replace('%¾', '%', $string);
self::cacheString($passed, $string);
return $string;
}
/**
* Remove color information from a string.
*
* @param string $string A string with color information.
* @param int $keep Optional. If the 1 bit is set, color tokens (eg "%n") won't be stripped. If the 2 bit is set, color encodings (ANSI escapes) won't be stripped. Default 0.
* @return string A string with color information removed.
*/
static public function decolorize( $string, $keep = 0 ) {
if ( ! ( $keep & 1 ) ) {
// Get rid of color tokens if they exist
$string = str_replace('%%', '%¾', $string);
$string = str_replace(array_keys(self::getColors()), '', $string);
$string = str_replace('%¾', '%', $string);
}
if ( ! ( $keep & 2 ) ) {
// Remove color encoding if it exists
foreach (self::getColors() as $key => $value) {
$string = str_replace(self::color($value), '', $string);
}
}
return $string;
}
/**
* Cache the original, colorized, and decolorized versions of a string.
*
* @param string $passed The original string before colorization.
* @param string $colorized The string after running through self::colorize.
* @param string $deprecated Optional. Not used. Default null.
*/
static public function cacheString( $passed, $colorized, $deprecated = null ) {
self::$_string_cache[md5($passed)] = array(
'passed' => $passed,
'colorized' => $colorized,
'decolorized' => self::decolorize($passed), // Not very useful but keep for BC.
);
}
/**
* Return the length of the string without color codes.
*
* @param string $string the string to measure
* @return int
*/
static public function length($string) {
return safe_strlen( self::decolorize( $string ) );
}
/**
* Return the width (length in characters) of the string without color codes if enabled.
*
* @param string $string The string to measure.
* @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int
*/
static public function width( $string, $pre_colorized = false, $encoding = false ) {
return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized ? 1 /*keep_tokens*/ : 0 ) : $string, $encoding );
}
/**
* Pad the string to a certain display length.
*
* @param string $string The string to pad.
* @param int $length The display length.
* @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @param int $pad_type Optional. Can be STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH. If pad_type is not specified it is assumed to be STR_PAD_RIGHT.
* @return string
*/
static public function pad( $string, $length, $pre_colorized = false, $encoding = false, $pad_type = STR_PAD_RIGHT ) {
$real_length = self::width( $string, $pre_colorized, $encoding );
$diff = strlen( $string ) - $real_length;
$length += $diff;
return str_pad( $string, $length, ' ', $pad_type );
}
/**
* Get the color mapping array.
*
* @return array Array of color tokens mapped to colors and styles.
*/
static public function getColors() {
return array(
'%y' => array('color' => 'yellow'),
'%g' => array('color' => 'green'),
'%b' => array('color' => 'blue'),
'%r' => array('color' => 'red'),
'%p' => array('color' => 'magenta'),
'%m' => array('color' => 'magenta'),
'%c' => array('color' => 'cyan'),
'%w' => array('color' => 'white'),
'%k' => array('color' => 'black'),
'%n' => array('color' => 'reset'),
'%Y' => array('color' => 'yellow', 'style' => 'bright'),
'%G' => array('color' => 'green', 'style' => 'bright'),
'%B' => array('color' => 'blue', 'style' => 'bright'),
'%R' => array('color' => 'red', 'style' => 'bright'),
'%P' => array('color' => 'magenta', 'style' => 'bright'),
'%M' => array('color' => 'magenta', 'style' => 'bright'),
'%C' => array('color' => 'cyan', 'style' => 'bright'),
'%W' => array('color' => 'white', 'style' => 'bright'),
'%K' => array('color' => 'black', 'style' => 'bright'),
'%N' => array('color' => 'reset', 'style' => 'bright'),
'%3' => array('background' => 'yellow'),
'%2' => array('background' => 'green'),
'%4' => array('background' => 'blue'),
'%1' => array('background' => 'red'),
'%5' => array('background' => 'magenta'),
'%6' => array('background' => 'cyan'),
'%7' => array('background' => 'white'),
'%0' => array('background' => 'black'),
'%F' => array('style' => 'blink'),
'%U' => array('style' => 'underline'),
'%8' => array('style' => 'reverse'),
'%9' => array('style' => 'bright'),
'%_' => array('style' => 'bright')
);
}
/**
* Get the cached string values.
*
* @return array The cached string values.
*/
static public function getStringCache() {
return self::$_string_cache;
}
/**
* Clear the string cache.
*/
static public function clearStringCache() {
self::$_string_cache = array();
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
abstract class Memoize {
protected $_memoCache = array();
public function __get($name) {
if (isset($this->_memoCache[$name])) {
return $this->_memoCache[$name];
}
// Hide probable private methods
if (0 == strncmp($name, '_', 1)) {
return ($this->_memoCache[$name] = null);
}
if (!method_exists($this, $name)) {
return ($this->_memoCache[$name] = null);
}
$method = array($this, $name);
($this->_memoCache[$name] = call_user_func($method));
return $this->_memoCache[$name];
}
protected function _unmemo($name) {
if ($name === true) {
$this->_memoCache = array();
} else {
unset($this->_memoCache[$name]);
}
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\Streams;
/**
* The `Notify` class is the basis of all feedback classes, such as Indicators
* and Progress meters. The default behaviour is to refresh output after 100ms
* have passed. This is done to preventing the screen from flickering and keep
* slowdowns from output to a minimum.
*
* The most basic form of Notifier has no maxim, and simply displays a series
* of characters to indicate progress is being made.
*/
abstract class Notify {
protected $_current = 0;
protected $_first = true;
protected $_interval;
protected $_message;
protected $_start;
protected $_timer;
/**
* Instatiates a Notification object.
*
* @param string $msg The text to display next to the Notifier.
* @param int $interval The interval in milliseconds between updates.
*/
public function __construct($msg, $interval = 100) {
$this->_message = $msg;
$this->_interval = (int)$interval;
}
/**
* This method should be used to print out the Notifier. This method is
* called from `cli\Notify::tick()` after `cli\Notify::$_interval` has passed.
*
* @abstract
* @param boolean $finish
* @see cli\Notify::tick()
*/
abstract public function display($finish = false);
/**
* Reset the notifier state so the same instance can be used in multiple loops.
*/
public function reset() {
$this->_current = 0;
$this->_first = true;
$this->_start = null;
$this->_timer = null;
}
/**
* Returns the formatted tick count.
*
* @return string The formatted tick count.
*/
public function current() {
return number_format($this->_current);
}
/**
* Calculates the time elapsed since the Notifier was first ticked.
*
* @return int The elapsed time in seconds.
*/
public function elapsed() {
if (!$this->_start) {
return 0;
}
$elapsed = time() - $this->_start;
return $elapsed;
}
/**
* Calculates the speed (number of ticks per second) at which the Notifier
* is being updated.
*
* @return int The number of ticks performed in 1 second.
*/
public function speed() {
static $tick, $iteration = 0, $speed = 0;
if (!$this->_start) {
return 0;
} else if (!$tick) {
$tick = $this->_start;
}
$now = microtime(true);
$span = $now - $tick;
if ($span > 1) {
$iteration++;
$tick = $now;
$speed = ($this->_current / $iteration) / $span;
}
return $speed;
}
/**
* Takes a time span given in seconds and formats it for display. The
* returned string will be in MM:SS form.
*
* @param int $time The time span in seconds to format.
* @return string The formatted time span.
*/
public function formatTime($time) {
return floor($time / 60) . ':' . str_pad($time % 60, 2, 0, STR_PAD_LEFT);
}
/**
* Finish our Notification display. Should be called after the Notifier is
* no longer needed.
*
* @see cli\Notify::display()
*/
public function finish() {
Streams::out("\r");
$this->display(true);
Streams::line();
}
/**
* Increments are tick counter by the given amount. If no amount is provided,
* the ticker is incremented by 1.
*
* @param int $increment The amount to increment by.
*/
public function increment($increment = 1) {
$this->_current += $increment;
}
/**
* Determines whether the display should be updated or not according to
* our interval setting.
*
* @return boolean `true` if the display should be updated, `false` otherwise.
*/
public function shouldUpdate() {
$now = microtime(true) * 1000;
if (empty($this->_timer)) {
$this->_start = (int)(($this->_timer = $now) / 1000);
return true;
}
if (($now - $this->_timer) > $this->_interval) {
$this->_timer = $now;
return true;
}
return false;
}
/**
* This method is the meat of all Notifiers. First we increment the ticker
* and then update the display if enough time has passed since our last tick.
*
* @param int $increment The amount to increment by.
* @see cli\Notify::increment()
* @see cli\Notify::shouldUpdate()
* @see cli\Notify::display()
*/
public function tick($increment = 1) {
$this->increment($increment);
if ($this->shouldUpdate()) {
Streams::out("\r");
$this->display();
}
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* A more complex type of Notifier, `Progress` Notifiers always have a maxim
* value and generally show some form of percent complete or estimated time
* to completion along with the standard Notifier displays.
*
* @see cli\Notify
*/
abstract class Progress extends \cli\Notify {
protected $_total = 0;
/**
* Instantiates a Progress Notifier.
*
* @param string $msg The text to display next to the Notifier.
* @param int $total The total number of ticks we will be performing.
* @param int $interval The interval in milliseconds between updates.
* @see cli\Progress::setTotal()
*/
public function __construct($msg, $total, $interval = 100) {
parent::__construct($msg, $interval);
$this->setTotal($total);
}
/**
* Set the max increments for this progress notifier.
*
* @param int $total The total number of times this indicator should be `tick`ed.
* @throws \InvalidArgumentException Thrown if the `$total` is less than 0.
*/
public function setTotal($total) {
$this->_total = (int)$total;
if ($this->_total < 0) {
throw new \InvalidArgumentException('Maximum value out of range, must be positive.');
}
}
/**
* Reset the progress state so the same instance can be used in multiple loops.
*/
public function reset($total = null) {
parent::reset();
if ($total) {
$this->setTotal($total);
}
}
/**
* Behaves in a similar manner to `cli\Notify::current()`, but the output
* is padded to match the length of `cli\Progress::total()`.
*
* @return string The formatted and padded tick count.
* @see cli\Progress::total()
*/
public function current() {
$size = strlen($this->total());
return str_pad(parent::current(), $size);
}
/**
* Returns the formatted total expected ticks.
*
* @return string The formatted total ticks.
*/
public function total() {
return number_format($this->_total);
}
/**
* Calculates the estimated total time for the tick count to reach the
* total ticks given.
*
* @return int The estimated total number of seconds for all ticks to be
* completed. This is not the estimated time left, but total.
* @see cli\Notify::speed()
* @see cli\Notify::elapsed()
*/
public function estimated() {
$speed = $this->speed();
if (!$speed || !$this->elapsed()) {
return 0;
}
$estimated = round($this->_total / $speed);
return $estimated;
}
/**
* Forces the current tick count to the total ticks given at instatiation
* time before passing on to `cli\Notify::finish()`.
*/
public function finish() {
$this->_current = $this->_total;
parent::finish();
}
/**
* Increments are tick counter by the given amount. If no amount is provided,
* the ticker is incremented by 1.
*
* @param int $increment The amount to increment by.
*/
public function increment($increment = 1) {
$this->_current = min($this->_total, $this->_current + $increment);
}
/**
* Calculate the percentage completed.
*
* @return float The percent completed.
*/
public function percent() {
if ($this->_total == 0) {
return 1;
}
return ($this->_current / $this->_total);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* The `Shell` class is a utility class for shell related tasks such as
* information on width.
*/
class Shell {
/**
* Returns the number of columns the current shell has for display.
*
* @return int The number of columns.
* @todo Test on more systems.
*/
static public function columns() {
static $columns;
if ( getenv( 'PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET' ) ) {
$columns = null;
}
if ( null === $columns ) {
if ( function_exists( 'exec' ) ) {
if ( self::is_windows() ) {
// Cater for shells such as Cygwin and Git bash where `mode CON` returns an incorrect value for columns.
if ( ( $shell = getenv( 'SHELL' ) ) && preg_match( '/(?:bash|zsh)(?:\.exe)?$/', $shell ) && getenv( 'TERM' ) ) {
$columns = (int) exec( 'tput cols' );
}
if ( ! $columns ) {
$return_var = -1;
$output = array();
exec( 'mode CON', $output, $return_var );
if ( 0 === $return_var && $output ) {
// Look for second line ending in ": <number>" (searching for "Columns:" will fail on non-English locales).
if ( preg_match( '/:\s*[0-9]+\n[^:]+:\s*([0-9]+)\n/', implode( "\n", $output ), $matches ) ) {
$columns = (int) $matches[1];
}
}
}
} else {
if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) {
$size = exec( '/usr/bin/env stty size 2>/dev/null' );
if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) {
$columns = (int) $matches[1];
}
if ( ! $columns ) {
if ( getenv( 'TERM' ) ) {
$columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' );
}
}
}
}
}
if ( ! $columns ) {
$columns = 80; // default width of cmd window on Windows OS
}
}
return $columns;
}
/**
* Checks whether the output of the current script is a TTY or a pipe / redirect
*
* Returns true if STDOUT output is being redirected to a pipe or a file; false is
* output is being sent directly to the terminal.
*
* If an env variable SHELL_PIPE exists, returned result depends it's
* value. Strings like 1, 0, yes, no, that validate to booleans are accepted.
*
* To enable ASCII formatting even when shell is piped, use the
* ENV variable SHELL_PIPE=0
*
* @return bool
*/
static public function isPiped() {
$shellPipe = getenv('SHELL_PIPE');
if ($shellPipe !== false) {
return filter_var($shellPipe, FILTER_VALIDATE_BOOLEAN);
} else {
return (function_exists('posix_isatty') && !posix_isatty(STDOUT));
}
}
/**
* Uses `stty` to hide input/output completely.
* @param boolean $hidden Will hide/show the next data. Defaults to true.
*/
static public function hide($hidden = true) {
system( 'stty ' . ( $hidden? '-echo' : 'echo' ) );
}
/**
* Is this shell in Windows?
*
* @return bool
*/
static private function is_windows() {
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
}
?>
+279
View File
@@ -0,0 +1,279 @@
<?php
namespace cli;
class Streams {
protected static $out = STDOUT;
protected static $in = STDIN;
protected static $err = STDERR;
static function _call( $func, $args ) {
$method = __CLASS__ . '::' . $func;
return call_user_func_array( $method, $args );
}
static public function isTty() {
return (function_exists('posix_isatty') && posix_isatty(static::$out));
}
/**
* Handles rendering strings. If extra scalar arguments are given after the `$msg`
* the string will be rendered with `sprintf`. If the second argument is an `array`
* then each key in the array will be the placeholder name. Placeholders are of the
* format {:key}.
*
* @param string $msg The message to render.
* @param mixed ... Either scalar arguments or a single array argument.
* @return string The rendered string.
*/
public static function render( $msg ) {
$args = func_get_args();
// No string replacement is needed
if( count( $args ) == 1 || ( is_string( $args[1] ) && '' === $args[1] ) ) {
return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg;
}
// If the first argument is not an array just pass to sprintf
if( !is_array( $args[1] ) ) {
// Colorize the message first so sprintf doesn't bitch at us
if ( Colors::shouldColorize() ) {
$args[0] = Colors::colorize( $args[0] );
}
// Escape percent characters for sprintf
$args[0] = preg_replace('/(%([^\w]|$))/', "%$1", $args[0]);
return call_user_func_array( 'sprintf', $args );
}
// Here we do named replacement so formatting strings are more understandable
foreach( $args[1] as $key => $value ) {
$msg = str_replace( '{:' . $key . '}', $value, $msg );
}
return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg;
}
/**
* Shortcut for printing to `STDOUT`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see \cli\render()
*/
public static function out( $msg ) {
fwrite( static::$out, self::_call( 'render', func_get_args() ) );
}
/**
* Pads `$msg` to the width of the shell before passing to `cli\out`.
*
* @param string $msg The message to pad and pass on.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see cli\out()
*/
public static function out_padded( $msg ) {
$msg = self::_call( 'render', func_get_args() );
self::out( str_pad( $msg, \cli\Shell::columns() ) );
}
/**
* Prints a message to `STDOUT` with a newline appended. See `\cli\out` for
* more documentation.
*
* @see cli\out()
*/
public static function line( $msg = '' ) {
// func_get_args is empty if no args are passed even with the default above.
$args = array_merge( func_get_args(), array( '' ) );
$args[0] .= "\n";
self::_call( 'out', $args );
}
/**
* Shortcut for printing to `STDERR`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format. With no string,
* a newline is printed.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
*/
public static function err( $msg = '' ) {
// func_get_args is empty if no args are passed even with the default above.
$args = array_merge( func_get_args(), array( '' ) );
$args[0] .= "\n";
fwrite( static::$err, self::_call( 'render', $args ) );
}
/**
* Takes input from `STDIN` in the given format. If an end of transmission
* character is sent (^D), an exception is thrown.
*
* @param string $format A valid input format. See `fscanf` for documentation.
* If none is given, all input up to the first newline
* is accepted.
* @param boolean $hide If true will hide what the user types in.
* @return string The input with whitespace trimmed.
* @throws \Exception Thrown if ctrl-D (EOT) is sent as input.
*/
public static function input( $format = null, $hide = false ) {
if ( $hide )
Shell::hide();
if( $format ) {
fscanf( static::$in, $format . "\n", $line );
} else {
$line = fgets( static::$in );
}
if ( $hide ) {
Shell::hide( false );
echo "\n";
}
if( $line === false ) {
throw new \Exception( 'Caught ^D during input' );
}
return trim( $line );
}
/**
* Displays an input prompt. If no default value is provided the prompt will
* continue displaying until input is received.
*
* @param string $question The question to ask the user.
* @param bool|string $default A default value if the user provides no input.
* @param string $marker A string to append to the question and default value
* on display.
* @param boolean $hide Optionally hides what the user types in.
* @return string The users input.
* @see cli\input()
*/
public static function prompt( $question, $default = null, $marker = ': ', $hide = false ) {
if( $default && strpos( $question, '[' ) === false ) {
$question .= ' [' . $default . ']';
}
while( true ) {
self::out( $question . $marker );
$line = self::input( null, $hide );
if ( trim( $line ) !== '' )
return $line;
if( $default !== false )
return $default;
}
}
/**
* Presents a user with a multiple choice question, useful for 'yes/no' type
* questions (which this public static function defaults too).
*
* @param string $question The question to ask the user.
* @param string $choice A string of characters allowed as a response. Case is ignored.
* @param string $default The default choice. NULL if a default is not allowed.
* @return string The users choice.
* @see cli\prompt()
*/
public static function choose( $question, $choice = 'yn', $default = 'n' ) {
if( !is_string( $choice ) ) {
$choice = join( '', $choice );
}
// Make every choice character lowercase except the default
$choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) );
// Seperate each choice with a forward-slash
$choices = trim( join( '/', preg_split( '//', $choice ) ), '/' );
while( true ) {
$line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' );
if( stripos( $choice, $line ) !== false ) {
return strtolower( $line );
}
if( !empty( $default ) ) {
return strtolower( $default );
}
}
}
/**
* Displays an array of strings as a menu where a user can enter a number to
* choose an option. The array must be a single dimension with either strings
* or objects with a `__toString()` method.
*
* @param array $items The list of items the user can choose from.
* @param string $default The index of the default item.
* @param string $title The message displayed to the user when prompted.
* @return string The index of the chosen item.
* @see cli\line()
* @see cli\input()
* @see cli\err()
*/
public static function menu( $items, $default = null, $title = 'Choose an item' ) {
$map = array_values( $items );
if( $default && strpos( $title, '[' ) === false && isset( $items[$default] ) ) {
$title .= ' [' . $items[$default] . ']';
}
foreach( $map as $idx => $item ) {
self::line( ' %d. %s', $idx + 1, (string)$item );
}
self::line();
while( true ) {
fwrite( static::$out, sprintf( '%s: ', $title ) );
$line = self::input();
if( is_numeric( $line ) ) {
$line--;
if( isset( $map[$line] ) ) {
return array_search( $map[$line], $items );
}
if( $line < 0 || $line >= count( $map ) ) {
self::err( 'Invalid menu selection: out of range' );
}
} else if( isset( $default ) ) {
return $default;
}
}
}
/**
* Sets one of the streams (input, output, or error) to a `stream` type resource.
*
* Valid $whichStream values are:
* - 'in' (default: STDIN)
* - 'out' (default: STDOUT)
* - 'err' (default: STDERR)
*
* Any custom streams will be closed for you on shutdown, so please don't close stream
* resources used with this method.
*
* @param string $whichStream The stream property to update
* @param resource $stream The new stream resource to use
* @return void
* @throws \Exception Thrown if $stream is not a resource of the 'stream' type.
*/
public static function setStream( $whichStream, $stream ) {
if( !is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) {
throw new \Exception( 'Invalid resource type!' );
}
if( property_exists( __CLASS__, $whichStream ) ) {
static::${$whichStream} = $stream;
}
register_shutdown_function( function() use ($stream) {
fclose( $stream );
} );
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\Shell;
use cli\Streams;
use cli\table\Ascii;
use cli\table\Renderer;
use cli\table\Tabular;
/**
* The `Table` class is used to display data in a tabular format.
*/
class Table {
protected $_renderer;
protected $_headers = array();
protected $_footers = array();
protected $_width = array();
protected $_rows = array();
/**
* Initializes the `Table` class.
*
* There are 3 ways to instantiate this class:
*
* 1. Pass an array of strings as the first parameter for the column headers
* and a 2-dimensional array as the second parameter for the data rows.
* 2. Pass an array of hash tables (string indexes instead of numerical)
* where each hash table is a row and the indexes of the *first* hash
* table are used as the header values.
* 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`.
*
* @param array $headers Headers used in this table. Optional.
* @param array $rows The rows of data for this table. Optional.
* @param array $footers Footers used in this table. Optional.
*/
public function __construct(array $headers = null, array $rows = null, array $footers = null) {
if (!empty($headers)) {
// If all the rows is given in $headers we use the keys from the
// first row for the header values
if ($rows === null) {
$rows = $headers;
$keys = array_keys(array_shift($headers));
$headers = array();
foreach ($keys as $header) {
$headers[$header] = $header;
}
}
$this->setHeaders($headers);
$this->setRows($rows);
}
if (!empty($footers)) {
$this->setFooters($footers);
}
if (Shell::isPiped()) {
$this->setRenderer(new Tabular());
} else {
$this->setRenderer(new Ascii());
}
}
public function resetTable()
{
$this->_headers = array();
$this->_width = array();
$this->_rows = array();
$this->_footers = array();
return $this;
}
/**
* Sets the renderer used by this table.
*
* @param table\Renderer $renderer The renderer to use for output.
* @see table\Renderer
* @see table\Ascii
* @see table\Tabular
*/
public function setRenderer(Renderer $renderer) {
$this->_renderer = $renderer;
}
/**
* Loops through the row and sets the maximum width for each column.
*
* @param array $row The table row.
* @return array $row
*/
protected function checkRow(array $row) {
foreach ($row as $column => $str) {
$width = Colors::width( $str, $this->isAsciiPreColorized( $column ) );
if (!isset($this->_width[$column]) || $width > $this->_width[$column]) {
$this->_width[$column] = $width;
}
}
return $row;
}
/**
* Output the table to `STDOUT` using `cli\line()`.
*
* If STDOUT is a pipe or redirected to a file, should output simple
* tab-separated text. Otherwise, renders table with ASCII table borders
*
* @uses cli\Shell::isPiped() Determine what format to output
*
* @see cli\Table::renderRow()
*/
public function display() {
foreach( $this->getDisplayLines() as $line ) {
Streams::line( $line );
}
}
/**
* Get the table lines to output.
*
* @see cli\Table::display()
* @see cli\Table::renderRow()
*
* @return array
*/
public function getDisplayLines() {
$this->_renderer->setWidths($this->_width, $fallback = true);
$border = $this->_renderer->border();
$out = array();
if (isset($border)) {
$out[] = $border;
}
$out[] = $this->_renderer->row($this->_headers);
if (isset($border)) {
$out[] = $border;
}
foreach ($this->_rows as $row) {
$row = $this->_renderer->row($row);
$row = explode( PHP_EOL, $row );
$out = array_merge( $out, $row );
}
if (isset($border)) {
$out[] = $border;
}
if ($this->_footers) {
$out[] = $this->_renderer->row($this->_footers);
if (isset($border)) {
$out[] = $border;
}
}
return $out;
}
/**
* Sort the table by a column. Must be called before `cli\Table::display()`.
*
* @param int $column The index of the column to sort by.
*/
public function sort($column) {
if (!isset($this->_headers[$column])) {
trigger_error('No column with index ' . $column, E_USER_NOTICE);
return;
}
usort($this->_rows, function($a, $b) use ($column) {
return strcmp($a[$column], $b[$column]);
});
}
/**
* Set the headers of the table.
*
* @param array $headers An array of strings containing column header names.
*/
public function setHeaders(array $headers) {
$this->_headers = $this->checkRow($headers);
}
/**
* Set the footers of the table.
*
* @param array $footers An array of strings containing column footers names.
*/
public function setFooters(array $footers) {
$this->_footers = $this->checkRow($footers);
}
/**
* Add a row to the table.
*
* @param array $row The row data.
* @see cli\Table::checkRow()
*/
public function addRow(array $row) {
$this->_rows[] = $this->checkRow($row);
}
/**
* Clears all previous rows and adds the given rows.
*
* @param array $rows A 2-dimensional array of row data.
* @see cli\Table::addRow()
*/
public function setRows(array $rows) {
$this->_rows = array();
foreach ($rows as $row) {
$this->addRow($row);
}
}
public function countRows() {
return count($this->_rows);
}
/**
* Set whether items in an Ascii table are pre-colorized.
*
* @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized.
* @see cli\Ascii::setPreColorized()
*/
public function setAsciiPreColorized( $pre_colorized ) {
if ( $this->_renderer instanceof Ascii ) {
$this->_renderer->setPreColorized( $pre_colorized );
}
}
/**
* Is a column in an Ascii table pre-colorized?
*
* @param int $column Column index to check.
* @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false.
* @see cli\Ascii::isPreColorized()
*/
private function isAsciiPreColorized( $column ) {
if ( $this->_renderer instanceof Ascii ) {
return $this->_renderer->isPreColorized( $column );
}
return false;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author Ryan Sullivan <rsullivan@connectstudios.com>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* The `Tree` class is used to display data in a tree-like format.
*/
class Tree {
protected $_renderer;
protected $_data = array();
/**
* Sets the renderer used by this tree.
*
* @param tree\Renderer $renderer The renderer to use for output.
* @see tree\Renderer
* @see tree\Ascii
* @see tree\Markdown
*/
public function setRenderer(tree\Renderer $renderer) {
$this->_renderer = $renderer;
}
/**
* Set the data.
* Format:
* [
* 'Label' => [
* 'Thing' => ['Thing'],
* ],
* 'Thing',
* ]
* @param array $data
*/
public function setData(array $data)
{
$this->_data = $data;
}
/**
* Render the tree and return it as a string.
*
* @return string|null
*/
public function render()
{
return $this->_renderer->render($this->_data);
}
/**
* Display the rendered tree
*/
public function display()
{
echo $this->render();
}
}
@@ -0,0 +1,137 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Memoize;
/**
* Represents an Argument or a value and provides several helpers related to parsing an argument list.
*/
class Argument extends Memoize {
/**
* The canonical name of this argument, used for aliasing.
*
* @param string
*/
public $key;
private $_argument;
private $_raw;
/**
* @param string $argument The raw argument, leading dashes included.
*/
public function __construct($argument) {
$this->_raw = $argument;
$this->key =& $this->_argument;
if ($this->isLong) {
$this->_argument = substr($this->_raw, 2);
} else if ($this->isShort) {
$this->_argument = substr($this->_raw, 1);
} else {
$this->_argument = $this->_raw;
}
}
/**
* Returns the raw input as a string.
*
* @return string
*/
public function __toString() {
return (string)$this->_raw;
}
/**
* Returns the formatted argument string.
*
* @return string
*/
public function value() {
return $this->_argument;
}
/**
* Returns the raw input.
*
* @return mixed
*/
public function raw() {
return $this->_raw;
}
/**
* Returns true if the string matches the pattern for long arguments.
*
* @return bool
*/
public function isLong() {
return (0 == strncmp($this->_raw, '--', 2));
}
/**
* Returns true if the string matches the pattern for short arguments.
*
* @return bool
*/
public function isShort() {
return !$this->isLong && (0 == strncmp($this->_raw, '-', 1));
}
/**
* Returns true if the string matches the pattern for arguments.
*
* @return bool
*/
public function isArgument() {
return $this->isShort() || $this->isLong();
}
/**
* Returns true if the string matches the pattern for values.
*
* @return bool
*/
public function isValue() {
return !$this->isArgument;
}
/**
* Returns true if the argument is short but contains several characters. Each
* character is considered a separate argument.
*
* @return bool
*/
public function canExplode() {
return $this->isShort && strlen($this->_argument) > 1;
}
/**
* Returns all but the first character of the argument, removing them from the
* objects representation at the same time.
*
* @return array
*/
public function exploded() {
$exploded = array();
for ($i = strlen($this->_argument); $i > 0; $i--) {
array_push($exploded, $this->_argument[$i - 1]);
}
$this->_argument = array_pop($exploded);
$this->_raw = '-' . $this->_argument;
return $exploded;
}
}
@@ -0,0 +1,122 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Arguments;
/**
* Arguments help screen renderer
*/
class HelpScreen {
protected $_flags = array();
protected $_maxFlag = 0;
protected $_options = array();
protected $_maxOption = 0;
public function __construct(Arguments $arguments) {
$this->setArguments($arguments);
}
public function __toString() {
return $this->render();
}
public function setArguments(Arguments $arguments) {
$this->consumeArgumentFlags($arguments);
$this->consumeArgumentOptions($arguments);
}
public function consumeArgumentFlags(Arguments $arguments) {
$data = $this->_consume($arguments->getFlags());
$this->_flags = $data[0];
$this->_flagMax = $data[1];
}
public function consumeArgumentOptions(Arguments $arguments) {
$data = $this->_consume($arguments->getOptions());
$this->_options = $data[0];
$this->_optionMax = $data[1];
}
public function render() {
$help = array();
array_push($help, $this->_renderFlags());
array_push($help, $this->_renderOptions());
return join($help, "\n\n");
}
private function _renderFlags() {
if (empty($this->_flags)) {
return null;
}
return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax);
}
private function _renderOptions() {
if (empty($this->_options)) {
return null;
}
return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax);
}
private function _renderScreen($options, $max) {
$help = array();
foreach ($options as $option => $settings) {
$formatted = ' ' . str_pad($option, $max);
$dlen = 80 - 4 - $max;
$description = str_split($settings['description'], $dlen);
$formatted.= ' ' . array_shift($description);
if ($settings['default']) {
$formatted .= ' [default: ' . $settings['default'] . ']';
}
$pad = str_repeat(' ', $max + 3);
while ($desc = array_shift($description)) {
$formatted .= "\n${pad}${desc}";
}
array_push($help, $formatted);
}
return join($help, "\n");
}
private function _consume($options) {
$max = 0;
$out = array();
foreach ($options as $option => $settings) {
$names = array('--' . $option);
foreach ($settings['aliases'] as $alias) {
array_push($names, '-' . $alias);
}
$names = join($names, ', ');
$max = max(strlen($names), $max);
$out[$names] = $settings;
}
return array($out, $max);
}
}
@@ -0,0 +1,43 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
/**
* Thrown when undefined arguments are detected in strict mode.
*/
class InvalidArguments extends \InvalidArgumentException {
protected $arguments;
/**
* @param array $arguments A list of arguments that do not fit the profile.
*/
public function __construct(array $arguments) {
$this->arguments = $arguments;
$this->message = $this->_generateMessage();
}
/**
* Get the arguments that caused the exception.
*
* @return array
*/
public function getArguments() {
return $this->arguments;
}
private function _generateMessage() {
return 'unknown argument' .
(count($this->arguments) > 1 ? 's' : '') .
': ' . join($this->arguments, ', ');
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Memoize;
class Lexer extends Memoize implements \Iterator {
private $_items = array();
private $_index = 0;
private $_length = 0;
private $_first = true;
/**
* @param array $items A list of strings to process as tokens.
*/
public function __construct(array $items) {
$this->_items = $items;
$this->_length = count($items);
}
/**
* The current token.
*
* @return string
*/
public function current() {
return $this->_item;
}
/**
* Peek ahead to the next token without moving the cursor.
*
* @return Argument
*/
public function peek() {
return new Argument($this->_items[0]);
}
/**
* Move the cursor forward 1 element if it is valid.
*/
public function next() {
if ($this->valid()) {
$this->_shift();
}
}
/**
* Return the current position of the cursor.
*
* @return int
*/
public function key() {
return $this->_index;
}
/**
* Move forward 1 element and, if the method hasn't been called before, reset
* the cursor's position to 0.
*/
public function rewind() {
$this->_shift();
if ($this->_first) {
$this->_index = 0;
$this->_first = false;
}
}
/**
* Returns true if the cursor has not reached the end of the list.
*
* @return bool
*/
public function valid() {
return ($this->_index < $this->_length);
}
/**
* Push an element to the front of the stack.
*
* @param mixed $item The value to set
*/
public function unshift($item) {
array_unshift($this->_items, $item);
$this->_length += 1;
}
/**
* Returns true if the cursor is at the end of the list.
*
* @return bool
*/
public function end() {
return ($this->_index + 1) == $this->_length;
}
private function _shift() {
$this->_item = new Argument(array_shift($this->_items));
$this->_index += 1;
$this->_explode();
$this->_unmemo('peek');
}
private function _explode() {
if (!$this->_item->canExplode) {
return false;
}
foreach ($this->_item->exploded as $piece) {
$this->unshift('-' . $piece);
}
}
}
+415
View File
@@ -0,0 +1,415 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* Handles rendering strings. If extra scalar arguments are given after the `$msg`
* the string will be rendered with `sprintf`. If the second argument is an `array`
* then each key in the array will be the placeholder name. Placeholders are of the
* format {:key}.
*
* @param string $msg The message to render.
* @param mixed ... Either scalar arguments or a single array argument.
* @return string The rendered string.
*/
function render( $msg ) {
return Streams::_call( 'render', func_get_args() );
}
/**
* Shortcut for printing to `STDOUT`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see \cli\render()
*/
function out( $msg ) {
Streams::_call( 'out', func_get_args() );
}
/**
* Pads `$msg` to the width of the shell before passing to `cli\out`.
*
* @param string $msg The message to pad and pass on.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see cli\out()
*/
function out_padded( $msg ) {
Streams::_call( 'out_padded', func_get_args() );
}
/**
* Prints a message to `STDOUT` with a newline appended. See `\cli\out` for
* more documentation.
*
* @see cli\out()
*/
function line( $msg = '' ) {
Streams::_call( 'line', func_get_args() );
}
/**
* Shortcut for printing to `STDERR`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format. With no string,
* a newline is printed.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
*/
function err( $msg = '' ) {
Streams::_call( 'err', func_get_args() );
}
/**
* Takes input from `STDIN` in the given format. If an end of transmission
* character is sent (^D), an exception is thrown.
*
* @param string $format A valid input format. See `fscanf` for documentation.
* If none is given, all input up to the first newline
* is accepted.
* @return string The input with whitespace trimmed.
* @throws \Exception Thrown if ctrl-D (EOT) is sent as input.
*/
function input( $format = null ) {
return Streams::input( $format );
}
/**
* Displays an input prompt. If no default value is provided the prompt will
* continue displaying until input is received.
*
* @param string $question The question to ask the user.
* @param string $default A default value if the user provides no input.
* @param string $marker A string to append to the question and default value on display.
* @param boolean $hide If the user input should be hidden
* @return string The users input.
* @see cli\input()
*/
function prompt( $question, $default = false, $marker = ': ', $hide = false ) {
return Streams::prompt( $question, $default, $marker, $hide );
}
/**
* Presents a user with a multiple choice question, useful for 'yes/no' type
* questions (which this function defaults too).
*
* @param string $question The question to ask the user.
* @param string $choice
* @param string|null $default The default choice. NULL if a default is not allowed.
* @internal param string $valid A string of characters allowed as a response. Case
* is ignored.
* @return string The users choice.
* @see cli\prompt()
*/
function choose( $question, $choice = 'yn', $default = 'n' ) {
return Streams::choose( $question, $choice, $default );
}
/**
* Does the same as {@see choose()}, but always asks yes/no and returns a boolean
*
* @param string $question The question to ask the user.
* @param bool|null $default The default choice, in a boolean format.
* @return bool
*/
function confirm( $question, $default = false ) {
if ( is_bool( $default ) ) {
$default = $default? 'y' : 'n';
}
$result = choose( $question, 'yn', $default );
return $result == 'y';
}
/**
* Displays an array of strings as a menu where a user can enter a number to
* choose an option. The array must be a single dimension with either strings
* or objects with a `__toString()` method.
*
* @param array $items The list of items the user can choose from.
* @param string $default The index of the default item.
* @param string $title The message displayed to the user when prompted.
* @return string The index of the chosen item.
* @see cli\line()
* @see cli\input()
* @see cli\err()
*/
function menu( $items, $default = null, $title = 'Choose an item' ) {
return Streams::menu( $items, $default, $title );
}
/**
* Attempts an encoding-safe way of getting string length. If intl extension or PCRE with '\X' or mb_string extension aren't
* available, falls back to basic strlen.
*
* @param string $str The string to check.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int Numeric value that represents the string's length
*/
function safe_strlen( $str, $encoding = false ) {
// Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen().
$test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' );
// Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) {
if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) {
return $length;
}
}
// Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $length = preg_match_all( '/\X/u', $str, $dummy /*needed for PHP 5.3*/ ) ) ) {
if ( ! $test_safe_strlen || ( $test_safe_strlen & 2 ) ) {
return $length;
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $str, null, true /*strict*/ );
}
$length = $encoding ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding ) {
// Subtract combining characters.
$length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ );
}
if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) {
return $length;
}
}
return strlen( $str );
}
/**
* Attempts an encoding-safe way of getting a substring. If intl extension or PCRE with '\X' or mb_string extension aren't
* available, falls back to substr().
*
* @param string $str The input string.
* @param int $start The starting position of the substring.
* @param int|bool|null $length Optional, unless $is_width is set. Maximum length of the substring. Default false. Negative not supported.
* @param int|bool $is_width Optional. If set and encoding is UTF-8, $length (which must be specified) is interpreted as spacing width. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return bool|string False if given unsupported args, otherwise substring of string specified by start and length parameters
*/
function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) {
// Negative $length or $is_width and $length not specified not supported.
if ( $length < 0 || ( $is_width && ( null === $length || false === $length ) ) ) {
return false;
}
// Need this for normalization below and other uses.
$safe_strlen = safe_strlen( $str, $encoding );
// Normalize `$length` when not specified - PHP 5.3 substr takes false as full length, PHP > 5.3 takes null.
if ( null === $length || false === $length ) {
$length = $safe_strlen;
}
// Normalize `$start` - various methods treat this differently.
if ( $start > $safe_strlen ) {
return '';
}
if ( $start < 0 && -$start > $safe_strlen ) {
$start = 0;
}
// Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_split( '/\X/' ), "4" mb_substr(), "8" substr().
$test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' );
// Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) {
if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) {
return $is_width ? _safe_substr_eaw( $try, $length ) : $try;
}
}
// Assume UTF-8 if no encoding given - `preg_split()` returns a one element array if given non-UTF-8 string (PHP bug) so need to check `preg_last_error()`.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() ) {
if ( false !== ( $try = preg_split( '/(\X)/u', $str, $safe_strlen + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) ) && ! preg_last_error() ) {
$try = implode( '', array_slice( $try, $start, $length ) );
if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) {
return $is_width ? _safe_substr_eaw( $try, $length ) : $try;
}
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $str, null, true /*strict*/ );
}
// Bug: not adjusting for combining chars.
$try = $encoding ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding && $is_width ) {
$try = _safe_substr_eaw( $try, $length );
}
if ( ! $test_safe_substr || ( $test_safe_substr & 4 ) ) {
return $try;
}
}
return substr( $str, $start, $length );
}
/**
* Internal function used by `safe_substr()` to adjust for East Asian double-width chars.
*
* @return string
*/
function _safe_substr_eaw( $str, $length ) {
// Set the East Asian Width regex.
$eaw_regex = get_unicode_regexs( 'eaw' );
// If there's any East Asian double-width chars...
if ( preg_match( $eaw_regex, $str ) ) {
// Note that if the length ends in the middle of a double-width char, the char is excluded, not included.
// See if it's all EAW.
if ( function_exists( 'mb_substr' ) && preg_match_all( $eaw_regex, $str, $dummy /*needed for PHP 5.3*/ ) === $length ) {
// Just halve the length so (rounded down to a minimum of 1).
$str = mb_substr( $str, 0, max( (int) ( $length / 2 ), 1 ), 'UTF-8' );
} else {
// Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php".
$chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
$cnt = min( count( $chars ), $length );
$width = $length;
for ( $length = 0; $length < $cnt && $width > 0; $length++ ) {
$width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1;
}
// Round down to a minimum of 1.
if ( $width < 0 && $length > 1 ) {
$length--;
}
return join( '', array_slice( $chars, 0, $length ) );
}
}
return $str;
}
/**
* An encoding-safe way of padding string length for display
*
* @param string $string The string to pad.
* @param int $length The length to pad it to.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return string
*/
function safe_str_pad( $string, $length, $encoding = false ) {
$real_length = strwidth( $string, $encoding );
$diff = strlen( $string ) - $real_length;
$length += $diff;
return str_pad( $string, $length );
}
/**
* Get width of string, ie length in characters, taking into account multi-byte and mark characters for UTF-8, and multi-byte for non-UTF-8.
*
* @param string $string The string to check.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int The string's width.
*/
function strwidth( $string, $encoding = false ) {
// Set the East Asian Width and Mark regexs.
list( $eaw_regex, $m_regex ) = get_unicode_regexs();
// Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen().
$test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );
// Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) {
if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) {
return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
}
// Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) {
if ( ! $test_strwidth || ( $test_strwidth & 2 ) ) {
return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $string, null, true /*strict*/ );
}
$width = $encoding ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding ) {
// Subtract combining characters.
$width -= preg_match_all( $m_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
if ( ! $test_strwidth || ( $test_strwidth & 4 ) ) {
return $width;
}
}
return safe_strlen( $string, $encoding );
}
/**
* Returns whether ICU is modern enough not to flake out.
*
* @return bool
*/
function can_use_icu() {
static $can_use_icu = null;
if ( null === $can_use_icu ) {
// Choosing ICU 54, Unicode 7.0.
$can_use_icu = defined( 'INTL_ICU_VERSION' ) && version_compare( INTL_ICU_VERSION, '54.1', '>=' ) && function_exists( 'grapheme_strlen' ) && function_exists( 'grapheme_substr' );
}
return $can_use_icu;
}
/**
* Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use.
*
* @return bool
*/
function can_use_pcre_x() {
static $can_use_pcre_x = null;
if ( null === $can_use_pcre_x ) {
// '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53.
// Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13.
$pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff.
$can_use_pcre_x = version_compare( $pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' );
}
return $can_use_pcre_x;
}
/**
* Get the regexs generated from Unicode data.
*
* @param string $idx Optional. Return a specific regex only. Default null.
* @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string.
*/
function get_unicode_regexs( $idx = null ) {
static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html
static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes.
if ( null === $eaw_regex ) {
// Load both regexs generated from Unicode data.
require __DIR__ . '/unicode/regex.php';
}
if ( null !== $idx ) {
if ( 'eaw' === $idx ) {
return $eaw_regex;
}
if ( 'm' === $idx ) {
return $m_regex;
}
}
return array( $eaw_regex, $m_regex, );
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\notify;
use cli\Notify;
use cli\Streams;
/**
* A Notifer that displays a string of periods.
*/
class Dots extends Notify {
protected $_dots;
protected $_format = '{:msg}{:dots} ({:elapsed}, {:speed}/s)';
protected $_iteration;
/**
* Instatiates a Notification object.
*
* @param string $msg The text to display next to the Notifier.
* @param int $dots The number of dots to iterate through.
* @param int $interval The interval in milliseconds between updates.
* @throws \InvalidArgumentException
*/
public function __construct($msg, $dots = 3, $interval = 100) {
parent::__construct($msg, $interval);
$this->_dots = (int)$dots;
if ($this->_dots <= 0) {
throw new \InvalidArgumentException('Dot count out of range, must be positive.');
}
}
/**
* Prints the correct number of dots to `STDOUT` with the time elapsed and
* tick speed.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out_padded()
* @see cli\Notify::formatTime()
* @see cli\Notify::speed()
*/
public function display($finish = false) {
$repeat = $this->_dots;
if (!$finish) {
$repeat = $this->_iteration++ % $repeat;
}
$msg = $this->_message;
$dots = str_pad(str_repeat('.', $repeat), $this->_dots);
$speed = number_format(round($this->speed()));
$elapsed = $this->formatTime($this->elapsed());
Streams::out_padded($this->_format, compact('msg', 'dots', 'speed', 'elapsed'));
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\notify;
use cli\Notify;
use cli\Streams;
/**
* The `Spinner` Notifier displays an ASCII spinner.
*/
class Spinner extends Notify {
protected $_chars = '-\|/';
protected $_format = '{:msg} {:char} ({:elapsed}, {:speed}/s)';
protected $_iteration = 0;
/**
* Prints the current spinner position to `STDOUT` with the time elapsed
* and tick speed.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out_padded()
* @see cli\Notify::formatTime()
* @see cli\Notify::speed()
*/
public function display($finish = false) {
$msg = $this->_message;
$idx = $this->_iteration++ % strlen($this->_chars);
$char = $this->_chars[$idx];
$speed = number_format(round($this->speed()));
$elapsed = $this->formatTime($this->elapsed());
Streams::out_padded($this->_format, compact('msg', 'char', 'elapsed', 'speed'));
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\progress;
use cli;
use cli\Notify;
use cli\Progress;
use cli\Shell;
use cli\Streams;
/**
* Displays a progress bar spanning the entire shell.
*
* Basic format:
*
* ^MSG PER% [======================= ] 00:00 / 00:00$
*/
class Bar extends Progress {
protected $_bars = '=>';
protected $_formatMessage = '{:msg} {:percent}% [';
protected $_formatTiming = '] {:elapsed} / {:estimated}';
protected $_format = '{:msg}{:bar}{:timing}';
/**
* Prints the progress bar to the screen with percent complete, elapsed time
* and estimated total time.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out()
* @see cli\Notify::formatTime()
* @see cli\Notify::elapsed()
* @see cli\Progress::estimated();
* @see cli\Progress::percent()
* @see cli\Shell::columns()
*/
public function display($finish = false) {
$_percent = $this->percent();
$percent = str_pad(floor($_percent * 100), 3);
$msg = $this->_message;
$msg = Streams::render($this->_formatMessage, compact('msg', 'percent'));
$estimated = $this->formatTime($this->estimated());
$elapsed = str_pad($this->formatTime($this->elapsed()), strlen($estimated));
$timing = Streams::render($this->_formatTiming, compact('elapsed', 'estimated'));
$size = Shell::columns();
$size -= strlen($msg . $timing);
if ( $size < 0 ) {
$size = 0;
}
$bar = str_repeat($this->_bars[0], floor($_percent * $size)) . $this->_bars[1];
// substr is needed to trim off the bar cap at 100%
$bar = substr(str_pad($bar, $size, ' '), 0, $size);
Streams::out($this->_format, compact('msg', 'bar', 'timing'));
}
/**
* This method augments the base definition from cli\Notify to optionally
* allow passing a new message.
*
* @param int $increment The amount to increment by.
* @param string $msg The text to display next to the Notifier. (optional)
* @see cli\Notify::tick()
*/
public function tick($increment = 1, $msg = null) {
if ($msg) {
$this->_message = $msg;
}
Notify::tick($increment);
}
}
+227
View File
@@ -0,0 +1,227 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\table;
use cli\Colors;
use cli\Shell;
/**
* The ASCII renderer renders tables with ASCII borders.
*/
class Ascii extends Renderer {
protected $_characters = array(
'corner' => '+',
'line' => '-',
'border' => '|',
'padding' => ' ',
);
protected $_border = null;
protected $_constraintWidth = null;
protected $_pre_colorized = false;
/**
* Set the widths of each column in the table.
*
* @param array $widths The widths of the columns.
* @param bool $fallback Whether to use these values as fallback only.
*/
public function setWidths(array $widths, $fallback = false) {
if ($fallback) {
foreach ( $this->_widths as $index => $value ) {
$widths[$index] = $value;
}
}
$this->_widths = $widths;
if ( is_null( $this->_constraintWidth ) ) {
$this->_constraintWidth = (int) Shell::columns();
}
$col_count = count( $widths );
$col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0;
$table_borders_count = strlen( $this->_characters['border'] ) * 2;
$col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2;
$max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count;
if ( $widths && $max_width && array_sum( $widths ) > $max_width ) {
$avg = floor( $max_width / count( $widths ) );
$resize_widths = array();
$extra_width = 0;
foreach( $widths as $width ) {
if ( $width > $avg ) {
$resize_widths[] = $width;
} else {
$extra_width = $extra_width + ( $avg - $width );
}
}
if ( ! empty( $resize_widths ) && $extra_width ) {
$avg_extra_width = floor( $extra_width / count( $resize_widths ) );
foreach( $widths as &$width ) {
if ( in_array( $width, $resize_widths ) ) {
$width = $avg + $avg_extra_width;
array_shift( $resize_widths );
// Last item gets the cake
if ( empty( $resize_widths ) ) {
$width = 0; // Zero it so not in sum.
$width = $max_width - array_sum( $widths );
}
}
}
}
}
$this->_widths = $widths;
}
/**
* Set the constraint width for the table
*
* @param int $constraintWidth
*/
public function setConstraintWidth( $constraintWidth ) {
$this->_constraintWidth = $constraintWidth;
}
/**
* Set the characters used for rendering the Ascii table.
*
* The keys `corner`, `line` and `border` are used in rendering.
*
* @param $characters array Characters used in rendering.
*/
public function setCharacters(array $characters) {
$this->_characters = array_merge($this->_characters, $characters);
}
/**
* Render a border for the top and bottom and separating the headers from the
* table rows.
*
* @return string The table border.
*/
public function border() {
if (!isset($this->_border)) {
$this->_border = $this->_characters['corner'];
foreach ($this->_widths as $width) {
$this->_border .= str_repeat($this->_characters['line'], $width + 2);
$this->_border .= $this->_characters['corner'];
}
}
return $this->_border;
}
/**
* Renders a row for output.
*
* @param array $row The table row.
* @return string The formatted table row.
*/
public function row( array $row ) {
$extra_row_count = 0;
if ( count( $row ) > 0 ) {
$extra_rows = array_fill( 0, count( $row ), array() );
foreach( $row as $col => $value ) {
$value = str_replace( array( "\r\n", "\n" ), ' ', $value );
$col_width = $this->_widths[ $col ];
$encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false;
$original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding );
if ( $col_width && $original_val_width > $col_width ) {
$row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding );
$value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ], $encoding ), null /*length*/, false /*is_width*/, $encoding );
$i = 0;
do {
$extra_value = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding );
$val_width = Colors::width( $extra_value, self::isPreColorized( $col ), $encoding );
if ( $val_width ) {
$extra_rows[ $col ][] = $extra_value;
$value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
$i++;
if ( $i > $extra_row_count ) {
$extra_row_count = $i;
}
}
} while( $value );
}
}
}
$row = array_map(array($this, 'padColumn'), $row, array_keys($row));
array_unshift($row, ''); // First border
array_push($row, ''); // Last border
$ret = join($this->_characters['border'], $row);
if ( $extra_row_count ) {
foreach( $extra_rows as $col => $col_values ) {
while( count( $col_values ) < $extra_row_count ) {
$col_values[] = '';
}
}
do {
$row_values = array();
$has_more = false;
foreach( $extra_rows as $col => &$col_values ) {
$row_values[ $col ] = array_shift( $col_values );
if ( count( $col_values ) ) {
$has_more = true;
}
}
$row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values));
array_unshift($row_values, ''); // First border
array_push($row_values, ''); // Last border
$ret .= PHP_EOL . join($this->_characters['border'], $row_values);
} while( $has_more );
}
return $ret;
}
private function padColumn($content, $column) {
return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding'];
}
/**
* Set whether items are pre-colorized.
*
* @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized.
*/
public function setPreColorized( $pre_colorized ) {
$this->_pre_colorized = $pre_colorized;
}
/**
* Is a column pre-colorized?
*
* @param int $column Column index to check.
* @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false.
*/
public function isPreColorized( $column ) {
if ( is_bool( $this->_pre_colorized ) ) {
return $this->_pre_colorized;
}
if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) {
return $this->_pre_colorized[ $column ];
}
return false;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\table;
/**
* Table renderers are used to change how a table is displayed.
*/
abstract class Renderer {
protected $_widths = array();
public function __construct(array $widths = array()) {
$this->setWidths($widths);
}
/**
* Set the widths of each column in the table.
*
* @param array $widths The widths of the columns.
* @param bool $fallback Whether to use these values as fallback only.
*/
public function setWidths(array $widths, $fallback = false) {
if ($fallback) {
foreach ( $this->_widths as $index => $value ) {
$widths[$index] = $value;
}
}
$this->_widths = $widths;
}
/**
* Render a border for the top and bottom and separating the headers from the
* table rows.
*
* @return string The table border.
*/
public function border() {
return null;
}
/**
* Renders a row for output.
*
* @param array $row The table row.
* @return string The formatted table row.
*/
abstract public function row(array $row);
}
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\table;
/**
* The tabular renderer is used for displaying data in a tabular format.
*/
class Tabular extends Renderer {
/**
* Renders a row for output.
*
* @param array $row The table row.
* @return string The formatted table row.
*/
public function row(array $row) {
return implode("\t", array_values($row));
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author Ryan Sullivan <rsullivan@connectstudios.com>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\tree;
/**
* The ASCII renderer renders trees with ASCII lines.
*/
class Ascii extends Renderer {
/**
* @param array $tree
* @return string
*/
public function render(array $tree)
{
$output = '';
$treeIterator = new \RecursiveTreeIterator(
new \RecursiveArrayIterator($tree),
\RecursiveTreeIterator::SELF_FIRST
);
foreach ($treeIterator as $val)
{
$output .= $val . "\n";
}
return $output;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author Ryan Sullivan <rsullivan@connectstudios.com>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\tree;
/**
* The ASCII renderer renders trees with ASCII lines.
*/
class Markdown extends Renderer {
/**
* How many spaces to indent by
* @var int
*/
protected $_padding = 2;
/**
* @param int $padding Optional. Default 2.
*/
function __construct($padding = null)
{
if ($padding)
{
$this->_padding = $padding;
}
}
/**
* Renders the tree
*
* @param array $tree
* @param int $level Optional
* @return string
*/
public function render(array $tree, $level = 0)
{
$output = '';
foreach ($tree as $label => $next)
{
if (is_string($next))
{
$label = $next;
}
// Output the label
$output .= sprintf("%s- %s\n", str_repeat(' ', $level * $this->_padding), $label);
// Next level
if (is_array($next))
{
$output .= $this->render($next, $level + 1);
}
}
return $output;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author Ryan Sullivan <rsullivan@connectstudios.com>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\tree;
/**
* Tree renderers are used to change how a tree is displayed.
*/
abstract class Renderer {
/**
* @param array $tree
* @return string|null
*/
abstract public function render(array $tree);
}
+6
View File
@@ -0,0 +1,6 @@
<?php
// Generated by "gen_east_asian_width.php" from "http://www.unicode.org/Public/10.0.0/ucd/EastAsianWidth.txt".
$eaw_regex = '/\xe1(?:\x84[\x80-\xbf]|\x85[\x80-\x9f])|\xe2(?:\x8c[\x9a\x9b\xa9\xaa]|\x8f[\xa9-\xac\xb0\xb3]|\x97[\xbd\xbe]|\x98[\x94\x95]|\x99[\x88-\x93\xbf]|\x9a[\x93\xa1\xaa\xab\xbd\xbe]|\x9b[\x84\x85\x8e\x94\xaa\xb2\xb3\xb5\xba\xbd]|\x9c[\x85\x8a\x8b\xa8]|\x9d[\x8c\x8e\x93-\x95\x97]|\x9e[\x95-\x97\xb0\xbf]|\xac[\x9b\x9c]|\xad[\x90\x95]|\xba[\x80-\x99\x9b-\xbf]|\xbb[\x80-\xb3]|[\xbc-\xbe][\x80-\xbf]|\xbf[\x80-\x95\xb0-\xbb])|\xe3(?:\x80[\x80-\xbe]|\x81[\x81-\xbf]|\x82[\x80-\x96\x99-\xbf]|\x83[\x80-\xbf]|\x84[\x85-\xae\xb1-\xbf]|\x85[\x80-\xbf]|\x86[\x80-\x8e\x90-\xba]|\x87[\x80-\xa3\xb0-\xbf]|\x88[\x80-\x9e\xa0-\xbf]|\x89[\x80-\x87\x90-\xbf]|\x8a[\x80-\xbf]|\x8b[\x80-\xbe]|[\x8c-\xbf][\x80-\xbf])|\xe4(?:[\x80-\xb6][\x80-\xbf]|[\xb8-\xbf][\x80-\xbf])|[\xe5-\xe9][\x80-\xbf][\x80-\xbf]|\xea(?:[\x80-\x91][\x80-\xbf]|\x92[\x80-\x8c\x90-\xbf]|\x93[\x80-\x86]|\xa5[\xa0-\xbc]|[\xb0-\xbf][\x80-\xbf])|[\xeb\xec][\x80-\xbf][\x80-\xbf]|\xed(?:[\x80-\x9d][\x80-\xbf]|\x9e[\x80-\xa3])|\xef(?:[\xa4-\xab][\x80-\xbf]|\xb8[\x90-\x99\xb0-\xbf]|\xb9[\x80-\x92\x94-\xa6\xa8-\xab]|\xbc[\x81-\xbf]|\xbd[\x80-\xa0]|\xbf[\xa0-\xa6])|\xf0(?:\x96\xbf[\xa0\xa1]|\x97[\x80-\xbf][\x80-\xbf]|\x98(?:[\x80-\x9e][\x80-\xbf]|\x9f[\x80-\xac]|[\xa0-\xaa][\x80-\xbf]|\xab[\x80-\xb2])|\x9b(?:[\x80-\x83][\x80-\xbf]|\x84[\x80-\x9e]|\x85[\xb0-\xbf]|[\x86-\x8a][\x80-\xbf]|\x8b[\x80-\xbb])|\x9f(?:\x80\x84|\x83\x8f|\x86[\x8e\x91-\x9a]|\x88[\x80-\x82\x90-\xbb]|\x89[\x80-\x88\x90\x91\xa0-\xa5]|\x8c[\x80-\xa0\xad-\xb5\xb7-\xbf]|\x8d[\x80-\xbc\xbe\xbf]|\x8e[\x80-\x93\xa0-\xbf]|\x8f[\x80-\x8a\x8f-\x93\xa0-\xb0\xb4\xb8-\xbf]|\x90[\x80-\xbe]|\x91[\x80\x82-\xbf]|\x92[\x80-\xbf]|\x93[\x80-\xbc\xbf]|\x94[\x80-\xbd]|\x95[\x8b-\x8e\x90-\xa7\xba]|\x96[\x95\x96\xa4]|\x97[\xbb-\xbf]|\x98[\x80-\xbf]|\x99[\x80-\x8f]|\x9a[\x80-\xbf]|\x9b[\x80-\x85\x8c\x90-\x92\xab\xac\xb4-\xb8]|\xa4[\x90-\xbe]|\xa5[\x80-\x8c\x90-\xab]|\xa6[\x80-\x97]|\xa7[\x80\x90-\xa6])|[\xa0-\xae][\x80-\xbf][\x80-\xbf]|\xaf(?:[\x80-\xbe][\x80-\xbf]|\xbf[\x80-\xbd])|[\xb0-\xbe][\x80-\xbf][\x80-\xbf]|\xbf(?:[\x80-\xbe][\x80-\xbf]|\xbf[\x80-\xbd]))/'; // 181738 code points.
// Generated by "gen_cat_regex_alts.php" from "http://www.unicode.org/Public/10.0.0/ucd/UnicodeData.txt".
$m_regex = '/\xcc[\x80-\xbf]|\xcd[\x80-\xaf]|\xd2[\x83-\x89]|\xd6[\x91-\xbd\xbf]|\xd7[\x81\x82\x84\x85\x87]|\xd8[\x90-\x9a]|\xd9[\x8b-\x9f\xb0]|\xdb[\x96-\x9c\x9f-\xa4\xa7\xa8\xaa-\xad]|\xdc[\x91\xb0-\xbf]|\xdd[\x80-\x8a]|\xde[\xa6-\xb0]|\xdf[\xab-\xb3]|\xe0(?:\xa0[\x96-\x99\x9b-\xa3\xa5-\xa7\xa9-\xad]|\xa1[\x99-\x9b]|\xa3[\x94-\xa1\xa3-\xbf]|\xa4[\x80-\x83\xba-\xbc\xbe\xbf]|\xa5[\x80-\x8f\x91-\x97\xa2\xa3]|\xa6[\x81-\x83\xbc\xbe\xbf]|\xa7[\x80-\x84\x87\x88\x8b-\x8d\x97\xa2\xa3]|\xa8[\x81-\x83\xbc\xbe\xbf]|\xa9[\x80-\x82\x87\x88\x8b-\x8d\x91\xb0\xb1\xb5]|\xaa[\x81-\x83\xbc\xbe\xbf]|\xab[\x80-\x85\x87-\x89\x8b-\x8d\xa2\xa3\xba-\xbf]|\xac[\x81-\x83\xbc\xbe\xbf]|\xad[\x80-\x84\x87\x88\x8b-\x8d\x96\x97\xa2\xa3]|\xae[\x82\xbe\xbf]|\xaf[\x80-\x82\x86-\x88\x8a-\x8d\x97]|\xb0[\x80-\x83\xbe\xbf]|\xb1[\x80-\x84\x86-\x88\x8a-\x8d\x95\x96\xa2\xa3]|\xb2[\x81-\x83\xbc\xbe\xbf]|\xb3[\x80-\x84\x86-\x88\x8a-\x8d\x95\x96\xa2\xa3]|\xb4[\x80-\x83\xbb\xbc\xbe\xbf]|\xb5[\x80-\x84\x86-\x88\x8a-\x8d\x97\xa2\xa3]|\xb6[\x82\x83]|\xb7[\x8a\x8f-\x94\x96\x98-\x9f\xb2\xb3]|\xb8[\xb1\xb4-\xba]|\xb9[\x87-\x8e]|\xba[\xb1\xb4-\xb9\xbb\xbc]|\xbb[\x88-\x8d]|\xbc[\x98\x99\xb5\xb7\xb9\xbe\xbf]|\xbd[\xb1-\xbf]|\xbe[\x80-\x84\x86\x87\x8d-\x97\x99-\xbc]|\xbf\x86)|\xe1(?:\x80[\xab-\xbe]|\x81[\x96-\x99\x9e-\xa0\xa2-\xa4\xa7-\xad\xb1-\xb4]|\x82[\x82-\x8d\x8f\x9a-\x9d]|\x8d[\x9d-\x9f]|\x9c[\x92-\x94\xb2-\xb4]|\x9d[\x92\x93\xb2\xb3]|\x9e[\xb4-\xbf]|\x9f[\x80-\x93\x9d]|\xa0[\x8b-\x8d]|\xa2[\x85\x86\xa9]|\xa4[\xa0-\xab\xb0-\xbb]|\xa8[\x97-\x9b]|\xa9[\x95-\x9e\xa0-\xbc\xbf]|\xaa[\xb0-\xbe]|\xac[\x80-\x84\xb4-\xbf]|\xad[\x80-\x84\xab-\xb3]|\xae[\x80-\x82\xa1-\xad]|\xaf[\xa6-\xb3]|\xb0[\xa4-\xb7]|\xb3[\x90-\x92\x94-\xa8\xad\xb2-\xb4\xb7-\xb9]|\xb7[\x80-\xb9\xbb-\xbf])|\xe2(?:\x83[\x90-\xb0]|\xb3[\xaf-\xb1]|\xb5\xbf|\xb7[\xa0-\xbf])|\xe3(?:\x80[\xaa-\xaf]|\x82[\x99\x9a])|\xea(?:\x99[\xaf-\xb2\xb4-\xbd]|\x9a[\x9e\x9f]|\x9b[\xb0\xb1]|\xa0[\x82\x86\x8b\xa3-\xa7]|\xa2[\x80\x81\xb4-\xbf]|\xa3[\x80-\x85\xa0-\xb1]|\xa4[\xa6-\xad]|\xa5[\x87-\x93]|\xa6[\x80-\x83\xb3-\xbf]|\xa7[\x80\xa5]|\xa8[\xa9-\xb6]|\xa9[\x83\x8c\x8d\xbb-\xbd]|\xaa[\xb0\xb2-\xb4\xb7\xb8\xbe\xbf]|\xab[\x81\xab-\xaf\xb5\xb6]|\xaf[\xa3-\xaa\xac\xad])|\xef(?:\xac\x9e|\xb8[\x80-\x8f\xa0-\xaf])|\xf0(?:\x90(?:\x87\xbd|\x8b\xa0|\x8d[\xb6-\xba]|\xa8[\x81-\x83\x85\x86\x8c-\x8f\xb8-\xba\xbf]|\xab[\xa5\xa6])|\x91(?:\x80[\x80-\x82\xb8-\xbf]|\x81[\x80-\x86\xbf]|\x82[\x80-\x82\xb0-\xba]|\x84[\x80-\x82\xa7-\xb4]|\x85\xb3|\x86[\x80-\x82\xb3-\xbf]|\x87[\x80\x8a-\x8c]|\x88[\xac-\xb7\xbe]|\x8b[\x9f-\xaa]|\x8c[\x80-\x83\xbc\xbe\xbf]|\x8d[\x80-\x84\x87\x88\x8b-\x8d\x97\xa2\xa3\xa6-\xac\xb0-\xb4]|\x90[\xb5-\xbf]|\x91[\x80-\x86]|\x92[\xb0-\xbf]|\x93[\x80-\x83]|\x96[\xaf-\xb5\xb8-\xbf]|\x97[\x80\x9c\x9d]|\x98[\xb0-\xbf]|\x99\x80|\x9a[\xab-\xb7]|\x9c[\x9d-\xab]|\xa8[\x81-\x8a\xb3-\xb9\xbb-\xbe]|\xa9[\x87\x91-\x9b]|\xaa[\x8a-\x99]|\xb0[\xaf-\xb6\xb8-\xbf]|\xb2[\x92-\xa7\xa9-\xb6]|\xb4[\xb1-\xb6\xba\xbc\xbd\xbf]|\xb5[\x80-\x85\x87])|\x96(?:\xab[\xb0-\xb4]|\xac[\xb0-\xb6]|\xbd[\x91-\xbe]|\xbe[\x8f-\x92])|\x9b\xb2[\x9d\x9e]|\x9d(?:\x85[\xa5-\xa9\xad-\xb2\xbb-\xbf]|\x86[\x80-\x82\x85-\x8b\xaa-\xad]|\x89[\x82-\x84]|\xa8[\x80-\xb6\xbb-\xbf]|\xa9[\x80-\xac\xb5]|\xaa[\x84\x9b-\x9f\xa1-\xaf])|\x9e(?:\x80[\x80-\x86\x88-\x98\x9b-\xa1\xa3\xa4\xa6-\xaa]|\xa3[\x90-\x96]|\xa5[\x84-\x8a]))|\xf3\xa0(?:[\x84-\x86][\x80-\xbf]|\x87[\x80-\xaf])/'; // 2177 code points.