diff --git a/Makefile.in b/Makefile.in index bf98da9..f9ebd45 100644 --- a/Makefile.in +++ b/Makefile.in @@ -51,15 +51,15 @@ LIBS = @READLINE_LIBS@ @LIBS@ $(ADDLIBS) HFILES = config.h es.h gc.h input.h prim.h print.h sigmsgs.h \ stdenv.h syntax.h term.h token.h var.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 + glom.c input.c heredoc.c list.c main.c match.c open.c opt.c \ + prim-ctl.c prim-etc.c prim-io.c prim-readline.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 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 + glom.o input.o heredoc.o list.o main.o match.o open.o opt.o \ + prim-ctl.o prim-etc.o prim-io.o prim-readline.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 OTHER = Makefile parse.y mksignal GEN = esdump y.tab.h y.output sigmsgs.c initial.c version.h @@ -134,7 +134,6 @@ glob.o : glob.c es.h config.h stdenv.h gc.h glom.o : glom.c es.h config.h stdenv.h gc.h input.o : input.c es.h config.h stdenv.h input.h token.h heredoc.o : heredoc.c es.h config.h stdenv.h gc.h input.h syntax.h token.h -history.o : history.c es.h config.h stdenv.h gc.h input.h token.h list.o : list.c es.h config.h stdenv.h gc.h main.o : main.c es.h config.h stdenv.h match.o : match.c es.h config.h stdenv.h @@ -144,6 +143,7 @@ prim.o : prim.c es.h config.h stdenv.h prim.h prim-ctl.o : prim-ctl.c es.h config.h stdenv.h prim.h prim-etc.o : prim-etc.c es.h config.h stdenv.h prim.h version.h prim-io.o : prim-io.c es.h config.h stdenv.h gc.h prim.h +prim-readline.o : prim-readline.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 diff --git a/es.h b/es.h index dd7e9c1..a291660 100644 --- a/es.h +++ b/es.h @@ -288,14 +288,13 @@ extern Boolean streq2(const char *s, const char *t1, const char *t2); /* input.c */ -extern Tree *parse(char *prompt1, char *prompt2); +extern Tree *parse(List *reader); extern Tree *parsestring(const char *str); extern Boolean isinteractive(void); extern Boolean isfromfd(void); -extern void initinput(void); extern List *runfd(int fd, const char *name, int flags); -extern List *runstring(const char *str, const char *name, int flags); +extern List *runstring(const char *str, int flags); /* eval_* flags are also understood as runflags */ #define run_interactive 4 /* -i or $0[0] = '-' */ @@ -304,25 +303,6 @@ 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 -extern Boolean resetterminal; -#endif - - -/* history.c */ -#if HAVE_READLINE -extern void inithistory(void); - -extern void sethistory(char *file); -extern void loghistory(char *cmd); -extern void setmaxhistorylength(int length); -extern void checkhistory(void); -#endif - -extern void newhistbuffer(void); -extern void addhistbuffer(char c); -extern char *dumphistbuffer(void); - /* opt.c */ diff --git a/heredoc.c b/heredoc.c index 3e1dace..2dcb667 100644 --- a/heredoc.c +++ b/heredoc.c @@ -43,7 +43,7 @@ extern Tree *snarfheredoc(Parser *p, const char *eof, Boolean quoted) { for (tree = NULL, tailp = &tree, buf = openbuffer(0);;) { int c; - print_prompt2(p); + p->input->lineno++; for (s = (unsigned char *) eof; (c = get(p)) == *s; s++) ; if (*s == '\0' && (c == '\n' || c == EOF)) { diff --git a/history.c b/history.c deleted file mode 100644 index 4d9d5a2..0000000 --- a/history.c +++ /dev/null @@ -1,163 +0,0 @@ -/* history.c -- control the history file ($Revision: 1.1.1.1 $) */ - -#include "es.h" -#include "gc.h" -#include "input.h" - - -/* - * constants - */ - -#define BUFSIZE ((size_t) 4096) /* buffer size to fill reads into */ - - -/* - * globals - */ - -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 - */ - - -extern void newhistbuffer(void) { - assert(histbuffer == NULL); - histbuffer = openbuffer(BUFSIZE); -} - -extern void addhistbuffer(char c) { - if (histbuffer == NULL) - return; - histbuffer = bufputc(histbuffer, c); -} - -extern char *dumphistbuffer(void) { - char *s; - size_t len; - assert(histbuffer != NULL); - - s = sealcountedbuffer(histbuffer); - histbuffer = NULL; - - len = strlen(s); - if (len > 0 && s[len - 1] == '\n') - 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/initial.es b/initial.es index 987919a..ed7bb9e 100644 --- a/initial.es +++ b/initial.es @@ -648,10 +648,39 @@ if {~ <=$&primitives writehistory} { # The parsed code is executed only if it is non-empty, because otherwise # result gets set to zero when it should not be. -fn-%parse = $&parse fn-%batch-loop = $&batchloop fn-%is-interactive = $&isinteractive +if {~ <=$&primitives readline} { + fn-%read-line = $&readline +} { + fn %read-line prompt { + echo -n $prompt + $&read + } +} + +fn %parse { + if %is-interactive { + let (in = (); p = $*(1)) + unwind-protect { + $&parse { + let (r = <={%read-line $p}) { + in = $in $r + p = $*(2) + result $r + } + } + } { + if {!~ $#fn-%write-history 0 && !~ $#in 0} { + %write-history <={%flatten \n $in} + } + } + } { + $&parse # fall back to built-in read with no prompt + } +} + fn %interactive-loop { let (result = <=true) { catch @ e type msg { @@ -685,6 +714,7 @@ fn %interactive-loop { } } + # These functions are potentially passed to a REPL as the %dispatch # function. (For %eval-noprint, note that an empty list prepended # to a command just causes the command to be executed.) diff --git a/input.c b/input.c index 1706ae3..9ec77fc 100644 --- a/input.c +++ b/input.c @@ -17,11 +17,6 @@ */ static Input *input = NULL; -Boolean resetterminal = FALSE; /* TODO: localize to readline code */ - -#if HAVE_READLINE -#include -#endif /* @@ -51,22 +46,66 @@ static void warn(Input *in, char *s) { * getting and ungetting characters */ -static int fill(Input *in); -static void cleanup(Input *in); +/* fill -- fill input buffer by running a command */ +static int fill(Parser *p) { + int ticket = UNREGISTERED; + List *result; + char *read; + size_t nread; + Input *in = p->input; + + assert(p->buf == p->bufend); + + if (in->fd < 0) { + in->eof = TRUE; + return EOF; + } + + ticket = defer_mvfd(TRUE, dup(in->fd), 0); + + ExceptionHandler + + if (p->reader != NULL) + result = eval(p->reader, NULL, 0); + else + result = prim("read", NULL, 0); + + CatchException (e) + + undefer(ticket); + throw(e); + + EndExceptionHandler + + undefer(ticket); + + if (result == NULL) { /* eof */ + in->eof = TRUE; + return EOF; + } + read = str("%L\n", result, " "); + if ((nread = strlen(read)) > p->buflen) { + p->bufbegin = erealloc(p->bufbegin, nread); + p->buflen = nread; + } + memcpy(p->bufbegin, read, nread); + + p->buf = p->bufbegin; + p->bufend = &p->buf[nread]; + + return *p->buf++; +} /* get -- get a character, filter out nulls */ extern int get(Parser *p) { int c; - Input *in = p->input; if (p->ungot > 0) return p->unget[--p->ungot]; - while ((c = (in->buf < in->bufend ? *in->buf++ : fill(in))) == '\0') - warn(in, "null character ignored"); - if (c != EOF) { - char buf = c; - addhistbuffer(buf); - if (p->input->runflags & run_echoinput) - ewrite(2, &buf, 1); + while ((c = (p->buf < p->bufend ? *p->buf++ : fill(p))) == '\0') + warn(p->input, "null character ignored"); + if (c != EOF && p->input->runflags & run_echoinput) { + char buf = (char)c; + ewrite(2, &buf, 1); } return c; } @@ -77,89 +116,21 @@ extern void unget(Parser *p, int c) { p->unget[p->ungot++] = c; } -#if HAVE_READLINE -/* callreadline -- readline wrapper */ -static 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) { - long nread; - assert(in->buf == in->bufend); - - if (in->fd < 0) { - in->eof = TRUE; - return EOF; - } - -#if HAVE_READLINE - if (in->runflags & run_interactive && in->fd == 0) { - char *rlinebuf = NULL; - do { - rlinebuf = callreadline(in->prompt); - } while (rlinebuf == NULL && errno == EINTR); - if (rlinebuf == NULL) - nread = 0; - else { - nread = strlen(rlinebuf) + 1; - if (in->buflen < (unsigned int)nread) { - while (in->buflen < (unsigned int)nread) - in->buflen *= 2; - in->bufbegin = erealloc(in->bufbegin, in->buflen); - } - memcpy(in->bufbegin, rlinebuf, nread - 1); - in->bufbegin[nread - 1] = '\n'; - efree(rlinebuf); - } +static void initbuf(Parser *p) { + const char *initial = p->input->str; + p->buflen = initial == NULL ? BUFSIZE : strlen(initial); + p->bufbegin = p->buf = ealloc(p->buflen); + if (initial != NULL) { + memcpy(p->buf, initial, p->buflen); + p->bufend = p->bufbegin + p->buflen; } else -#endif - do { - nread = read(in->fd, (char *) in->bufbegin, in->buflen); - SIGCHK(); - } while (nread == -1 && errno == EINTR); - - if (nread == -1) - fail("$&parse", "%s: %s", in->name == NULL ? "es" : in->name, esstrerror(errno)); - if (nread == 0) { - in->eof = TRUE; - return EOF; - } - - in->buf = in->bufbegin; - in->bufend = &in->buf[nread]; - return *in->buf++; + p->bufend = p->bufbegin; } - /* - * the input loop + * parse -- call yyparse(), but disable garbage collection and catch errors */ - -/* parse -- yyparse() wrapper */ -extern Tree *parse(char *pr1, char *pr2) { +extern Tree *parse(List *reader) { int result; Parser p; void *oldpspace; @@ -171,24 +142,21 @@ extern Tree *parse(char *pr1, char *pr2) { memzero(&p, sizeof (Parser)); p.input = input; + p.reader = reader; + RefAdd(p.reader); p.space = createpspace(); oldpspace = setpspace(p.space); inityy(&p); + initbuf(&p); p.tokenbuf = ealloc(p.bufsize); - RefAdd(pr2); - input->prompt = pr1 == NULL ? NULL : pstr("%s", pr1); - input->prompt2 = pr2 == NULL ? NULL : pstr("%s", pr2); - RefRemove(pr2); -#if !HAVE_READLINE - if (input->prompt != NULL) - eprint("%s", input->prompt); -#endif - result = yyparse(&p); + RefRemove(p.reader); assert(p.ungot == 0); + if (p.bufbegin != NULL) + efree(p.bufbegin); if (p.tokenbuf != NULL) efree(p.tokenbuf); @@ -210,6 +178,19 @@ extern Tree *parse(char *pr1, char *pr2) { RefReturn(tree); } + +/* + * the input loop + */ + +/* cleanup -- clean up after an input source */ +static void cleanup(Input *in) { + if (in->fd != -1) { + unregisterfd(&in->fd); + close(in->fd); + } +} + /* runinput -- run from an input source */ extern List *runinput(Input *in, int runflags) { volatile int flags = runflags; @@ -268,14 +249,6 @@ extern List *runinput(Input *in, int runflags) { * pushing new input sources */ -static void cleanup(Input *in) { - if (in->fd != -1) { - unregisterfd(&in->fd); - close(in->fd); - } - efree(in->bufbegin); -} - /* runfd -- run commands from a file descriptor */ extern List *runfd(int fd, const char *name, int flags) { Input in; @@ -285,9 +258,6 @@ extern List *runfd(int fd, const char *name, int flags) { in.lineno = 1; in.fd = fd; registerfd(&in.fd, TRUE); - in.buflen = BUFSIZE; - in.bufbegin = in.buf = ealloc(in.buflen); - in.bufend = in.bufbegin; in.name = (name == NULL) ? str("fd %d", fd) : name; RefAdd(in.name); @@ -298,25 +268,22 @@ extern List *runfd(int fd, const char *name, int flags) { } /* runstring -- run commands from a string */ -extern List *runstring(const char *str, const char *name, int flags) { +extern List *runstring(const char *str, int flags) { Input in; List *result; - unsigned char *buf; assert(str != NULL); memzero(&in, sizeof (Input)); in.fd = -1; in.lineno = 1; - in.name = (name == NULL) ? str : name; - in.buflen = strlen(str); - buf = ealloc(in.buflen + 1); - memcpy(buf, str, in.buflen); - in.bufbegin = in.buf = buf; - in.bufend = in.buf + in.buflen; + in.name = str; + in.str = str; RefAdd(in.name); + RefAdd(in.str); result = runinput(&in, flags); + RefRemove(in.str); RefRemove(in.name); return result; } @@ -330,7 +297,7 @@ extern Tree *parseinput(Input *in) { input = in; ExceptionHandler - result = parse(NULL, NULL); + result = parse(NULL); if (!in->eof) fail("$&parse", "more than one value in term"); CatchException (e) @@ -348,24 +315,19 @@ extern Tree *parseinput(Input *in) { extern Tree *parsestring(const char *str) { Input in; Tree *result; - unsigned char *buf; assert(str != NULL); - /* TODO: abstract out common code with runstring */ - memzero(&in, sizeof (Input)); in.fd = -1; in.lineno = 1; in.name = str; - in.buflen = strlen(str); - buf = ealloc(in.buflen + 1); - memcpy(buf, str, in.buflen); - in.bufbegin = in.buf = buf; - in.bufend = in.buf + in.buflen; + in.str = str; RefAdd(in.name); + RefAdd(in.str); result = parseinput(&in); + RefRemove(in.str); RefRemove(in.name); return result; } @@ -377,255 +339,5 @@ extern Boolean isinteractive(void) { /* isfromfd -- is the innermost input source reading from a file descriptor? */ 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 + return input == NULL ? FALSE : input->fd >= 0; } diff --git a/input.h b/input.h index 656ee1d..f96f72e 100644 --- a/input.h +++ b/input.h @@ -6,19 +6,11 @@ /* Input contains state that lasts longer than a $&parse. */ struct Input { - /* input buffer */ - size_t buflen; - unsigned char *buf, *bufend, *bufbegin; - - /* input metadata and flags */ - const char *name; + const char *name, *str; int lineno; int fd; - int runflags; Boolean eof; - - /* TODO: these belong in Parser */ - char *prompt, *prompt2; /* pspace-allocated */ + int runflags; }; typedef enum { NW, RW, KW } WordState; /* nonword, realword, keyword */ @@ -26,20 +18,24 @@ typedef enum { NW, RW, KW } WordState; /* nonword, realword, keyword */ /* Parser contains state that lasts for one call to $&parse or less. */ struct Parser { Input *input; - void *space; /* where the parse tree is kept in memory */ + List *reader; + void *space; /* pspace, for parser-related allocations */ /* these variables are all allocated in pspace */ Tree *tree; /* the final parse tree */ Here *hereq; /* pending here document queue */ const char *error; /* syntax error, if it exists */ + /* read buffer */ + size_t buflen; + unsigned char *buf, *bufend, *bufbegin; + /* token pushback buffer */ - int unget[MAXUNGET]; - int ungot; + int ungot, unget[MAXUNGET]; /* lexer state */ WordState ws; - Boolean newline, goterror, dollar; + Boolean dollar; size_t bufsize; char *tokenbuf; }; diff --git a/main.c b/main.c index f8f19ad..3c8e941 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(); @@ -215,7 +210,7 @@ int main(int argc, char **argv0) { vardef("*", NULL, argp); vardef("0", NULL, mklist(mkstr(argv[0]), NULL)); if (cmd != NULL) - status = exitstatus(runstring(cmd, NULL, runflags)); + status = exitstatus(runstring(cmd, runflags)); else status = exitstatus(runfd(0, "stdin", runflags)); diff --git a/prim-etc.c b/prim-etc.c index de6a0f7..2c91f8e 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -149,54 +149,16 @@ PRIM(var) { return list; } -static void loginput(char *input) { - char *c; - List *fn = varlookup("fn-%write-history", NULL); - if (!isinteractive() || !isfromfd() || fn == NULL) - return; - for (c = input;; c++) - switch (*c) { - case '#': case '\n': return; - case ' ': case '\t': break; - default: goto writeit; - } -writeit: - gcdisable(); - Ref(List *, list, append(fn, mklist(mkstr(input), NULL))); - gcenable(); - eval(list, NULL, 0); - RefEnd(list); -} - PRIM(parse) { List *result; - Ref(char *, prompt1, NULL); - Ref(char *, prompt2, NULL); - Ref(List *, lp, list); - if (lp != NULL) { - prompt1 = getstr(lp->term); - if ((lp = lp->next) != NULL) - prompt2 = getstr(lp->term); - } - RefEnd(lp); - newhistbuffer(); - + Ref(List *, reader, list); Ref(Tree *, tree, NULL); - ExceptionHandler - tree = parse(prompt1, prompt2); - CatchException (ex) - Ref(List *, e, ex); - loginput(dumphistbuffer()); - throw(e); - RefEnd(e); - EndExceptionHandler - - loginput(dumphistbuffer()); + tree = parse(reader); result = (tree == NULL) ? NULL : mklist(mkterm(NULL, mkclosure(gcmk(nThunk, tree), NULL)), NULL); - RefEnd3(tree, prompt2, prompt1); + RefEnd2(tree, reader); return result; } @@ -303,47 +265,6 @@ PRIM(setmaxevaldepth) { RefReturn(lp); } -#if HAVE_READLINE -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 @@ -372,11 +293,5 @@ extern Dict *initprims_etc(Dict *primdict) { X(exitonfalse); X(noreturn); X(setmaxevaldepth); -#if HAVE_READLINE - X(sethistory); - X(writehistory); - X(resetterminal); - X(setmaxhistorylength); -#endif return primdict; } diff --git a/prim-readline.c b/prim-readline.c new file mode 100644 index 0000000..4ae0bdc --- /dev/null +++ b/prim-readline.c @@ -0,0 +1,487 @@ +/* prim-readline.c -- readline integration, including history */ + +#include "es.h" +#include "prim.h" + + +/* + * globals + */ + +#if HAVE_READLINE +#include +#include + +static Boolean reloadhistory = FALSE; +static Boolean resetterminal = FALSE; +static char *history = NULL; + +#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 + */ + +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; +} + +static void inithistory(void) { + static Boolean initialized = FALSE; + if (initialized) + return; + globalroot(&history); + initialized = TRUE; +} + +extern void sethistory(char *file) { + inithistory(); + 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 + */ + +/* 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. */ +static 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)); +} + +static void initreadline(void) { + 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); +} + +static void preprl(void) { + static Boolean initialized = FALSE; + if (!initialized) { + initreadline(); + initialized = TRUE; + } + checkhistory(); + if (resetterminal) { + rl_reset_terminal(NULL); + resetterminal = FALSE; + } + if (RL_ISSTATE(RL_STATE_INITIALIZED)) + rl_reset_screen_size(); +} + +extern char *callreadline(char *prompt0) { + char *r; + Ref(char *volatile, prompt, prompt0); + preprl(); + if (prompt == NULL) + prompt = ""; /* bug fix for readline 2.0 */ + if (!sigsetjmp(slowlabel, 1)) { + slow = TRUE; + r = readline(prompt); + } else { + r = NULL; + errno = EINTR; + } + slow = FALSE; + SIGCHK(); + RefEnd(prompt); + return r; +} + + +/* + * primitive interface + */ + +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; +} + +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 = fdopen(dup(fdmap(0)), "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; +} + + +/* + * initialization + */ + +extern Dict *initprims_readline(Dict *primdict) { + X(sethistory); + X(writehistory); + X(resetterminal); + X(setmaxhistorylength); + X(readline); + + return primdict; +} +#endif 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..8a7b241 100644 --- a/prim.h +++ b/prim.h @@ -16,6 +16,7 @@ typedef struct { List *(*prim)(List *, int); } Prim; extern Dict *initprims_controlflow(Dict *primdict); /* prim-ctl.c */ extern Dict *initprims_io(Dict *primdict); /* prim-io.c */ extern Dict *initprims_etc(Dict *primdict); /* prim-etc.c */ +extern Dict *initprims_readline(Dict *primdict); /* prim-readline.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 */ diff --git a/token.c b/token.c index 0a454c3..05de5d8 100644 --- a/token.c +++ b/token.c @@ -57,23 +57,11 @@ const char dnw[] = { }; -/* print_prompt2 -- called before all continuation lines */ -extern void print_prompt2(Parser *p) { - Input *in = p->input; - in->lineno++; -#if HAVE_READLINE - in->prompt = in->prompt2; -#else - if ((p->input->runflags & run_interactive) && in->prompt2 != NULL) - eprint("%s", in->prompt2); -#endif -} - /* scanerror -- called for lexical errors */ static void scanerror(Parser *p, int c, char *s) { while (c != '\n' && c != EOF) c = get(p); - p->goterror = TRUE; + unget(p, '\n'); yyerror(p, s); } @@ -140,19 +128,9 @@ extern int yylex(YYSTYPE *y, Parser *p) { const char *meta; /* allow optimizing compilers like gcc to load these */ char *buf = p->tokenbuf; /* values into registers. */ - if (p->goterror) { - p->goterror = FALSE; - return NL; - } - /* rc variable-names may contain only alnum, '*' and '_', so use dnw if we are scanning one. */ meta = (p->dollar ? dnw : nw); p->dollar = FALSE; - if (p->newline) { - --p->input->lineno; /* slight space optimization; print_prompt2() always increments lineno */ - print_prompt2(p); - p->newline = FALSE; - } top: while ((c = get(p)) == ' ' || c == '\t') p->ws = NW; if (c == EOF) @@ -224,7 +202,7 @@ top: while ((c = get(p)) == ' ' || c == '\t') while ((c = get(p)) != '\'' || (c = get(p)) == '\'') { buf[i++] = c; if (c == '\n') - print_prompt2(p); + p->input->lineno++; if (c == EOF) { p->ws = NW; scanerror(p, c, "eof in quoted string"); @@ -239,7 +217,7 @@ top: while ((c = get(p)) == ' ' || c == '\t') return QWORD; case '\\': if ((c = get(p)) == '\n') { - print_prompt2(p); + p->input->lineno++; unget(p, ' '); goto top; /* Pretend it was just another space. */ } @@ -306,7 +284,6 @@ top: while ((c = get(p)) == ' ' || c == '\t') FALLTHROUGH; case '\n': p->input->lineno++; - p->newline = TRUE; p->ws = NW; return NL; case '(': @@ -402,7 +379,7 @@ top: while ((c = get(p)) == ' ' || c == '\t') } extern void inityy(Parser *p) { - p->newline = p->dollar = p->goterror = FALSE; + p->dollar = FALSE; p->ws = NW; p->bufsize = BUFSIZE; }