From 34be37860702e1ced96f642c44b692142b690889 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 28 Mar 2026 09:58:31 -0700 Subject: [PATCH 1/5] Create a $&readline primitive --- doc/es.1 | 11 +++++++++-- es.h | 1 + input.c | 4 +++- prim-etc.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/doc/es.1 b/doc/es.1 index 9099905..28bc1bf 100644 --- a/doc/es.1 +++ b/doc/es.1 @@ -2795,11 +2795,18 @@ library: .ta 2i .Ds .ft \*(Cf -resetterminal sethistory -setmaxhistorylength writehistory +readline resetterminal +sethistory setmaxhistorylength +writehistory .ft R .De .PP +.Cr readline +reads from standard input, using the +.I readline +library if its input is a terminal, taking a single optional prompt +argument and returning a string containing the line it read or the +empty list on EOF. .Cr sethistory and .Cr setmaxhistorylength diff --git a/es.h b/es.h index dd7e9c1..f47dde3 100644 --- a/es.h +++ b/es.h @@ -306,6 +306,7 @@ extern List *runstring(const char *str, const char *name, int flags); #if HAVE_READLINE extern Boolean resetterminal; +extern char *callreadline(char *prompt); #endif diff --git a/input.c b/input.c index 1706ae3..c63e44d 100644 --- a/input.c +++ b/input.c @@ -79,7 +79,7 @@ extern void unget(Parser *p, int c) { #if HAVE_READLINE /* callreadline -- readline wrapper */ -static char *callreadline(char *prompt0) { +extern char *callreadline(char *prompt0) { char *r; Ref(char *volatile, prompt, prompt0); if (prompt == NULL) @@ -118,6 +118,8 @@ static int fill(Input *in) { #if HAVE_READLINE if (in->runflags & run_interactive && in->fd == 0) { char *rlinebuf = NULL; + rl_instream = stdin; + rl_outstream = stdout; do { rlinebuf = callreadline(in->prompt); } while (rlinebuf == NULL && errno == EINTR); diff --git a/prim-etc.c b/prim-etc.c index de6a0f7..cd7efb4 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -304,6 +304,49 @@ PRIM(setmaxevaldepth) { } #if HAVE_READLINE +#include + +PRIM(readline) { + char *line; + char *prompt = (list == NULL ? "" : getstr(list->term)); + int input = fdmap(0); + if (list != NULL && list->next != NULL) + fail("$&readline", "usage: %read-line [prompt]"); + + if (!isatty(input)) { + list = prim("read", NULL, 0); + if (length(list) <= 1) + return list; + return mklist(mkstr(str("%L", list, "")), NULL); + } + + rl_instream = fdopen(dup(input), "r"); + rl_outstream = fdopen(dup(fdmap(1)), "w"); + + ExceptionHandler + + do { + line = callreadline(prompt); + } while (line == NULL && errno == EINTR); + + CatchException (e) + + fclose(rl_instream); + fclose(rl_outstream); + throw(e); + + EndExceptionHandler + + fclose(rl_instream); + fclose(rl_outstream); + + if (line == NULL) + return NULL; + list = mklist(mkstr(str("%s", line)), NULL); + efree(line); + return list; +} + PRIM(sethistory) { if (list == NULL) { sethistory(NULL); @@ -373,6 +416,7 @@ extern Dict *initprims_etc(Dict *primdict) { X(noreturn); X(setmaxevaldepth); #if HAVE_READLINE + X(readline); X(sethistory); X(writehistory); X(resetterminal); From 5a973024e7dd31ec73f4b3172b9c5e05d3b2657f Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 28 Mar 2026 10:32:55 -0700 Subject: [PATCH 2/5] Use stderr as rl_outstream in $&readline --- prim-etc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prim-etc.c b/prim-etc.c index cd7efb4..b3cae4c 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -321,7 +321,7 @@ PRIM(readline) { } rl_instream = fdopen(dup(input), "r"); - rl_outstream = fdopen(dup(fdmap(1)), "w"); + rl_outstream = fdopen(dup(fdmap(2)), "w"); ExceptionHandler From e61a7fe1d173472612d65cc624da8a9f92a0159d Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sun, 29 Mar 2026 11:51:26 -0700 Subject: [PATCH 3/5] Add test cases including one for $&readline --- test/tests/read.es | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/tests/read.es b/test/tests/read.es index 32edee5..0080258 100644 --- a/test/tests/read.es +++ b/test/tests/read.es @@ -6,21 +6,51 @@ test 'null reading' { echo first line > $tmp ./testrun 0 >> $tmp + let (first = (); second = ()) { + { + first = <=$&read + second = <=$&read + } < $tmp + assert {~ $first 'first line'} '$&read reads valid line' + assert {~ $second(1) 're'} '$&read reads line with zero (1)' + assert {~ $second(2) 'sult 6'} '$&read reads line with zero (2)' + } + let (first = (); second = ()) { { first = <=%read second = <=%read } < $tmp - assert {~ $first 'first line'} 'read reads valid line' - assert {~ $second 'result 6'} 'read reads line with zero' + assert {~ $first 'first line'} '%read reads valid line' + assert {~ $second 'result 6'} '%read reads line with zero' + } + + let ((first second) = `` \n { + let (fl = ()) + cat $tmp | {echo <=$&read; echo <=$&read} + }) { + assert {~ $first 'first line'} 'pipe $&read reads valid line' + assert {~ $second 're sult 6'} 'pipe $&read reads line with zero' } let ((first second) = `` \n { let (fl = ()) - cat $tmp | {echo <=%read^\n^<=%read} + cat $tmp | {echo <=%read; echo <=%read} }) { - assert {~ $first 'first line'} 'pipe read reads valid line' - assert {~ $second 'result 6'} 'pipe read reads line with zero' + assert {~ $first 'first line'} 'pipe %read reads valid line' + assert {~ $second 'result 6'} 'pipe %read reads line with zero' + } + + # TODO: $&readline should not be tested in this file + if {~ <=$&primitives readline} { + let (first = (); second = ()) { + { + first = <=$&readline + second = <=$&readline + } < $tmp + assert {~ $first 'first line'} 'read reads valid line' + assert {~ $second 'result 6'} 'read reads line with zero' + } } } { rm -f $tmp From 0a92459a1cd2c2d39d56a012e90ddb47b548270d Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sun, 29 Mar 2026 12:47:27 -0700 Subject: [PATCH 4/5] Remove $&read fallback in $&readline Also do a better job at handling errors setting up the fds for readline. --- doc/es.1 | 5 ++--- prim-etc.c | 29 +++++++++++++++++++---------- test/tests/read.es | 18 +++++++++++++++--- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/doc/es.1 b/doc/es.1 index e967076..8d1f37c 100644 --- a/doc/es.1 +++ b/doc/es.1 @@ -2809,9 +2809,8 @@ writehistory .Cr readline reads from standard input, using the .I readline -library if its input is a terminal, taking a single optional prompt -argument and returning a string containing the line it read or the -empty list on EOF. +library, taking a single optional prompt argument and returning a string +containing the line it read or the empty list on EOF. .Cr sethistory and .Cr setmaxhistorylength diff --git a/prim-etc.c b/prim-etc.c index b3cae4c..c6b1a39 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -306,22 +306,31 @@ PRIM(setmaxevaldepth) { #if HAVE_READLINE #include +static FILE *fdmapopen(int fd, const char *mode) { + FILE *f; + if ((fd = dup(fdmap(fd))) == -1) + fail("$&readline", "dup: %s", esstrerror(errno)); + if ((f = fdopen(fd, mode)) == NULL) { + int err = errno; + close(fd); + fail("$&readline", "fdopen: %s", esstrerror(err)); + } + return f; +} + PRIM(readline) { char *line; char *prompt = (list == NULL ? "" : getstr(list->term)); - int input = fdmap(0); if (list != NULL && list->next != NULL) fail("$&readline", "usage: %read-line [prompt]"); - if (!isatty(input)) { - list = prim("read", NULL, 0); - if (length(list) <= 1) - return list; - return mklist(mkstr(str("%L", list, "")), NULL); - } - - rl_instream = fdopen(dup(input), "r"); - rl_outstream = fdopen(dup(fdmap(2)), "w"); + rl_instream = fdmapopen(0, "r"); + ExceptionHandler + rl_outstream = fdmapopen(2, "w"); + CatchException (e) + fclose(rl_instream); + throw(e); + EndExceptionHandler ExceptionHandler diff --git a/test/tests/read.es b/test/tests/read.es index 0080258..605b03a 100644 --- a/test/tests/read.es +++ b/test/tests/read.es @@ -1,4 +1,4 @@ -#!/usr/local/bin/es +# read.es -- test that reading handles edge cases test 'null reading' { let (tmp = `{mktemp test-nul.XXXXXX}) @@ -41,13 +41,13 @@ test 'null reading' { assert {~ $second 'result 6'} 'pipe %read reads line with zero' } - # TODO: $&readline should not be tested in this file + if {~ <=$&primitives readline} { let (first = (); second = ()) { { first = <=$&readline second = <=$&readline - } < $tmp + } < $tmp >[2] /dev/null # hush the echoing assert {~ $first 'first line'} 'read reads valid line' assert {~ $second 'result 6'} 'read reads line with zero' } @@ -56,3 +56,15 @@ test 'null reading' { rm -f $tmp } } + +test 'fd error handling' { + assert {catch @ {true} {$&read <<< '' <[0=]; false}} + assert {catch @ {false} {$&read <<< '' >[1=]; true}} + assert {catch @ {false} {$&read <<< '' >[2=]; true}} + + if {~ <=$&primitives readline} { + assert {catch @ {true} {$&readline <<< '' <[0=]; false}} + assert {catch @ {false} {$&readline <<< '' >[1=]; true}} + assert {catch @ {true} {$&readline <<< '' >[2=]; false}} + } +} From f8f9c16700a92d017b1c33308e5dada83853bf87 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sun, 29 Mar 2026 14:32:55 -0700 Subject: [PATCH 5/5] Begin to consolidate readline logic --- Makefile.in | 9 +- es.h | 9 +- history.c | 113 ------------ input.c | 277 ------------------------------ main.c | 5 - prim-etc.c | 101 ----------- prim.c | 3 + prim.h | 3 + readline.c | 482 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 498 insertions(+), 504 deletions(-) create mode 100644 readline.c diff --git a/Makefile.in b/Makefile.in index bf98da9..ae2c253 100644 --- a/Makefile.in +++ b/Makefile.in @@ -53,13 +53,13 @@ HFILES = config.h es.h gc.h input.h prim.h print.h sigmsgs.h \ CFILES = access.c closure.c conv.c dict.c eval.c except.c fd.c gc.c glob.c \ glom.c input.c heredoc.c history.c list.c main.c match.c open.c opt.c \ prim-ctl.c prim-etc.c prim-io.c prim-sys.c prim.c print.c proc.c \ - sigmsgs.c signal.c split.c status.c str.c syntax.c term.c token.c \ - tree.c util.c var.c vec.c version.c y.tab.c dump.c + readline.c sigmsgs.c signal.c split.c status.c str.c syntax.c term.c \ + token.c tree.c util.c var.c vec.c version.c y.tab.c dump.c OFILES = access.o closure.o conv.o dict.o eval.o except.o fd.o gc.o glob.o \ glom.o input.o heredoc.o history.o list.o main.o match.o open.o opt.o \ prim-ctl.o prim-etc.o prim-io.o prim-sys.o prim.o print.o proc.o \ - sigmsgs.o signal.o split.o status.o str.o syntax.o term.o token.o \ - tree.o util.o var.o vec.o version.o y.tab.o + readline.o sigmsgs.o signal.o split.o status.o str.o syntax.o term.o \ + token.o tree.o util.o var.o vec.o version.o y.tab.o OTHER = Makefile parse.y mksignal GEN = esdump y.tab.h y.output sigmsgs.c initial.c version.h @@ -147,6 +147,7 @@ prim-io.o : prim-io.c es.h config.h stdenv.h gc.h prim.h prim-sys.o : prim-sys.c es.h config.h stdenv.h prim.h print.o : print.c es.h config.h stdenv.h print.h proc.o : proc.c es.h config.h stdenv.h prim.h +readline.o : readline.c es.h config.h stdenv.h prim.h signal.o : signal.c es.h config.h stdenv.h sigmsgs.h split.o : split.c es.h config.h stdenv.h gc.h status.o : status.c es.h config.h stdenv.h term.h diff --git a/es.h b/es.h index f47dde3..08f58b4 100644 --- a/es.h +++ b/es.h @@ -304,14 +304,13 @@ extern List *runstring(const char *str, const char *name, int flags); #define run_printcmds 32 /* -x */ #define run_lisptrees 64 /* -L and defined(LISPTREES) */ + #if HAVE_READLINE +/* readline.c */ + extern Boolean resetterminal; extern char *callreadline(char *prompt); -#endif - -/* history.c */ -#if HAVE_READLINE extern void inithistory(void); extern void sethistory(char *file); @@ -320,6 +319,8 @@ extern void setmaxhistorylength(int length); extern void checkhistory(void); #endif + +/* history.c */ extern void newhistbuffer(void); extern void addhistbuffer(char c); extern char *dumphistbuffer(void); diff --git a/history.c b/history.c index 4d9d5a2..b4f4cec 100644 --- a/history.c +++ b/history.c @@ -18,21 +18,6 @@ static Buffer *histbuffer = NULL; -#if HAVE_READLINE -#include - -Boolean reloadhistory = FALSE; -static char *history; - -#if 0 -/* These split history file entries by timestamp, which allows readline to pick up - * multi-line commands correctly across process boundaries. Disabled by default, - * because it leaves the history file itself kind of ugly. */ -static int history_write_timestamps = 1; -static char history_comment_char = '#'; -#endif -#endif - /* * histbuffer -- build the history line during input and dump it as a gc-string @@ -63,101 +48,3 @@ extern char *dumphistbuffer(void) { s[len - 1] = '\0'; return s; } - - -/* - * history file - */ - -#if HAVE_READLINE -static int sethistorylength = -1; /* unlimited */ - -extern void setmaxhistorylength(int len) { - sethistorylength = len; -} - -extern void loghistory(char *cmd) { - int err; - if (cmd == NULL) - return; - add_history(cmd); - if (history == NULL) - return; - - if ((err = append_history(1, history))) { - eprint("history(%s): %s\n", history, esstrerror(err)); - vardef("history", NULL, NULL); - } -} - -static int count_history(void) { - int i, n, count = 0, fd = eopen(history, oOpen); - char buf[4096]; - if (fd < 0) - return -1; - while ((n = read(fd, &buf, 4096)) != 0) { - if (n < 0) { - if (errno == EINTR) { - SIGCHK(); - continue; - } else { - close(fd); - return -1; - } - } - for (i = 0; i < n; i++) - if (buf[i] == '\n') - count++; - } - close(fd); - return count; -} - -static void reload_history(void) { - /* Attempt to populate readline history with new history file. */ - if (history != NULL) { - int n = count_history() - sethistorylength; - if (sethistorylength < 0 || n < 0) n = 0; - read_history_range(history, n, -1); - } - using_history(); - - reloadhistory = FALSE; -} - -extern void sethistory(char *file) { - if (reloadhistory) - reload_history(); - reloadhistory = TRUE; - history = file; -} - -extern void checkhistory(void) { - static int effectivelength = -1; - if (reloadhistory) - reload_history(); - if (sethistorylength != effectivelength) { - switch (sethistorylength) { - case -1: - unstifle_history(); - break; - case 0: - clear_history(); - FALLTHROUGH; - default: - stifle_history(sethistorylength); - } - effectivelength = sethistorylength; - } -} - -/* - * initialization - */ - -/* inithistory -- called at dawn of time from main() */ -extern void inithistory(void) { - /* declare the global roots */ - globalroot(&history); /* history file */ -} -#endif diff --git a/input.c b/input.c index c63e44d..9d08753 100644 --- a/input.c +++ b/input.c @@ -77,33 +77,6 @@ extern void unget(Parser *p, int c) { p->unget[p->ungot++] = c; } -#if HAVE_READLINE -/* callreadline -- readline wrapper */ -extern char *callreadline(char *prompt0) { - char *r; - Ref(char *volatile, prompt, prompt0); - if (prompt == NULL) - prompt = ""; /* bug fix for readline 2.0 */ - checkhistory(); - if (resetterminal) { - rl_reset_terminal(NULL); - resetterminal = FALSE; - } - if (RL_ISSTATE(RL_STATE_INITIALIZED)) - rl_reset_screen_size(); - if (!sigsetjmp(slowlabel, 1)) { - slow = TRUE; - r = readline(prompt); - } else { - r = NULL; - errno = EINTR; - } - slow = FALSE; - SIGCHK(); - RefEnd(prompt); - return r; -} -#endif static int fill(Input *in) { @@ -381,253 +354,3 @@ extern Boolean isinteractive(void) { extern Boolean isfromfd(void) { return input == NULL ? FALSE : (input->fd >= 0); } - - -/* - * readline integration. - */ -#if HAVE_READLINE -/* quote -- teach readline how to quote a word during completion. - * prefix is prepended _before_ the quotes, such as: $'foo bar' */ -static char *quote(char *text, Boolean open, char *prefix, char *qp) { - char *quoted; - if (*qp != '\0' || strpbrk(text, rl_filename_quote_characters)) { - quoted = mprint("%s%#S", prefix, text); - if (open) - quoted[strlen(quoted)-1] = '\0'; - } else { - quoted = mprint("%s%s", prefix, text); - } - efree(text); - return quoted; -} - -/* unquote -- remove quotes from text and point *qp at the relevant quote char */ -static char *unquote(const char *text, char **qp) { - char *p, *r; - Boolean quoted = FALSE; - - p = r = ealloc(strlen(text) + 1); - while ((*p = *text++)) { - if (*p == '\'') { - if (quoted && *text == '\'') { - p++; - text++; - } else { - quoted = !quoted; - if (quoted && qp != NULL) - *qp = p; - } - } else if (!quoted && *p == '\\') { - /* anything else won't be handled correctly by the completer */ - if (*text == ' ' || *text == '\'') - *p++ = *text++; - } else - p++; - } - *p = '\0'; - if (!quoted && qp != NULL) - *qp = p; - return r; -} - -/* Unquote files to allow readline to detect which are directories. */ -static int unquote_for_stat(char **name) { - char *unquoted; - if (!strpbrk(*name, rl_filename_quote_characters)) - return 0; - - unquoted = unquote(*name, NULL); - efree(*name); - *name = unquoted; - return 1; -} - -/* Find the start of the word to complete. This uses the trick where we set rl_point - * to the start of the word to indicate the start of the word. For this to work, - * rl_basic_quote_characters must be the empty string or else this function's result - * is overwritten, and doing that means we have to reimplement basically all quoting - * behavior manually. */ -static char *completion_start(void) { - int i, start = 0; - Boolean quoted = FALSE, backslash = FALSE; - for (i = 0; i < rl_point; i++) { - char c = rl_line_buffer[i]; - if (backslash) { - backslash = FALSE; - continue; - } - if (c == '\'') - quoted = !quoted; - else if (!quoted && c == '\\') - backslash = TRUE; - else if (!quoted && strchr(rl_basic_word_break_characters, c)) - start = i; /* keep possible '$' char in term */ - } - rl_point = start; - return NULL; -} - -/* Basic function to use an es List created by gen() to generate readline matches. */ -static char *list_completion(const char *text, int state, List *(*gen)(const char *)) { - static char **matches = NULL; - static int i, len; - - if (!state) { - Vector *vm = vectorize(gen(text)); - matches = vm->vector; - len = vm->count; - i = 0; - } - - if (!matches || i >= len) - return NULL; - - return mprint("%s", matches[i++]); -} - -static char *var_completion(const char *text, int state) { - return list_completion(text, state, varswithprefix); -} - -static char *prim_completion(const char *text, int state) { - return list_completion(text, state, primswithprefix); -} - -static int matchcmp(const void *a, const void *b) { - return strcoll(*(const char **)a, *(const char **)b); -} - -/* Pick out a completion to perform based on the string's prefix */ -rl_compentry_func_t *select_completion(const char *text, char **prefix) { - if (*text == '$') { - switch (text[1]) { - case '&': - *prefix = "$&"; - return prim_completion; - case '^': *prefix = "$^"; break; - case '#': *prefix = "$#"; break; - default: *prefix = "$"; - } - return var_completion; - } else if (*text == '~' && !strchr(text, '/')) { - /* ~foo => username. ~foo/bar gets completed as a filename. */ - return rl_username_completion_function; - } - return rl_filename_completion_function; -} - -static rl_compentry_func_t *completion_func = NULL; - -/* Top-level completion function. If completion_func is set, performs that completion. - * Otherwise, performs a completion based on the prefix of the text. */ -char **builtin_completion(const char *text, int UNUSED start, int UNUSED end) { - char **matches = NULL, *qp = NULL, *prefix = ""; - /* Manually unquote the text, since we told readline not to. */ - char *t = unquote(text, &qp); - rl_compentry_func_t *completion; - - if (completion_func != NULL) { - completion = completion_func; - completion_func = NULL; - } else - completion = select_completion(text, &prefix); - - matches = rl_completion_matches(t+strlen(prefix), completion); - - /* Manually sort and then re-quote the matches. */ - if (matches != NULL) { - size_t i, n; - for (n = 1; matches[n]; n++) - ; - qsort(&matches[1], n - 1, sizeof(matches[0]), matchcmp); - matches[0] = quote(matches[0], n > 1, prefix, qp); - for (i = 1; i < n; i++) - matches[i] = quote(matches[i], FALSE, prefix, qp); - } - - efree(t); - - /* Since we had to sort and quote results ourselves, we disable the automatic - * filename completion and sorting. */ - rl_attempted_completion_over = 1; - rl_sort_completion_matches = 0; - return matches; -} - -/* Unquote matches when displaying in a menu. This wouldn't be necessary, if not for - * menu-complete. */ -static void display_matches(char **matches, int num, int max) { - int i; - char **unquoted; - - if (rl_completion_query_items > 0 && num >= rl_completion_query_items) { - int c; - rl_crlf(); - fprintf(rl_outstream, "Display all %d possibilities? (y or n)", num); - fflush(rl_outstream); - c = rl_read_key(); - if (c != 'y' && c != 'Y' && c != ' ') { - rl_crlf(); - rl_forced_update_display(); - return; - } - } - - unquoted = ealloc(sizeof(char *) * (num + 2)); - for (i = 0; matches[i]; i++) - unquoted[i] = unquote(matches[i], NULL); - unquoted[i] = NULL; - - rl_display_match_list(unquoted, num, max); - rl_forced_update_display(); - - for (i = 0; unquoted[i]; i++) - efree(unquoted[i]); - efree(unquoted); -} - -static int es_complete_filename(int UNUSED count, int UNUSED key) { - completion_func = rl_filename_completion_function; - return rl_complete_internal(rl_completion_mode(es_complete_filename)); -} - -static int es_complete_variable(int UNUSED count, int UNUSED key) { - completion_func = var_completion; - return rl_complete_internal(rl_completion_mode(es_complete_variable)); -} - -static int es_complete_primitive(int UNUSED count, int UNUSED key) { - completion_func = prim_completion; - return rl_complete_internal(rl_completion_mode(es_complete_primitive)); -} -#endif /* HAVE_READLINE */ - - -/* - * initialization - */ - -/* initinput -- called at dawn of time from main() */ -extern void initinput(void) { -#if HAVE_READLINE - rl_readline_name = "es"; - - /* this word_break_characters excludes '&' due to primitive completion */ - rl_basic_word_break_characters = " \t\n`$><=;|{()}"; - rl_filename_quote_characters = " \t\n\\`'$><=;|&{()}"; - rl_basic_quote_characters = ""; - rl_special_prefixes = "$"; - - rl_completion_word_break_hook = completion_start; - rl_filename_stat_hook = unquote_for_stat; - rl_attempted_completion_function = builtin_completion; - rl_completion_display_matches_hook = display_matches; - - rl_add_funmap_entry("es-complete-filename", es_complete_filename); - rl_add_funmap_entry("es-complete-variable", es_complete_variable); - rl_add_funmap_entry("es-complete-primitive", es_complete_primitive); - rl_bind_keyseq("\033/", es_complete_filename); - rl_bind_keyseq("\033$", es_complete_variable); -#endif -} diff --git a/main.c b/main.c index f8f19ad..623ec70 100644 --- a/main.c +++ b/main.c @@ -177,11 +177,6 @@ int main(int argc, char **argv0) { ExceptionHandler roothandler = &_localhandler; /* unhygeinic */ - - initinput(); -#if HAVE_READLINE - inithistory(); -#endif initprims(); initvars(); diff --git a/prim-etc.c b/prim-etc.c index c6b1a39..60a9137 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -303,100 +303,6 @@ PRIM(setmaxevaldepth) { RefReturn(lp); } -#if HAVE_READLINE -#include - -static FILE *fdmapopen(int fd, const char *mode) { - FILE *f; - if ((fd = dup(fdmap(fd))) == -1) - fail("$&readline", "dup: %s", esstrerror(errno)); - if ((f = fdopen(fd, mode)) == NULL) { - int err = errno; - close(fd); - fail("$&readline", "fdopen: %s", esstrerror(err)); - } - return f; -} - -PRIM(readline) { - char *line; - char *prompt = (list == NULL ? "" : getstr(list->term)); - if (list != NULL && list->next != NULL) - fail("$&readline", "usage: %read-line [prompt]"); - - rl_instream = fdmapopen(0, "r"); - ExceptionHandler - rl_outstream = fdmapopen(2, "w"); - CatchException (e) - fclose(rl_instream); - throw(e); - EndExceptionHandler - - ExceptionHandler - - do { - line = callreadline(prompt); - } while (line == NULL && errno == EINTR); - - CatchException (e) - - fclose(rl_instream); - fclose(rl_outstream); - throw(e); - - EndExceptionHandler - - fclose(rl_instream); - fclose(rl_outstream); - - if (line == NULL) - return NULL; - list = mklist(mkstr(str("%s", line)), NULL); - efree(line); - return list; -} - -PRIM(sethistory) { - if (list == NULL) { - sethistory(NULL); - return NULL; - } - Ref(List *, lp, list); - sethistory(getstr(lp->term)); - RefReturn(lp); -} - -PRIM(writehistory) { - if (list == NULL || list->next != NULL) - fail("$&writehistory", "usage: $&writehistory command"); - loghistory(getstr(list->term)); - return NULL; -} - -PRIM(setmaxhistorylength) { - char *s; - int n; - if (list == NULL) { - setmaxhistorylength(-1); /* unlimited */ - return NULL; - } - if (list->next != NULL) - fail("$&setmaxhistorylength", "usage: $&setmaxhistorylength [limit]"); - Ref(List *, lp, list); - n = (int)strtol(getstr(lp->term), &s, 0); - if (n < 0 || (s != NULL && *s != '\0')) - fail("$&setmaxhistorylength", "max-history-length must be set to a positive integer"); - setmaxhistorylength(n); - RefReturn(lp); -} - -PRIM(resetterminal) { - resetterminal = TRUE; - return ltrue; -} -#endif - - /* * initialization */ @@ -424,12 +330,5 @@ extern Dict *initprims_etc(Dict *primdict) { X(exitonfalse); X(noreturn); X(setmaxevaldepth); -#if HAVE_READLINE - X(readline); - X(sethistory); - X(writehistory); - X(resetterminal); - X(setmaxhistorylength); -#endif return primdict; } diff --git a/prim.c b/prim.c index aaf24c7..1dc5299 100644 --- a/prim.c +++ b/prim.c @@ -47,6 +47,9 @@ extern void initprims(void) { prims = initprims_sys(prims); prims = initprims_proc(prims); prims = initprims_access(prims); +#if HAVE_READLINE + prims = initprims_readline(prims); +#endif #define primdict prims X(primitives); diff --git a/prim.h b/prim.h index de11f81..76416ca 100644 --- a/prim.h +++ b/prim.h @@ -19,3 +19,6 @@ extern Dict *initprims_etc(Dict *primdict); /* prim-etc.c */ extern Dict *initprims_sys(Dict *primdict); /* prim-sys.c */ extern Dict *initprims_proc(Dict *primdict); /* proc.c */ extern Dict *initprims_access(Dict *primdict); /* access.c */ +#if HAVE_READLINE +extern Dict *initprims_readline(Dict *primdict); /* readline.c */ +#endif diff --git a/readline.c b/readline.c new file mode 100644 index 0000000..ff58dac --- /dev/null +++ b/readline.c @@ -0,0 +1,482 @@ +/* readline.c -- readline primitives */ + +#include "es.h" +#include "prim.h" + +#if HAVE_READLINE + +#include +#include + +Boolean reloadhistory = FALSE; +static char *history; + +#if 0 +/* These split history file entries by timestamp, which allows readline to pick up + * multi-line commands correctly across process boundaries. Disabled by default, + * because it leaves the history file itself kind of ugly. */ +static int history_write_timestamps = 1; +static char history_comment_char = '#'; +#endif + +/* + * history functions + */ + +static int sethistorylength = -1; /* unlimited */ + +extern void setmaxhistorylength(int len) { + sethistorylength = len; +} + +extern void loghistory(char *cmd) { + int err; + if (cmd == NULL) + return; + add_history(cmd); + if (history == NULL) + return; + + if ((err = append_history(1, history))) { + eprint("history(%s): %s\n", history, esstrerror(err)); + vardef("history", NULL, NULL); + } +} + +static int count_history(void) { + int i, n, count = 0, fd = eopen(history, oOpen); + char buf[4096]; + if (fd < 0) + return -1; + while ((n = read(fd, &buf, 4096)) != 0) { + if (n < 0) { + if (errno == EINTR) { + SIGCHK(); + continue; + } else { + close(fd); + return -1; + } + } + for (i = 0; i < n; i++) + if (buf[i] == '\n') + count++; + } + close(fd); + return count; +} + +static void reload_history(void) { + /* Attempt to populate readline history with new history file. */ + if (history != NULL) { + int n = count_history() - sethistorylength; + if (sethistorylength < 0 || n < 0) n = 0; + read_history_range(history, n, -1); + } + using_history(); + + reloadhistory = FALSE; +} + +extern void sethistory(char *file) { + if (reloadhistory) + reload_history(); + reloadhistory = TRUE; + history = file; +} + +extern void checkhistory(void) { + static int effectivelength = -1; + if (reloadhistory) + reload_history(); + if (sethistorylength != effectivelength) { + switch (sethistorylength) { + case -1: + unstifle_history(); + break; + case 0: + clear_history(); + FALLTHROUGH; + default: + stifle_history(sethistorylength); + } + effectivelength = sethistorylength; + } +} + + +/* + * readline library functions + */ + +/* quote -- teach readline how to quote a word during completion. + * prefix is prepended _before_ the quotes, such as: $'foo bar' */ +static char *quote(char *text, Boolean open, char *prefix, char *qp) { + char *quoted; + if (*qp != '\0' || strpbrk(text, rl_filename_quote_characters)) { + quoted = mprint("%s%#S", prefix, text); + if (open) + quoted[strlen(quoted)-1] = '\0'; + } else { + quoted = mprint("%s%s", prefix, text); + } + efree(text); + return quoted; +} + +/* unquote -- remove quotes from text and point *qp at the relevant quote char */ +static char *unquote(const char *text, char **qp) { + char *p, *r; + Boolean quoted = FALSE; + + p = r = ealloc(strlen(text) + 1); + while ((*p = *text++)) { + if (*p == '\'') { + if (quoted && *text == '\'') { + p++; + text++; + } else { + quoted = !quoted; + if (quoted && qp != NULL) + *qp = p; + } + } else if (!quoted && *p == '\\') { + /* anything else won't be handled correctly by the completer */ + if (*text == ' ' || *text == '\'') + *p++ = *text++; + } else + p++; + } + *p = '\0'; + if (!quoted && qp != NULL) + *qp = p; + return r; +} + +/* Unquote files to allow readline to detect which are directories. */ +static int unquote_for_stat(char **name) { + char *unquoted; + if (!strpbrk(*name, rl_filename_quote_characters)) + return 0; + + unquoted = unquote(*name, NULL); + efree(*name); + *name = unquoted; + return 1; +} + +/* Find the start of the word to complete. This uses the trick where we set rl_point + * to the start of the word to indicate the start of the word. For this to work, + * rl_basic_quote_characters must be the empty string or else this function's result + * is overwritten, and doing that means we have to reimplement basically all quoting + * behavior manually. */ +static char *completion_start(void) { + int i, start = 0; + Boolean quoted = FALSE, backslash = FALSE; + for (i = 0; i < rl_point; i++) { + char c = rl_line_buffer[i]; + if (backslash) { + backslash = FALSE; + continue; + } + if (c == '\'') + quoted = !quoted; + else if (!quoted && c == '\\') + backslash = TRUE; + else if (!quoted && strchr(rl_basic_word_break_characters, c)) + start = i; /* keep possible '$' char in term */ + } + rl_point = start; + return NULL; +} + +/* Basic function to use an es List created by gen() to generate readline matches. */ +static char *list_completion(const char *text, int state, List *(*gen)(const char *)) { + static char **matches = NULL; + static int i, len; + + if (!state) { + Vector *vm = vectorize(gen(text)); + matches = vm->vector; + len = vm->count; + i = 0; + } + + if (!matches || i >= len) + return NULL; + + return mprint("%s", matches[i++]); +} + +static char *var_completion(const char *text, int state) { + return list_completion(text, state, varswithprefix); +} + +static char *prim_completion(const char *text, int state) { + return list_completion(text, state, primswithprefix); +} + +static int matchcmp(const void *a, const void *b) { + return strcoll(*(const char **)a, *(const char **)b); +} + +/* Pick out a completion to perform based on the string's prefix */ +rl_compentry_func_t *select_completion(const char *text, char **prefix) { + if (*text == '$') { + switch (text[1]) { + case '&': + *prefix = "$&"; + return prim_completion; + case '^': *prefix = "$^"; break; + case '#': *prefix = "$#"; break; + default: *prefix = "$"; + } + return var_completion; + } else if (*text == '~' && !strchr(text, '/')) { + /* ~foo => username. ~foo/bar gets completed as a filename. */ + return rl_username_completion_function; + } + return rl_filename_completion_function; +} + +static rl_compentry_func_t *completion_func = NULL; + +/* Top-level completion function. If completion_func is set, performs that completion. + * Otherwise, performs a completion based on the prefix of the text. */ +char **builtin_completion(const char *text, int UNUSED start, int UNUSED end) { + char **matches = NULL, *qp = NULL, *prefix = ""; + /* Manually unquote the text, since we told readline not to. */ + char *t = unquote(text, &qp); + rl_compentry_func_t *completion; + + if (completion_func != NULL) { + completion = completion_func; + completion_func = NULL; + } else + completion = select_completion(text, &prefix); + + matches = rl_completion_matches(t+strlen(prefix), completion); + + /* Manually sort and then re-quote the matches. */ + if (matches != NULL) { + size_t i, n; + for (n = 1; matches[n]; n++) + ; + qsort(&matches[1], n - 1, sizeof(matches[0]), matchcmp); + matches[0] = quote(matches[0], n > 1, prefix, qp); + for (i = 1; i < n; i++) + matches[i] = quote(matches[i], FALSE, prefix, qp); + } + + efree(t); + + /* Since we had to sort and quote results ourselves, we disable the automatic + * filename completion and sorting. */ + rl_attempted_completion_over = 1; + rl_sort_completion_matches = 0; + return matches; +} + +/* Unquote matches when displaying in a menu. This wouldn't be necessary, if not for + * menu-complete. */ +static void display_matches(char **matches, int num, int max) { + int i; + char **unquoted; + + if (rl_completion_query_items > 0 && num >= rl_completion_query_items) { + int c; + rl_crlf(); + fprintf(rl_outstream, "Display all %d possibilities? (y or n)", num); + fflush(rl_outstream); + c = rl_read_key(); + if (c != 'y' && c != 'Y' && c != ' ') { + rl_crlf(); + rl_forced_update_display(); + return; + } + } + + unquoted = ealloc(sizeof(char *) * (num + 2)); + for (i = 0; matches[i]; i++) + unquoted[i] = unquote(matches[i], NULL); + unquoted[i] = NULL; + + rl_display_match_list(unquoted, num, max); + rl_forced_update_display(); + + for (i = 0; unquoted[i]; i++) + efree(unquoted[i]); + efree(unquoted); +} + +static int es_complete_filename(int UNUSED count, int UNUSED key) { + completion_func = rl_filename_completion_function; + return rl_complete_internal(rl_completion_mode(es_complete_filename)); +} + +static int es_complete_variable(int UNUSED count, int UNUSED key) { + completion_func = var_completion; + return rl_complete_internal(rl_completion_mode(es_complete_variable)); +} + +static int es_complete_primitive(int UNUSED count, int UNUSED key) { + completion_func = prim_completion; + return rl_complete_internal(rl_completion_mode(es_complete_primitive)); +} + +/* callreadline -- readline wrapper */ +extern char *callreadline(char *prompt0) { + char *r; + Ref(char *volatile, prompt, prompt0); + if (prompt == NULL) + prompt = ""; /* bug fix for readline 2.0 */ + checkhistory(); + if (resetterminal) { + rl_reset_terminal(NULL); + resetterminal = FALSE; + } + if (RL_ISSTATE(RL_STATE_INITIALIZED)) + rl_reset_screen_size(); + if (!sigsetjmp(slowlabel, 1)) { + slow = TRUE; + r = readline(prompt); + } else { + r = NULL; + errno = EINTR; + } + slow = FALSE; + SIGCHK(); + RefEnd(prompt); + return r; +} + +static FILE *fdmapopen(int fd, const char *mode) { + FILE *f; + if ((fd = dup(fdmap(fd))) == -1) + fail("$&readline", "dup: %s", esstrerror(errno)); + if ((f = fdopen(fd, mode)) == NULL) { + int err = errno; + close(fd); + fail("$&readline", "fdopen: %s", esstrerror(err)); + } + return f; +} + + +/* + * primitive interface + */ + +PRIM(readline) { + char *line; + char *prompt = (list == NULL ? "" : getstr(list->term)); + if (list != NULL && list->next != NULL) + fail("$&readline", "usage: %read-line [prompt]"); + + rl_instream = fdmapopen(0, "r"); + ExceptionHandler + rl_outstream = fdmapopen(2, "w"); + CatchException (e) + fclose(rl_instream); + throw(e); + EndExceptionHandler + + ExceptionHandler + + do { + line = callreadline(prompt); + } while (line == NULL && errno == EINTR); + + CatchException (e) + + fclose(rl_instream); + fclose(rl_outstream); + throw(e); + + EndExceptionHandler + + fclose(rl_instream); + fclose(rl_outstream); + + if (line == NULL) + return NULL; + list = mklist(mkstr(str("%s", line)), NULL); + efree(line); + return list; +} + +PRIM(sethistory) { + if (list == NULL) { + sethistory(NULL); + return NULL; + } + Ref(List *, lp, list); + sethistory(getstr(lp->term)); + RefReturn(lp); +} + +PRIM(writehistory) { + if (list == NULL || list->next != NULL) + fail("$&writehistory", "usage: $&writehistory command"); + loghistory(getstr(list->term)); + return NULL; +} + +PRIM(setmaxhistorylength) { + char *s; + int n; + if (list == NULL) { + setmaxhistorylength(-1); /* unlimited */ + return NULL; + } + if (list->next != NULL) + fail("$&setmaxhistorylength", "usage: $&setmaxhistorylength [limit]"); + Ref(List *, lp, list); + n = (int)strtol(getstr(lp->term), &s, 0); + if (n < 0 || (s != NULL && *s != '\0')) + fail("$&setmaxhistorylength", "max-history-length must be set to a positive integer"); + setmaxhistorylength(n); + RefReturn(lp); +} + +PRIM(resetterminal) { + resetterminal = TRUE; + return ltrue; +} + + +/* + * initialization + */ + +extern Dict *initprims_readline(Dict *primdict) { + rl_readline_name = "es"; + + /* this word_break_characters excludes '&' due to primitive completion */ + rl_basic_word_break_characters = " \t\n`$><=;|{()}"; + rl_filename_quote_characters = " \t\n\\`'$><=;|&{()}"; + rl_basic_quote_characters = ""; + rl_special_prefixes = "$"; + + rl_completion_word_break_hook = completion_start; + rl_filename_stat_hook = unquote_for_stat; + rl_attempted_completion_function = builtin_completion; + rl_completion_display_matches_hook = display_matches; + + rl_add_funmap_entry("es-complete-filename", es_complete_filename); + rl_add_funmap_entry("es-complete-variable", es_complete_variable); + rl_add_funmap_entry("es-complete-primitive", es_complete_primitive); + rl_bind_keyseq("\033/", es_complete_filename); + rl_bind_keyseq("\033$", es_complete_variable); + + globalroot(&history); /* history file */ + + X(readline); + X(sethistory); + X(writehistory); + X(resetterminal); + X(setmaxhistorylength); + + return primdict; +} +#endif