Skip to content

RFC: Custom callback to dump results in php -a #5962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions ext/readline/readline.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include "php_readline.h"
#include "readline_cli.h"
#include "readline_arginfo.h"
#include "main/php_output.h"
#include <ext/standard/php_var.h>
#include "zend_smart_str.h"

#if HAVE_LIBREADLINE || HAVE_LIBEDIT

Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ext/readline/readline.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
8 changes: 7 additions & 1 deletion ext/readline/readline_arginfo.h
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
140 changes: 131 additions & 9 deletions ext/readline/readline_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions ext/readline/readline_cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
1 change: 1 addition & 0 deletions ext/readline/tests/bug77812-libedit.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Interactive shell

bar
xx
=> 1
xxx

Warning: Uncaught Error: Undefined constant "FOO" in php shell code:1
Expand Down
1 change: 1 addition & 0 deletions ext/readline/tests/bug77812-readline.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ php > print(<<<FOO
<<< > xx
<<< > FOO);
xx
=> 1
php > echo <<<FOO
<<< > xxx
<<< > FOO;
Expand Down
50 changes: 50 additions & 0 deletions ext/readline/tests/libedit_interactive_result.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Configurable interactive results with libedit
--SKIPIF--
<?php
if (!extension_loaded('readline')) die('skip readline extension not available');
if (READLINE_LIB !== 'libedit') { die('skip libedit only'); }
if (!function_exists('proc_open')) die('skip proc_open() not available');
?>
--FILE--
<?php
$php = getenv('TEST_PHP_EXECUTABLE');
$ini = getenv('TEST_PHP_EXTRA_ARGS');
$descriptorspec = [['pipe', 'r'], STDOUT, STDERR];
$proc = proc_open("$php $ini -a", $descriptorspec, $pipes);
var_dump($proc);

// var_dump, json_encode, or any more complex dumping can be used in readline_interactive_shell_result_function.
fwrite($pipes[0], <<<'EOT'
readline_interactive_shell_result_function(
function(string $code, $result) {
if (isset($result)) {
echo "\n";
var_dump($result);
}});

EOT);
fwrite($pipes[0], "1+1;\n");
fwrite($pipes[0], 'echo "test\n";' . "\n");
fwrite($pipes[0], "__FILE__;\n");
fwrite($pipes[0], "namespace\MyClass::class;\n");
fwrite($pipes[0], "fn()=>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) {
}
Loading