xref: /PHP-7.0/ext/readline/readline_cli.c (revision 478f119a)
1 /*
2    +----------------------------------------------------------------------+
3    | PHP Version 7                                                        |
4    +----------------------------------------------------------------------+
5    | Copyright (c) 1997-2017 The PHP Group                                |
6    +----------------------------------------------------------------------+
7    | This source file is subject to version 3.01 of the PHP license,      |
8    | that is bundled with this package in the file LICENSE, and is        |
9    | available through the world-wide-web at the following url:           |
10    | http://www.php.net/license/3_01.txt                                  |
11    | If you did not receive a copy of the PHP license and are unable to   |
12    | obtain it through the world-wide-web, please send a note to          |
13    | license@php.net so we can mail you a copy immediately.               |
14    +----------------------------------------------------------------------+
15    | Author: Marcus Boerger <helly@php.net>                               |
16    |         Johannes Schlueter <johannes@php.net>                        |
17    +----------------------------------------------------------------------+
18 */
19 
20 /* $Id$ */
21 
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 
26 #include "php.h"
27 
28 #ifndef HAVE_RL_COMPLETION_MATCHES
29 #define rl_completion_matches completion_matches
30 #endif
31 
32 #include "php_globals.h"
33 #include "php_variables.h"
34 #include "zend_hash.h"
35 #include "zend_modules.h"
36 
37 #include "SAPI.h"
38 
39 #if HAVE_SETLOCALE
40 #include <locale.h>
41 #endif
42 #include "zend.h"
43 #include "zend_extensions.h"
44 #include "php_ini.h"
45 #include "php_globals.h"
46 #include "php_main.h"
47 #include "fopen_wrappers.h"
48 #include "ext/standard/php_standard.h"
49 #include "zend_smart_str.h"
50 
51 #ifdef __riscos__
52 #include <unixlib/local.h>
53 #endif
54 
55 #if HAVE_LIBEDIT
56 #include <editline/readline.h>
57 #else
58 #include <readline/readline.h>
59 #include <readline/history.h>
60 #endif
61 
62 #include "zend_compile.h"
63 #include "zend_execute.h"
64 #include "zend_highlight.h"
65 #include "zend_exceptions.h"
66 
67 #include "sapi/cli/cli.h"
68 #include "readline_cli.h"
69 
70 #ifdef COMPILE_DL_READLINE
71 #include <dlfcn.h>
72 #endif
73 
74 #ifndef RTLD_DEFAULT
75 #define RTLD_DEFAULT NULL
76 #endif
77 
78 #define DEFAULT_PROMPT "\\b \\> "
79 
80 ZEND_DECLARE_MODULE_GLOBALS(cli_readline);
81 
82 static char php_last_char = '\0';
83 static FILE *pager_pipe = NULL;
84 
readline_shell_write(const char * str,size_t str_length)85 static size_t readline_shell_write(const char *str, size_t str_length) /* {{{ */
86 {
87 	if (CLIR_G(prompt_str)) {
88 		smart_str_appendl(CLIR_G(prompt_str), str, str_length);
89 		return str_length;
90 	}
91 
92 	if (CLIR_G(pager) && *CLIR_G(pager) && !pager_pipe) {
93 		pager_pipe = VCWD_POPEN(CLIR_G(pager), "w");
94 	}
95 	if (pager_pipe) {
96 		return fwrite(str, 1, MIN(str_length, 16384), pager_pipe);
97 	}
98 
99 	return -1;
100 }
101 /* }}} */
102 
readline_shell_ub_write(const char * str,size_t str_length)103 static size_t readline_shell_ub_write(const char *str, size_t str_length) /* {{{ */
104 {
105 	/* We just store the last char here and then pass back to the
106 	   caller (sapi_cli_single_write in sapi/cli) which will actually
107 	   write due to -1 return code */
108 	php_last_char = str[str_length-1];
109 
110 	return (size_t) -1;
111 }
112 /* }}} */
113 
cli_readline_init_globals(zend_cli_readline_globals * rg)114 static void cli_readline_init_globals(zend_cli_readline_globals *rg)
115 {
116 	rg->pager = NULL;
117 	rg->prompt = NULL;
118 	rg->prompt_str = NULL;
119 }
120 
121 PHP_INI_BEGIN()
122 	STD_PHP_INI_ENTRY("cli.pager", "", PHP_INI_ALL, OnUpdateString, pager, zend_cli_readline_globals, cli_readline_globals)
123 	STD_PHP_INI_ENTRY("cli.prompt", DEFAULT_PROMPT, PHP_INI_ALL, OnUpdateString, prompt, zend_cli_readline_globals, cli_readline_globals)
124 PHP_INI_END()
125 
126 
127 
128 typedef enum {
129 	body,
130 	sstring,
131 	dstring,
132 	sstring_esc,
133 	dstring_esc,
134 	comment_line,
135 	comment_block,
136 	heredoc_start,
137 	heredoc,
138 	outside,
139 } php_code_type;
140 
cli_get_prompt(char * block,char prompt)141 static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */
142 {
143 	smart_str retval = {0};
144 	char *prompt_spec = CLIR_G(prompt) ? CLIR_G(prompt) : DEFAULT_PROMPT;
145 
146 	do {
147 		if (*prompt_spec == '\\') {
148 			switch (prompt_spec[1]) {
149 			case '\\':
150 				smart_str_appendc(&retval, '\\');
151 				prompt_spec++;
152 				break;
153 			case 'n':
154 				smart_str_appendc(&retval, '\n');
155 				prompt_spec++;
156 				break;
157 			case 't':
158 				smart_str_appendc(&retval, '\t');
159 				prompt_spec++;
160 				break;
161 			case 'e':
162 				smart_str_appendc(&retval, '\033');
163 				prompt_spec++;
164 				break;
165 
166 
167 			case 'v':
168 				smart_str_appends(&retval, PHP_VERSION);
169 				prompt_spec++;
170 				break;
171 			case 'b':
172 				smart_str_appends(&retval, block);
173 				prompt_spec++;
174 				break;
175 			case '>':
176 				smart_str_appendc(&retval, prompt);
177 				prompt_spec++;
178 				break;
179 			case '`':
180 				smart_str_appendc(&retval, '`');
181 				prompt_spec++;
182 				break;
183 			default:
184 				smart_str_appendc(&retval, '\\');
185 				break;
186 			}
187 		} else if (*prompt_spec == '`') {
188 			char *prompt_end = strstr(prompt_spec + 1, "`");
189 			char *code;
190 
191 			if (prompt_end) {
192 				code = estrndup(prompt_spec + 1, prompt_end - prompt_spec - 1);
193 
194 				CLIR_G(prompt_str) = &retval;
195 				zend_try {
196 					zend_eval_stringl(code, prompt_end - prompt_spec - 1, NULL, "php prompt code");
197 				} zend_end_try();
198 				CLIR_G(prompt_str) = NULL;
199 				efree(code);
200 				prompt_spec = prompt_end;
201 			}
202 		} else {
203 			smart_str_appendc(&retval, *prompt_spec);
204 		}
205 	} while (++prompt_spec && *prompt_spec);
206 	smart_str_0(&retval);
207 	return retval.s;
208 }
209 /* }}} */
210 
cli_is_valid_code(char * code,int len,zend_string ** prompt)211 static int cli_is_valid_code(char *code, int len, zend_string **prompt) /* {{{ */
212 {
213 	int valid_end = 1, last_valid_end;
214 	int brackets_count = 0;
215 	int brace_count = 0;
216 	int i;
217 	php_code_type code_type = body;
218 	char *heredoc_tag;
219 	int heredoc_len;
220 
221 	for (i = 0; i < len; ++i) {
222 		switch(code_type) {
223 			default:
224 				switch(code[i]) {
225 					case '{':
226 						brackets_count++;
227 						valid_end = 0;
228 						break;
229 					case '}':
230 						if (brackets_count > 0) {
231 							brackets_count--;
232 						}
233 						valid_end = brackets_count ? 0 : 1;
234 						break;
235 					case '(':
236 						brace_count++;
237 						valid_end = 0;
238 						break;
239 					case ')':
240 						if (brace_count > 0) {
241 							brace_count--;
242 						}
243 						valid_end = 0;
244 						break;
245 					case ';':
246 						valid_end = brace_count == 0 && brackets_count == 0;
247 						break;
248 					case ' ':
249 					case '\r':
250 					case '\n':
251 					case '\t':
252 						break;
253 					case '\'':
254 						code_type = sstring;
255 						break;
256 					case '"':
257 						code_type = dstring;
258 						break;
259 					case '#':
260 						code_type = comment_line;
261 						break;
262 					case '/':
263 						if (code[i+1] == '/') {
264 							i++;
265 							code_type = comment_line;
266 							break;
267 						}
268 						if (code[i+1] == '*') {
269 							last_valid_end = valid_end;
270 							valid_end = 0;
271 							code_type = comment_block;
272 							i++;
273 							break;
274 						}
275 						valid_end = 0;
276 						break;
277 					case '?':
278 						if (code[i+1] == '>') {
279 							i++;
280 							code_type = outside;
281 							break;
282 						}
283 						valid_end = 0;
284 						break;
285 					case '<':
286 						valid_end = 0;
287 						if (i + 2 < len && code[i+1] == '<' && code[i+2] == '<') {
288 							i += 2;
289 							code_type = heredoc_start;
290 							heredoc_len = 0;
291 						}
292 						break;
293 					default:
294 						valid_end = 0;
295 						break;
296 				}
297 				break;
298 			case sstring:
299 				if (code[i] == '\\') {
300 					code_type = sstring_esc;
301 				} else {
302 					if (code[i] == '\'') {
303 						code_type = body;
304 					}
305 				}
306 				break;
307 			case sstring_esc:
308 				code_type = sstring;
309 				break;
310 			case dstring:
311 				if (code[i] == '\\') {
312 					code_type = dstring_esc;
313 				} else {
314 					if (code[i] == '"') {
315 						code_type = body;
316 					}
317 				}
318 				break;
319 			case dstring_esc:
320 				code_type = dstring;
321 				break;
322 			case comment_line:
323 				if (code[i] == '\n') {
324 					code_type = body;
325 				}
326 				break;
327 			case comment_block:
328 				if (code[i-1] == '*' && code[i] == '/') {
329 					code_type = body;
330 					valid_end = last_valid_end;
331 				}
332 				break;
333 			case heredoc_start:
334 				switch(code[i]) {
335 					case ' ':
336 					case '\t':
337 					case '\'':
338 						break;
339 					case '\r':
340 					case '\n':
341 						code_type = heredoc;
342 						break;
343 					default:
344 						if (!heredoc_len) {
345 							heredoc_tag = code+i;
346 						}
347 						heredoc_len++;
348 						break;
349 				}
350 				break;
351 			case heredoc:
352 				if (code[i - (heredoc_len + 1)] == '\n' && !strncmp(code + i - heredoc_len, heredoc_tag, heredoc_len) && code[i] == '\n') {
353 					code_type = body;
354 				} else if (code[i - (heredoc_len + 2)] == '\n' && !strncmp(code + i - heredoc_len - 1, heredoc_tag, heredoc_len) && code[i-1] == ';' && code[i] == '\n') {
355 					code_type = body;
356 					valid_end = 1;
357 				}
358 				break;
359 			case outside:
360 				if ((CG(short_tags) && !strncmp(code+i-1, "<?", 2))
361 				||  (i > 3 && !strncmp(code+i-4, "<?php", 5))
362 				) {
363 					code_type = body;
364 				}
365 				break;
366 		}
367 	}
368 
369 	switch (code_type) {
370 		default:
371 			if (brace_count) {
372 				*prompt = cli_get_prompt("php", '(');
373 			} else if (brackets_count) {
374 				*prompt = cli_get_prompt("php", '{');
375 			} else {
376 				*prompt = cli_get_prompt("php", '>');
377 			}
378 			break;
379 		case sstring:
380 		case sstring_esc:
381 			*prompt = cli_get_prompt("php", '\'');
382 			break;
383 		case dstring:
384 		case dstring_esc:
385 			*prompt = cli_get_prompt("php", '"');
386 			break;
387 		case comment_block:
388 			*prompt = cli_get_prompt("/* ", '>');
389 			break;
390 		case heredoc:
391 			*prompt = cli_get_prompt("<<<", '>');
392 			break;
393 		case outside:
394 			*prompt = cli_get_prompt("   ", '>');
395 			break;
396 	}
397 
398 	if (!valid_end || brackets_count) {
399 		return 0;
400 	} else {
401 		return 1;
402 	}
403 }
404 /* }}} */
405 
cli_completion_generator_ht(const char * text,int textlen,int * state,HashTable * ht,void ** pData)406 static char *cli_completion_generator_ht(const char *text, int textlen, int *state, HashTable *ht, void **pData) /* {{{ */
407 {
408 	zend_string *name;
409 	zend_ulong number;
410 
411 	if (!(*state % 2)) {
412 		zend_hash_internal_pointer_reset(ht);
413 		(*state)++;
414 	}
415 	while(zend_hash_has_more_elements(ht) == SUCCESS) {
416 		zend_hash_get_current_key(ht, &name, &number);
417 		if (!textlen || !strncmp(ZSTR_VAL(name), text, textlen)) {
418 			if (pData) {
419 				*pData = zend_hash_get_current_data_ptr(ht);
420 			}
421 			zend_hash_move_forward(ht);
422 			return ZSTR_VAL(name);
423 		}
424 		if (zend_hash_move_forward(ht) == FAILURE) {
425 			break;
426 		}
427 	}
428 	(*state)++;
429 	return NULL;
430 } /* }}} */
431 
cli_completion_generator_var(const char * text,int textlen,int * state)432 static char *cli_completion_generator_var(const char *text, int textlen, int *state) /* {{{ */
433 {
434 	char *retval, *tmp;
435 	zend_array *symbol_table = &EG(symbol_table);
436 
437 	tmp = retval = cli_completion_generator_ht(text + 1, textlen - 1, state, symbol_table, NULL);
438 	if (retval) {
439 		retval = malloc(strlen(tmp) + 2);
440 		retval[0] = '$';
441 		strcpy(&retval[1], tmp);
442 		rl_completion_append_character = '\0';
443 	}
444 	return retval;
445 } /* }}} */
446 
cli_completion_generator_ini(const char * text,int textlen,int * state)447 static char *cli_completion_generator_ini(const char *text, int textlen, int *state) /* {{{ */
448 {
449 	char *retval, *tmp;
450 
451 	tmp = retval = cli_completion_generator_ht(text + 1, textlen - 1, state, EG(ini_directives), NULL);
452 	if (retval) {
453 		retval = malloc(strlen(tmp) + 2);
454 		retval[0] = '#';
455 		strcpy(&retval[1], tmp);
456 		rl_completion_append_character = '=';
457 	}
458 	return retval;
459 } /* }}} */
460 
cli_completion_generator_func(const char * text,int textlen,int * state,HashTable * ht)461 static char *cli_completion_generator_func(const char *text, int textlen, int *state, HashTable *ht) /* {{{ */
462 {
463 	zend_function *func;
464 	char *retval = cli_completion_generator_ht(text, textlen, state, ht, (void**)&func);
465 	if (retval) {
466 		rl_completion_append_character = '(';
467 		retval = strdup(ZSTR_VAL(func->common.function_name));
468 	}
469 
470 	return retval;
471 } /* }}} */
472 
cli_completion_generator_class(const char * text,int textlen,int * state)473 static char *cli_completion_generator_class(const char *text, int textlen, int *state) /* {{{ */
474 {
475 	zend_class_entry *ce;
476 	char *retval = cli_completion_generator_ht(text, textlen, state, EG(class_table), (void**)&ce);
477 	if (retval) {
478 		rl_completion_append_character = '\0';
479 		retval = strdup(ZSTR_VAL(ce->name));
480 	}
481 
482 	return retval;
483 } /* }}} */
484 
cli_completion_generator_define(const char * text,int textlen,int * state,HashTable * ht)485 static char *cli_completion_generator_define(const char *text, int textlen, int *state, HashTable *ht) /* {{{ */
486 {
487 	zend_class_entry **pce;
488 	char *retval = cli_completion_generator_ht(text, textlen, state, ht, (void**)&pce);
489 	if (retval) {
490 		rl_completion_append_character = '\0';
491 		retval = strdup(retval);
492 	}
493 
494 	return retval;
495 } /* }}} */
496 
497 static int cli_completion_state;
498 
cli_completion_generator(const char * text,int index)499 static char *cli_completion_generator(const char *text, int index) /* {{{ */
500 {
501 /*
502 TODO:
503 - constants
504 - maybe array keys
505 - language constructs and other things outside a hashtable (echo, try, function, class, ...)
506 - object/class members
507 
508 - future: respect scope ("php > function foo() { $[tab]" should only expand to local variables...)
509 */
510 	char *retval = NULL;
511 	int textlen = strlen(text);
512 
513 	if (!index) {
514 		cli_completion_state = 0;
515 	}
516 	if (text[0] == '$') {
517 		retval = cli_completion_generator_var(text, textlen, &cli_completion_state);
518 	} else if (text[0] == '#') {
519 		retval = cli_completion_generator_ini(text, textlen, &cli_completion_state);
520 	} else {
521 		char *lc_text, *class_name_end;
522 		int class_name_len;
523 		zend_string *class_name;
524 		zend_class_entry *ce = NULL;
525 
526 		class_name_end = strstr(text, "::");
527 		if (class_name_end) {
528 			class_name_len = class_name_end - text;
529 			class_name = zend_string_alloc(class_name_len, 0);
530 			zend_str_tolower_copy(ZSTR_VAL(class_name), text, class_name_len);
531 			if ((ce = zend_lookup_class(class_name)) == NULL) {
532 				zend_string_release(class_name);
533 				return NULL;
534 			}
535 			lc_text = zend_str_tolower_dup(class_name_end + 2, textlen - 2 - class_name_len);
536 			textlen -= (class_name_len + 2);
537 		} else {
538 			lc_text = zend_str_tolower_dup(text, textlen);
539 		}
540 
541 		switch (cli_completion_state) {
542 			case 0:
543 			case 1:
544 				retval = cli_completion_generator_func(lc_text, textlen, &cli_completion_state, ce ? &ce->function_table : EG(function_table));
545 				if (retval) {
546 					break;
547 				}
548 			case 2:
549 			case 3:
550 				retval = cli_completion_generator_define(text, textlen, &cli_completion_state, ce ? &ce->constants_table : EG(zend_constants));
551 				if (retval || ce) {
552 					break;
553 				}
554 			case 4:
555 			case 5:
556 				retval = cli_completion_generator_class(lc_text, textlen, &cli_completion_state);
557 				break;
558 			default:
559 				break;
560 		}
561 		efree(lc_text);
562 		if (class_name_end) {
563 			zend_string_release(class_name);
564 		}
565 		if (ce && retval) {
566 			int len = class_name_len + 2 + strlen(retval) + 1;
567 			char *tmp = malloc(len);
568 
569 			snprintf(tmp, len, "%s::%s", ZSTR_VAL(ce->name), retval);
570 			free(retval);
571 			retval = tmp;
572 		}
573 	}
574 
575 	return retval;
576 } /* }}} */
577 
cli_code_completion(const char * text,int start,int end)578 static char **cli_code_completion(const char *text, int start, int end) /* {{{ */
579 {
580 	return rl_completion_matches(text, cli_completion_generator);
581 }
582 /* }}} */
583 
readline_shell_run(void)584 static int readline_shell_run(void) /* {{{ */
585 {
586 	char *line;
587 	size_t size = 4096, pos = 0, len;
588 	char *code = emalloc(size);
589 	zend_string *prompt = cli_get_prompt("php", '>');
590 	char *history_file;
591 	int history_lines_to_write = 0;
592 
593 	if (PG(auto_prepend_file) && PG(auto_prepend_file)[0]) {
594 		zend_file_handle *prepend_file_p;
595 		zend_file_handle prepend_file = {{0}};
596 
597 		prepend_file.filename = PG(auto_prepend_file);
598 		prepend_file.opened_path = NULL;
599 		prepend_file.free_filename = 0;
600 		prepend_file.type = ZEND_HANDLE_FILENAME;
601 		prepend_file_p = &prepend_file;
602 
603 		zend_execute_scripts(ZEND_REQUIRE, NULL, 1, prepend_file_p);
604 	}
605 
606 	history_file = tilde_expand("~/.php_history");
607 	rl_attempted_completion_function = cli_code_completion;
608 	rl_special_prefixes = "$";
609 	read_history(history_file);
610 
611 	EG(exit_status) = 0;
612 	while ((line = readline(ZSTR_VAL(prompt))) != NULL) {
613 		if (strcmp(line, "exit") == 0 || strcmp(line, "quit") == 0) {
614 			free(line);
615 			break;
616 		}
617 
618 		if (!pos && !*line) {
619 			free(line);
620 			continue;
621 		}
622 
623 		len = strlen(line);
624 
625 		if (line[0] == '#') {
626 			char *param = strstr(&line[1], "=");
627 			if (param) {
628 				zend_string *cmd;
629 				param++;
630 				cmd = zend_string_init(&line[1], param - &line[1] - 1, 0);
631 
632 				zend_alter_ini_entry_chars_ex(cmd, param, strlen(param), PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0);
633 				zend_string_release(cmd);
634 				add_history(line);
635 
636 				zend_string_release(prompt);
637 				/* TODO: This might be wrong! */
638 				prompt = cli_get_prompt("php", '>');
639 				continue;
640 			}
641 		}
642 
643 		if (pos + len + 2 > size) {
644 			size = pos + len + 2;
645 			code = erealloc(code, size);
646 		}
647 		memcpy(&code[pos], line, len);
648 		pos += len;
649 		code[pos] = '\n';
650 		code[++pos] = '\0';
651 
652 		if (*line) {
653 			add_history(line);
654 			history_lines_to_write += 1;
655 		}
656 
657 		free(line);
658 		zend_string_release(prompt);
659 
660 		if (!cli_is_valid_code(code, pos, &prompt)) {
661 			continue;
662 		}
663 
664 		if (history_lines_to_write) {
665 #if HAVE_LIBEDIT
666 			write_history(history_file);
667 #else
668 			append_history(history_lines_to_write, history_file);
669 #endif
670 			history_lines_to_write = 0;
671 		}
672 
673 		zend_try {
674 			zend_eval_stringl(code, pos, NULL, "php shell code");
675 		} zend_end_try();
676 
677 		pos = 0;
678 
679 		if (!pager_pipe && php_last_char != '\0' && php_last_char != '\n') {
680 			php_write("\n", 1);
681 		}
682 
683 		if (EG(exception)) {
684 			zend_exception_error(EG(exception), E_WARNING);
685 		}
686 
687 		if (pager_pipe) {
688 			fclose(pager_pipe);
689 			pager_pipe = NULL;
690 		}
691 
692 		php_last_char = '\0';
693 	}
694 	free(history_file);
695 	efree(code);
696 	zend_string_release(prompt);
697 	return EG(exit_status);
698 }
699 /* }}} */
700 
701 /*
702 #ifdef COMPILE_DL_READLINE
703 This dlsym() is always used as even the CGI SAPI is linked against "CLI"-only
704 extensions. If that is being changed dlsym() should only be used when building
705 this extension sharedto offer compatibility.
706 */
707 #define GET_SHELL_CB(cb) \
708 	do { \
709 		(cb) = NULL; \
710 		cli_shell_callbacks_t *(*get_callbacks)(); \
711 		get_callbacks = dlsym(RTLD_DEFAULT, "php_cli_get_shell_callbacks"); \
712 		if (get_callbacks) { \
713 			(cb) = get_callbacks(); \
714 		} \
715 	} while(0)
716 /*#else
717 #define GET_SHELL_CB(cb) (cb) = php_cli_get_shell_callbacks()
718 #endif*/
719 
PHP_MINIT_FUNCTION(cli_readline)720 PHP_MINIT_FUNCTION(cli_readline)
721 {
722 	cli_shell_callbacks_t *cb;
723 
724 	ZEND_INIT_MODULE_GLOBALS(cli_readline, cli_readline_init_globals, NULL);
725 	REGISTER_INI_ENTRIES();
726 
727 #if HAVE_LIBEDIT
728 	REGISTER_STRING_CONSTANT("READLINE_LIB", "libedit", CONST_CS|CONST_PERSISTENT);
729 #else
730 	REGISTER_STRING_CONSTANT("READLINE_LIB", "readline", CONST_CS|CONST_PERSISTENT);
731 #endif
732 
733 	GET_SHELL_CB(cb);
734 	if (cb) {
735 		cb->cli_shell_write = readline_shell_write;
736 		cb->cli_shell_ub_write = readline_shell_ub_write;
737 		cb->cli_shell_run = readline_shell_run;
738 	}
739 
740 	return SUCCESS;
741 }
742 
PHP_MSHUTDOWN_FUNCTION(cli_readline)743 PHP_MSHUTDOWN_FUNCTION(cli_readline)
744 {
745 	cli_shell_callbacks_t *cb;
746 
747 	UNREGISTER_INI_ENTRIES();
748 
749 	GET_SHELL_CB(cb);
750 	if (cb) {
751 		cb->cli_shell_write = NULL;
752 		cb->cli_shell_ub_write = NULL;
753 		cb->cli_shell_run = NULL;
754 	}
755 
756 	return SUCCESS;
757 }
758 
PHP_MINFO_FUNCTION(cli_readline)759 PHP_MINFO_FUNCTION(cli_readline)
760 {
761 	php_info_print_table_start();
762 	php_info_print_table_header(2, "Readline Support", "enabled");
763 	php_info_print_table_row(2, "Readline library", (rl_library_version ? rl_library_version : "Unknown"));
764 	php_info_print_table_end();
765 
766 	DISPLAY_INI_ENTRIES();
767 }
768 
769 /*
770  * Local variables:
771  * tab-width: 4
772  * c-basic-offset: 4
773  * End:
774  * vim600: sw=4 ts=4 fdm=marker
775  * vim<600: sw=4 ts=4
776  */
777