diff --git a/ext/readline/readline.c b/ext/readline/readline.c index 2b001e8ebd896..eeb7cb06ccaf7 100644 --- a/ext/readline/readline.c +++ b/ext/readline/readline.c @@ -24,6 +24,9 @@ #include "php_readline.h" #include "readline_cli.h" #include "readline_arginfo.h" +#include "main/php_output.h" +#include +#include "zend_smart_str.h" #if HAVE_LIBREADLINE || HAVE_LIBEDIT @@ -44,7 +47,9 @@ static zval _prepped_callback; #endif + static zval _readline_completion; +static zval _readline_interactive_shell_result_function; static zval _readline_array; PHP_MINIT_FUNCTION(readline); @@ -79,6 +84,7 @@ PHP_MINIT_FUNCTION(readline) using_history(); #endif ZVAL_UNDEF(&_readline_completion); + ZVAL_NULL(&_readline_interactive_shell_result_function); #if HAVE_RL_CALLBACK_READ_CHAR ZVAL_UNDEF(&_prepped_callback); #endif @@ -94,6 +100,8 @@ PHP_RSHUTDOWN_FUNCTION(readline) { zval_ptr_dtor(&_readline_completion); ZVAL_UNDEF(&_readline_completion); + zval_ptr_dtor(&_readline_interactive_shell_result_function); + ZVAL_UNDEF(&_readline_interactive_shell_result_function); #if HAVE_RL_CALLBACK_READ_CHAR if (Z_TYPE(_prepped_callback) != IS_UNDEF) { rl_callback_handler_remove(); @@ -488,6 +496,62 @@ PHP_FUNCTION(readline_completion_function) RETURN_TRUE; } +bool php_readline_should_dump_interactive_result() +{ + return Z_TYPE(_readline_interactive_shell_result_function) != IS_UNDEF; +} + +void php_readline_dump_interactive_result(const char* code, const size_t codelen, zval *returned_zv) +{ + if (Z_TYPE(_readline_interactive_shell_result_function) == IS_NULL) { + ZVAL_DEREF(returned_zv); + if (Z_TYPE_P(returned_zv) > IS_NULL) { + if (Z_TYPE_P(returned_zv) >= IS_ARRAY) { + /* Use var_dump to dump arrays, objects, and resources. + * var_dump's representation distinguishes between ints/floats and can show recursive data structures. */ + PHPWRITE("=> ", sizeof("=> ") - 1); + php_var_dump(returned_zv, 1); + } else { + /* Use var_export to dump scalars */ + smart_str buf = {0}; + php_var_export_ex(returned_zv, 1, &buf); + PHPWRITE("=> ", sizeof("=> ") - 1); + PHPWRITE(ZSTR_VAL(buf.s), ZSTR_LEN(buf.s)); + smart_str_free(&buf); + } + } + } else if (Z_TYPE(_readline_interactive_shell_result_function) != IS_UNDEF) { + zval dump_result; + zval args[2]; + ZVAL_STRINGL(&args[0], code, codelen); + ZVAL_COPY_VALUE(&args[1], returned_zv); + call_user_function(NULL, NULL, &_readline_interactive_shell_result_function, &dump_result, 2, args); + zval_ptr_dtor(&dump_result); + zval_ptr_dtor(&args[0]); + } + zval_ptr_dtor(returned_zv); + ZVAL_UNDEF(returned_zv); +} + +PHP_FUNCTION(readline_interactive_shell_result_function) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "f!", &fci, &fcc)) { + RETURN_THROWS(); + } + + zval_ptr_dtor(&_readline_interactive_shell_result_function); + if (ZEND_FCI_INITIALIZED(fci)) { + ZVAL_COPY(&_readline_interactive_shell_result_function, &fci.function_name); + } else { + ZVAL_UNDEF(&_readline_interactive_shell_result_function); + } + + RETURN_TRUE; +} + /* }}} */ #if HAVE_RL_CALLBACK_READ_CHAR diff --git a/ext/readline/readline.stub.php b/ext/readline/readline.stub.php index cfc1d0d8d5970..2d651ba2af674 100644 --- a/ext/readline/readline.stub.php +++ b/ext/readline/readline.stub.php @@ -24,6 +24,7 @@ function readline_write_history(?string $filename = null): bool {} function readline_completion_function(callable $callback): bool {} +function readline_interactive_shell_result_function(?callable $callback): bool {} #if HAVE_RL_CALLBACK_READ_CHAR function readline_callback_handler_install(string $prompt, callable $callback): bool {} diff --git a/ext/readline/readline_arginfo.h b/ext/readline/readline_arginfo.h index 0b432d1e4ee35..a57dc05740dbe 100644 --- a/ext/readline/readline_arginfo.h +++ b/ext/readline/readline_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 226b138a99e3e32aea90cbb5c44446ac7c16db71 */ + * Stub hash: f756ad4cde88bfef32707a677ddf6624fa4ed146 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_readline, 0, 0, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, prompt, IS_STRING, 1, "null") @@ -32,6 +32,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_readline_completion_function, 0, ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_readline_interactive_shell_result_function, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 1) +ZEND_END_ARG_INFO() + #if HAVE_RL_CALLBACK_READ_CHAR ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_readline_callback_handler_install, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, prompt, IS_STRING, 0) @@ -69,6 +73,7 @@ ZEND_FUNCTION(readline_list_history); ZEND_FUNCTION(readline_read_history); ZEND_FUNCTION(readline_write_history); ZEND_FUNCTION(readline_completion_function); +ZEND_FUNCTION(readline_interactive_shell_result_function); #if HAVE_RL_CALLBACK_READ_CHAR ZEND_FUNCTION(readline_callback_handler_install); #endif @@ -97,6 +102,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(readline_read_history, arginfo_readline_read_history) ZEND_FE(readline_write_history, arginfo_readline_write_history) ZEND_FE(readline_completion_function, arginfo_readline_completion_function) + ZEND_FE(readline_interactive_shell_result_function, arginfo_readline_interactive_shell_result_function) #if HAVE_RL_CALLBACK_READ_CHAR ZEND_FE(readline_callback_handler_install, arginfo_readline_callback_handler_install) #endif diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c index 2930796ae7605..78167c081ca20 100644 --- a/ext/readline/readline_cli.c +++ b/ext/readline/readline_cli.c @@ -114,9 +114,8 @@ static void cli_readline_init_globals(zend_cli_readline_globals *rg) PHP_INI_BEGIN() STD_PHP_INI_ENTRY("cli.pager", "", PHP_INI_ALL, OnUpdateString, pager, zend_cli_readline_globals, cli_readline_globals) STD_PHP_INI_ENTRY("cli.prompt", DEFAULT_PROMPT, PHP_INI_ALL, OnUpdateString, prompt, zend_cli_readline_globals, cli_readline_globals) -PHP_INI_END() - - + STD_PHP_INI_BOOLEAN("cli.enable_interactive_shell_result_function", "1", PHP_INI_SYSTEM, OnUpdateBool, enable_interactive_shell_result_function, zend_cli_readline_globals, cli_readline_globals) +PHP_INI_END(); typedef enum { body, @@ -131,6 +130,133 @@ typedef enum { outside, } php_code_type; +/* Generate a snippet to try to convert single-statement statement lists to a statement returning an expression. + * If this fails to parse, the passed-in statements are used instead. */ +static zend_string *php_readline_create_return_expression_string(const char* str, size_t str_len) /* {{{ */ +{ + /* E.g. convert "2+2; \t" to "return (2+2);" */ + zend_string *result; + while (str_len > 0) { + char c = str[str_len - 1]; + /* Remove trailing whitespace and semicolon(s) from statements. + * TODO: Could tokenize to remove trailing comments as well. */ + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ';') { + str_len--; + } else { + break; + } + } + + result = zend_string_alloc(str_len + sizeof("return ();")-1, 0); + memcpy(ZSTR_VAL(result), "return (", sizeof("return (") - 1); + memcpy(ZSTR_VAL(result) + sizeof("return (") - 1, str, str_len); + memcpy(ZSTR_VAL(result) + sizeof("return (") - 1 + str_len, ");", sizeof(");") - 1); + ZSTR_VAL(result)[ZSTR_LEN(result)] = '\0'; + return result; +} + +static int php_readline_eval_stringl(const char *str, size_t str_len, zval *retval_ptr, const char *string_name) /* {{{ */ +{ + zend_string *pv; + zend_op_array *new_op_array; + uint32_t original_compiler_options = CG(compiler_options); + int retval; + + if (retval_ptr) { + pv = php_readline_create_return_expression_string(str, str_len); + } else { +recompile_without_block_expression: + pv = zend_string_init(str, str_len, 0); + } + + CG(compiler_options) = ZEND_COMPILE_DEFAULT_FOR_EVAL; + if (retval_ptr) { + int original_error_reporting = EG(error_reporting); + EG(error_reporting) &= ~(E_PARSE); + new_op_array = zend_compile_string(pv, string_name); + EG(error_reporting) = original_error_reporting; + if (!new_op_array) { + zend_string_release(pv); + if (!EG(exception) || EG(exception)->ce != zend_ce_parse_error) { + /* Don't retry if this is any exception other than ParseError (even CompileError). + * If we could parse but not compile `return (expr);`, we likely couldn't compile `expr;` either */ + return FAILURE; + } + retval_ptr = NULL; + /* TODO: Only retry if the error in question was a ParseError, not a compile error. */ + /* Code such as `return 123;;` with too many semicolons doesn't parse with the mechanism this uses to get the value of the last expression/statement. */ + zend_clear_exception(); + goto recompile_without_block_expression; + } + } else { + new_op_array = zend_compile_string(pv, string_name); + } + CG(compiler_options) = original_compiler_options; + + if (new_op_array) { + zval local_retval; + + EG(no_extensions)=1; + + new_op_array->scope = zend_get_executed_scope(); + + zend_try { + ZVAL_UNDEF(&local_retval); + zend_execute(new_op_array, &local_retval); + } zend_catch { + destroy_op_array(new_op_array); + efree_size(new_op_array, sizeof(zend_op_array)); + zend_bailout(); + } zend_end_try(); + + if (Z_TYPE(local_retval) != IS_UNDEF) { + if (retval_ptr) { + ZVAL_COPY_VALUE(retval_ptr, &local_retval); + } else { + zval_ptr_dtor(&local_retval); + } + } else { + if (retval_ptr) { + ZVAL_NULL(retval_ptr); + } + } + + EG(no_extensions)=0; + destroy_op_array(new_op_array); + efree_size(new_op_array, sizeof(zend_op_array)); + retval = SUCCESS; + } else { + retval = FAILURE; + } + zend_string_release(pv); + return retval; +} +/* }}} */ + +static void php_readline_try_interactive_eval(const char* code, const size_t codelen, const char* string_name) /* {{{ */ +{ + zval returned_zv; + /* The snippet that's evaluated depends on whether the result would be dumped */ + const bool should_dump = CLIR_G(enable_interactive_shell_result_function) && php_readline_should_dump_interactive_result(); + ZVAL_UNDEF(&returned_zv); + zend_try { + php_readline_eval_stringl(code, codelen, should_dump ? &returned_zv : NULL, string_name); + if (!EG(exception) && should_dump && !Z_ISUNDEF(returned_zv)) { + /* If the evaluated expression caused output, and that output did not add in a newline, + * append a newline before possibly dumping the result expression */ + if (!pager_pipe && php_last_char != '\0' && php_last_char != '\n') { + PHPWRITE("\n", 1); + } + php_readline_dump_interactive_result(code, codelen, &returned_zv); + } + } zend_end_try(); + if (should_dump) { + zend_try { + zval_ptr_dtor(&returned_zv); + } zend_end_try(); + } +} + static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ { smart_str retval = {0}; @@ -185,9 +311,7 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ code = estrndup(prompt_spec + 1, prompt_end - prompt_spec - 1); CLIR_G(prompt_str) = &retval; - zend_try { - zend_eval_stringl(code, prompt_end - prompt_spec - 1, NULL, "php prompt code"); - } zend_end_try(); + php_readline_try_interactive_eval(prompt_spec + 1, prompt_end - (prompt_spec + 1), "php prompt code"); CLIR_G(prompt_str) = NULL; efree(code); prompt_spec = prompt_end; @@ -682,9 +806,7 @@ static int readline_shell_run(void) /* {{{ */ history_lines_to_write = 0; } - zend_try { - zend_eval_stringl(code, pos, NULL, "php shell code"); - } zend_end_try(); + php_readline_try_interactive_eval(code, pos, "php shell code"); pos = 0; diff --git a/ext/readline/readline_cli.h b/ext/readline/readline_cli.h index 7afadded0708b..3fa70326e05d5 100644 --- a/ext/readline/readline_cli.h +++ b/ext/readline/readline_cli.h @@ -22,6 +22,7 @@ ZEND_BEGIN_MODULE_GLOBALS(cli_readline) char *pager; char *prompt; smart_str *prompt_str; + bool enable_interactive_shell_result_function; ZEND_END_MODULE_GLOBALS(cli_readline) #ifdef ZTS @@ -35,5 +36,7 @@ extern PHP_MSHUTDOWN_FUNCTION(cli_readline); extern PHP_MINFO_FUNCTION(cli_readline); char **php_readline_completion_cb(const char *text, int start, int end); +void php_readline_dump_interactive_result(const char* code, const size_t codelen, zval *returned_zv); +bool php_readline_should_dump_interactive_result(); ZEND_EXTERN_MODULE_GLOBALS(cli_readline) diff --git a/ext/readline/tests/bug77812-libedit.phpt b/ext/readline/tests/bug77812-libedit.phpt index a5e39f05ae7f8..3a45f7650c769 100644 --- a/ext/readline/tests/bug77812-libedit.phpt +++ b/ext/readline/tests/bug77812-libedit.phpt @@ -27,6 +27,7 @@ Interactive shell bar xx +=> 1 xxx Warning: Uncaught Error: Undefined constant "FOO" in php shell code:1 diff --git a/ext/readline/tests/bug77812-readline.phpt b/ext/readline/tests/bug77812-readline.phpt index 335797aa6667f..d268886a0fd99 100644 --- a/ext/readline/tests/bug77812-readline.phpt +++ b/ext/readline/tests/bug77812-readline.phpt @@ -33,6 +33,7 @@ php > print(<< xx <<< > FOO); xx +=> 1 php > echo << xxx <<< > FOO; diff --git a/ext/readline/tests/libedit_interactive_result.phpt b/ext/readline/tests/libedit_interactive_result.phpt new file mode 100644 index 0000000000000..a047221cd895e --- /dev/null +++ b/ext/readline/tests/libedit_interactive_result.phpt @@ -0,0 +1,50 @@ +--TEST-- +Configurable interactive results with libedit +--SKIPIF-- + +--FILE-- +true;"); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(%d) of type (process) +Interactive shell + + +bool(true) + +int(2) +test + +string(14) "php shell code" + +string(7) "MyClass" + +object(Closure)#%d (0) { +} diff --git a/ext/readline/tests/libedit_interactive_result_error.phpt b/ext/readline/tests/libedit_interactive_result_error.phpt new file mode 100644 index 0000000000000..28b7e96c3379d --- /dev/null +++ b/ext/readline/tests/libedit_interactive_result_error.phpt @@ -0,0 +1,55 @@ +--TEST-- +Configurable interactive results in libedit (error handling) +--SKIPIF-- + +--FILE-- +true;"); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(%d) of type (process) +Interactive shell + + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('readline_intera...', true) +#1 {main} + thrown in php shell code on line 2 + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('sprintf('hello,...', 'hello, world') +#1 {main} + thrown in php shell code on line 2 +0123456789 + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('fn()=>true;\n', Object(Closure)) +#1 {main} + thrown in php shell code on line 2 diff --git a/ext/readline/tests/readline_interactive_result.phpt b/ext/readline/tests/readline_interactive_result.phpt new file mode 100644 index 0000000000000..504a241546826 --- /dev/null +++ b/ext/readline/tests/readline_interactive_result.phpt @@ -0,0 +1,73 @@ +--TEST-- +Configurable interactive results in readline +--INI-- +cli.enable_interactive_shell_result_function=1 +--SKIPIF-- + +--FILE-- +true;"); +// Setting the callback to null clears the handler. +fwrite($pipes[0], "readline_interactive_shell_result_function(null);\n"); +fwrite($pipes[0], "1+1;\n"); +// This passes (string $code, $result) to any callable +fwrite($pipes[0], "readline_interactive_shell_result_function('var_dump');\n"); +fwrite($pipes[0], "2+2;\n"); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(%d) of type (process) +Interactive shell + +php > readline_interactive_shell_result_function( +php ( function(string $code, $result) { +php ( if (isset($result)) { +php ( echo "\n"; +php ( var_dump($result); +php ( }}); + +bool(true) +php > 1+1; + +int(2) +php > echo "test\n"; +test +php > __FILE__; + +string(14) "php shell code" +php > namespace\MyClass::class; + +string(7) "MyClass" +php > fn()=>true;readline_interactive_shell_result_function(null); +php > 1+1; +php > readline_interactive_shell_result_function('var_dump'); +php > 2+2; +string(5) "2+2; +" +int(4) +php > diff --git a/ext/readline/tests/readline_interactive_result_default.phpt b/ext/readline/tests/readline_interactive_result_default.phpt new file mode 100644 index 0000000000000..4df2316ba55e9 --- /dev/null +++ b/ext/readline/tests/readline_interactive_result_default.phpt @@ -0,0 +1,91 @@ +--TEST-- +Configurable interactive results in readline +--INI-- +cli.enable_interactive_shell_result_function=1 +--SKIPIF-- + +--FILE-- + fwrite($pipes[0], $line . "\n"); +$write_line('1+1;'); +$write_line('0.5 * 2;'); +$write_line('namespace\MyClass::class;'); +$write_line('fn()=>true;'); +$write_line('$x = ["foo", "bar"];'); +$write_line('asort($x);'); +$write_line('$x;'); +$write_line('json_encode($x);'); +$write_line('unset($x);'); +$write_line('function do_something() { echo "in do_something()\n"; }'); +$write_line('do_something();'); +$write_line('json_decode(\'{"key": "value"}\');'); +$write_line('throw new RuntimeException("test");'); +$write_line('printf("newline is automatically appended by shell");'); +$write_line('printf("newline not automatically appended by shell\n");'); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(5) of type (process) +Interactive shell + +php > 1+1; +=> 2 +php > 0.5 * 2; +=> 1.0 +php > namespace\MyClass::class; +=> 'MyClass' +php > fn()=>true; +=> object(Closure)#1 (0) { +} +php > $x = ["foo", "bar"]; +=> array(2) { + [0]=> + string(3) "foo" + [1]=> + string(3) "bar" +} +php > asort($x); +=> true +php > $x; +=> array(2) { + [1]=> + string(3) "bar" + [0]=> + string(3) "foo" +} +php > json_encode($x); +=> '{"1":"bar","0":"foo"}' +php > unset($x); +php > function do_something() { echo "in do_something()\n"; } +php > do_something(); +in do_something() +php > json_decode('{"key": "value"}'); +=> object(stdClass)#1 (1) { + ["key"]=> + string(5) "value" +} +php > throw new RuntimeException("test"); + +Warning: Uncaught RuntimeException: test in php shell code:1 +Stack trace: +#0 {main} + thrown in php shell code on line 1 +php > printf("newline is automatically appended by shell"); +newline is automatically appended by shell +=> 42 +php > printf("newline not automatically appended by shell\n"); +newline not automatically appended by shell +=> 44 +php > diff --git a/ext/readline/tests/readline_interactive_result_disabled.phpt b/ext/readline/tests/readline_interactive_result_disabled.phpt new file mode 100644 index 0000000000000..f966f63c3384f --- /dev/null +++ b/ext/readline/tests/readline_interactive_result_disabled.phpt @@ -0,0 +1,47 @@ +--TEST-- +Interactive results in readline disabled +--INI-- +cli.enable_interactive_shell_result_function=0 +--SKIPIF-- + +--FILE-- + fwrite($pipes[0], $line . "\n"); +// Result expressions aren't dumped when this functionality is disabled. +$write_line('1+1;'); +$write_line('printf("Test\n");'); +// Calling readline_interactive_shell_result_function is deliberately a no-op when this functionality +// is disabled. +$write_line(<<<'EOT' +readline_interactive_shell_result_function( + function(string $code, $result) { var_dump($result); }); +EOT); +$write_line('1+1;'); +$write_line('printf("Test\n");'); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(5) of type (process) +Interactive shell + +php > 1+1; +php > printf("Test\n"); +Test +php > readline_interactive_shell_result_function( +php ( function(string $code, $result) { var_dump($result); }); +php > 1+1; +php > printf("Test\n"); +Test +php > diff --git a/ext/readline/tests/readline_interactive_result_error.phpt b/ext/readline/tests/readline_interactive_result_error.phpt new file mode 100644 index 0000000000000..13c8c618211bb --- /dev/null +++ b/ext/readline/tests/readline_interactive_result_error.phpt @@ -0,0 +1,67 @@ +--TEST-- +Configurable interactive results in readline +--INI-- +cli.enable_interactive_shell_result_function=1 +--SKIPIF-- + +--FILE-- +true;"); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(%d) of type (process) +Interactive shell + +php > readline_interactive_shell_result_function( +php ( function(string $code, $result, $unexpectedExtraParam) { +php ( if (isset($result)) { +php ( echo "\n"; +php ( var_dump($result); +php ( }}); + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('readline_intera...', true) +#1 {main} + thrown in php shell code on line 2 +php > sprintf('hello, %s', 'world');; + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('sprintf('hello,...', 'hello, world') +#1 {main} + thrown in php shell code on line 2 +php > for($i = 0; $i < 10; $i++) { echo $i; }; echo "\n"; +0123456789 +php > fn()=>true; + +Fatal error: Uncaught ArgumentCountError: Too few arguments to function {closure}(), 2 passed and exactly 3 expected in php shell code:2 +Stack trace: +#0 [internal function]: {closure}('fn()=>true;\n', Object(Closure)) +#1 {main} + thrown in php shell code on line 2 +php > diff --git a/ext/readline/tests/readline_interactive_result_pretty_proposed_default.phpt b/ext/readline/tests/readline_interactive_result_pretty_proposed_default.phpt new file mode 100644 index 0000000000000..a7e68820c565c --- /dev/null +++ b/ext/readline/tests/readline_interactive_result_pretty_proposed_default.phpt @@ -0,0 +1,106 @@ +--TEST-- +Configurable interactive results in readline +--INI-- +cli.enable_interactive_shell_result_function=1 +--SKIPIF-- + +--FILE-- + fwrite($pipes[0], $line . "\n"); +$write_line(<<<'EOT' +readline_interactive_shell_result_function( + function(string $code, $result) { + if (!isset($result)) { + return; + } + if (is_scalar($result)) { + echo "=> " . var_export($result, true) . "\n"; + } else { + echo "=> "; var_dump($result); + }}); +EOT); +$write_line('1+1;'); +$write_line('0.5 * 2;'); +$write_line('namespace\MyClass::class;'); +$write_line('fn()=>true;'); +$write_line('$x = ["foo", "bar"];'); +$write_line('asort($x);'); +$write_line('$x;'); +$write_line('json_encode($x);'); +$write_line('unset($x);'); +$write_line('function do_something() { echo "in do_something()\n"; }'); +$write_line('do_something();'); +$write_line('json_decode(\'{"key": "value"}\');'); +$write_line('throw new RuntimeException("test");'); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECTF-- +resource(5) of type (process) +Interactive shell + +php > readline_interactive_shell_result_function( +php ( function(string $code, $result) { +php ( if (!isset($result)) { +php ( return; +php ( } +php ( if (is_scalar($result)) { +php ( echo "=> " . var_export($result, true) . "\n"; +php ( } else { +php ( echo "=> "; var_dump($result); +php ( }}); +=> true +php > 1+1; +=> 2 +php > 0.5 * 2; +=> 1.0 +php > namespace\MyClass::class; +=> 'MyClass' +php > fn()=>true; +=> object(Closure)#2 (0) { +} +php > $x = ["foo", "bar"]; +=> array(2) { + [0]=> + string(3) "foo" + [1]=> + string(3) "bar" +} +php > asort($x); +=> true +php > $x; +=> array(2) { + [1]=> + string(3) "bar" + [0]=> + string(3) "foo" +} +php > json_encode($x); +=> '{"1":"bar","0":"foo"}' +php > unset($x); +php > function do_something() { echo "in do_something()\n"; } +php > do_something(); +in do_something() +php > json_decode('{"key": "value"}'); +=> object(stdClass)#2 (1) { + ["key"]=> + string(5) "value" +} +php > throw new RuntimeException("test"); + +Warning: Uncaught RuntimeException: test in php shell code:1 +Stack trace: +#0 {main} + thrown in php shell code on line 1 +php >