-
Notifications
You must be signed in to change notification settings - Fork 115
Expand file tree
/
Copy pathStreams.php
More file actions
executable file
·285 lines (249 loc) · 8.85 KB
/
Streams.php
File metadata and controls
executable file
·285 lines (249 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
<?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] ) ) {
// Normalize color tokens before sprintf: colorize or strip them so no raw %tokens reach sprintf
if ( Colors::shouldColorize() ) {
$args[0] = Colors::colorize( $args[0] );
} else {
$args[0] = Colors::decolorize( $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 = false, $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 ) );
// Separate 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 );
} );
}
}