284 lines
8.8 KiB
PHP
Executable File
284 lines
8.8 KiB
PHP
Executable File
<?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() {
|
|
if ( function_exists('stream_isatty') ) {
|
|
return stream_isatty(static::$out);
|
|
} else {
|
|
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 );
|
|
} );
|
|
}
|
|
|
|
}
|