diff --git a/Makefile.in b/Makefile.in index 92385e95..4501cdb1 100644 --- a/Makefile.in +++ b/Makefile.in @@ -56,12 +56,12 @@ VPATH = $(srcdir) HFILES = config.h es.h gc.h input.h prim.h print.h sigmsgs.h \ stdenv.h syntax.h term.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 list.c main.c match.c open.c opt.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 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 list.o main.o match.o open.o opt.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 @@ -133,6 +133,7 @@ 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 heredoc.o : heredoc.c es.h config.h stdenv.h gc.h input.h syntax.h +history.o : history.c es.h config.h stdenv.h gc.h input.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 diff --git a/doc/es.1 b/doc/es.1 index 58e7f39e..53586062 100644 --- a/doc/es.1 +++ b/doc/es.1 @@ -2559,6 +2559,22 @@ For each named returns the pathname, primitive, lambda, or code fragment which would be run if the program appeared as the first word of a command. .TP +.Cr "%write-history \fIinput\fP" +Called at the end of +.Cr %parse +to write the +.I input +to the file given in +.Cr $history , +if such a file exists and can be written. +Also appends the +.I input +to the in-memory history log if +.I readline +support is compiled in. +(For more on this, see +.Cr max-history-length .) +.TP .Cr "%writeto \fIvar output cmd\fP" Runs .I cmd @@ -2690,8 +2706,8 @@ apids here read close home run count newfd seq dup openfile split -flatten parse var -fsplit pipe whatis +flatten var fsplit +pipe whatis .ft R .De .PP @@ -2707,12 +2723,19 @@ batchloop exitonfalse isinteractive .De .PP The +.Cr parse +primitive is used to implement the +.Cr %parse +hook function. +.PP +The .Cr background primitive is used to implement the .Cr %background hook function, but does not print the process ID of the background process or set .Cr $apid . +.PP The .Cr backquote primitive is used to implement the @@ -2726,7 +2749,7 @@ The following primitives implement the similarly named settor functions: .ta 1.75i 3.5i .Ds .ft \*(Cf -sethistory setnoexport setsignals +setnoexport setsignals .ft R .De .PP @@ -2745,27 +2768,37 @@ writeto %writeto .ft R .De .PP -The primitive -.Cr resetterminal -is if +The following primitives in particular are included if .I es is compiled with support for the .I readline -library. -It is used in the implementation of settor functions of the -.Cr TERM +library: +.ta 2i +.Ds +.ft \*(Cf +resetterminal sethistory +setmaxhistorylength writehistory +.ft R +.De +.PP +.Cr sethistory and -.Cr TERMCAP -variables to notify the line editing packages that the terminal -configuration has changed. -Similarly, .Cr setmaxhistorylength -is used as the settor function for the +are used as settor functions for the +.Cr history +and .Cr max-history-length -variable, which signals to the -.I readline -library how much of the history file should be read into the in-memory -history log. +variables. +.Cr resetterminal +is used in the settor functions for the +.Cr TERM +and +.Cr TERMCAP +variables. +.Cr writehistory +is used as the initial implementation of the +.Cr %write-history +function. .PP Several primitives are not directly associated with other function. They are: diff --git a/es.h b/es.h index 472e88ee..b6c391ec 100644 --- a/es.h +++ b/es.h @@ -292,11 +292,8 @@ extern Boolean streq2(const char *s, const char *t1, const char *t2); extern char *prompt, *prompt2; extern Tree *parse(char *esprompt1, char *esprompt2); extern Tree *parsestring(const char *str); -extern void sethistory(char *file); extern Boolean isinteractive(void); -#if HAVE_READLINE -extern void setmaxhistorylength(int length); -#endif +extern Boolean isfromfd(void); extern void initgetenv(void); extern void initinput(void); extern void resetparser(void); @@ -316,6 +313,21 @@ 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 checkreloadhistory(void); +#endif + +extern void newhistbuffer(void); +extern void addhistbuffer(char c); +extern char *dumphistbuffer(void); + + /* opt.c */ extern void esoptbegin(List *list, const char *caller, const char *usage, Boolean throws); diff --git a/heredoc.c b/heredoc.c index 87959ccc..60ad0b1c 100644 --- a/heredoc.c +++ b/heredoc.c @@ -44,7 +44,6 @@ extern Tree *snarfheredoc(const char *eof, Boolean quoted) { return NULL; } ignoreeof = TRUE; - disablehistory = TRUE; for (tree = NULL, tailp = &tree, buf = openbuffer(0);;) { int c; @@ -65,7 +64,6 @@ extern Tree *snarfheredoc(const char *eof, Boolean quoted) { yyerror("incomplete here document"); freebuffer(buf); ignoreeof = FALSE; - disablehistory = FALSE; return NULL; } if (c == '$' && !quoted && (c = GETC()) != '$') { @@ -81,7 +79,6 @@ extern Tree *snarfheredoc(const char *eof, Boolean quoted) { if (var == NULL) { freebuffer(buf); ignoreeof = FALSE; - disablehistory = FALSE; return NULL; } *tailp = treecons(var, NULL); @@ -96,7 +93,6 @@ extern Tree *snarfheredoc(const char *eof, Boolean quoted) { } ignoreeof = FALSE; - disablehistory = FALSE; return tree->CDR == NULL ? tree->CAR : tree; } @@ -146,5 +142,4 @@ extern Boolean queueheredoc(Tree *t) { extern void emptyherequeue(void) { hereq = NULL; - disablehistory = FALSE; } diff --git a/history.c b/history.c new file mode 100644 index 00000000..7032e96a --- /dev/null +++ b/history.c @@ -0,0 +1,134 @@ +/* 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() { + assert(histbuffer == NULL); + histbuffer = openbuffer(BUFSIZE); +} + +extern void addhistbuffer(char c) { + if (histbuffer == NULL) + return; + histbuffer = bufputc(histbuffer, c); +} + +extern char *dumphistbuffer() { + 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 +extern void setmaxhistorylength(int len) { + static int currenthistlen = -1; /* unlimited */ + if (len != currenthistlen) { + switch (len) { + case -1: + unstifle_history(); + break; + case 0: + clear_history(); + FALLTHROUGH; + default: + stifle_history(len); + } + currenthistlen = 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 void reload_history(void) { + /* Attempt to populate readline history with new history file. */ + if (history != NULL) + read_history(history); + using_history(); + + reloadhistory = FALSE; +} + +extern void sethistory(char *file) { + if (reloadhistory) + reload_history(); + reloadhistory = TRUE; + history = file; +} + +extern void checkreloadhistory(void) { + if (reloadhistory) + reload_history(); +} + +/* + * 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 452c18d3..a813803f 100644 --- a/initial.es +++ b/initial.es @@ -579,6 +579,28 @@ fn %pathsearch name { access -n $name -1e -xf $path } if {~ <=$&primitives execfailure} {fn-%exec-failure = $&execfailure} +# The %write-history hook is used in interactive contexts to write +# command input to the history file (and/or readline's in-memory +# history log). By default, $&writehistory (which is available if +# readline is compiled in) will write to readline's history log if +# $max-history-length allows, and will write to the file designated +# by $history if that variable is set and the file it points to +# exists and is writeable. + +if {~ <=$&primitives writehistory} { + fn-%write-history = $&writehistory +} { + fn %write-history input { + if {!~ $history ()} { + if {access -w $history} { + echo $input >> $history + } { + history = () + } + } + } +} + # # Read-eval-print loops @@ -599,12 +621,11 @@ if {~ <=$&primitives execfailure} {fn-%exec-failure = $&execfailure} # %batch-loop is used. # # The function %parse can be used to call the parser, which returns -# an es command. %parse takes two arguments, which are used as the -# main and secondary prompts, respectively. %parse typically returns -# one line of input, but es allows commands (notably those with braces -# or backslash continuations) to continue across multiple lines; in -# that case, the complete command and not just one physical line is -# returned. +# an es command. %parse takes two arguments, which are used as the main +# and secondary prompts, respectively. %parse typically returns one line +# of input, but es allows commands (notably those with braces or backslash +# continuations) to continue across multiple lines; in that case, the +# complete command and not just one physical line is returned. # # By convention, the REPL must pass commands to the fn %dispatch, # which has the actual responsibility for executing the command. @@ -626,9 +647,9 @@ if {~ <=$&primitives execfailure} {fn-%exec-failure = $&execfailure} # 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 +fn-%parse = $&parse +fn-%batch-loop = $&batchloop +fn-%is-interactive = $&isinteractive fn %interactive-loop { let (result = <=true) { @@ -706,14 +727,17 @@ set-PATH = @ { local (set-path = ) path = <={%fsplit : $*}; result $* } # These settor functions call primitives to set data structures used # inside of es. -set-history = $&sethistory set-signals = $&setsignals set-noexport = $&setnoexport set-max-eval-depth = $&setmaxevaldepth -# If the primitive $&resetterminal is defined (meaning that readline -# is being used), setting the variables $TERM or $TERMCAP should -# notify the line editor library. +# If the primitives $&sethistory or $&resetterminal are defined (meaning +# that readline or editline is being used), setting the variables $TERM, +# $TERMCAP, or $history should notify the line editor library. + +if {~ <=$&primitives sethistory} { + set-history = $&sethistory +} if {~ <=$&primitives resetterminal} { set-TERM = @ { $&resetterminal; result $* } diff --git a/input.c b/input.c index f1877661..1fac0f29 100644 --- a/input.c +++ b/input.c @@ -25,17 +25,11 @@ Input *input; char *prompt, *prompt2; -Boolean disablehistory = FALSE; Boolean ignoreeof = FALSE; Boolean resetterminal = FALSE; -static char *history; -static int historyfd = -1; #if HAVE_READLINE #include -#include - -Boolean reloadhistory = FALSE; #endif #if LOCAL_GETENV @@ -76,89 +70,6 @@ static void warn(char *s) { } -/* - * history - */ - -/* loghistory -- write the last command out to a file */ -static void loghistory(const char *cmd, size_t len) { - const char *s, *end; - if (history == NULL || disablehistory) - return; - if (historyfd == -1) { - historyfd = eopen(history, oAppend); - if (historyfd == -1) { - eprint("history(%s): %s\n", history, esstrerror(errno)); - vardef("history", NULL, NULL); - return; - } - } - /* skip empty lines and comments in history */ - for (s = cmd, end = s + len; s < end; s++) - switch (*s) { - case '#': case '\n': return; - case ' ': case '\t': break; - default: goto writeit; - } - writeit: - ; - /* - * Small unix hack: since read() reads only up to a newline - * from a terminal, then presumably this write() will write at - * most only one input line at a time. - */ - ewrite(historyfd, cmd, len); -} - -#if HAVE_READLINE -/* Manage maximum in-memory history length. This has speed & memory - * implications to which different users have different tolerances, so let them - * pick. */ -extern void setmaxhistorylength(int len) { - static int currenthistlen = -1; /* unlimited */ - if (len != currenthistlen) { - switch (len) { - case -1: - unstifle_history(); - break; - case 0: - clear_history(); - FALLTHROUGH; - default: - stifle_history(len); - } - currenthistlen = len; - } -} - -static void reload_history(void) { - /* Attempt to populate readline history with new history file. */ - if (history != NULL) - read_history(history); - using_history(); - - reloadhistory = FALSE; -} -#endif - -/* sethistory -- change the file for the history log */ -extern void sethistory(char *file) { -#if HAVE_READLINE - /* make sure the old file has a chance to get loaded */ - if (reloadhistory) - reload_history(); -#endif - if (historyfd != -1) { - close(historyfd); - historyfd = -1; - } -#if HAVE_READLINE - reloadhistory = TRUE; -#endif - history = file; -} - - /* * unget -- character pushback */ @@ -184,9 +95,7 @@ extern void unget(Input *in, int c) { if (in->ungot > 0) { assert(in->ungot < MAXUNGET); in->unget[in->ungot++] = c; - } else if (in->bufbegin < in->buf && in->buf[-1] == c && (input->runflags & run_echoinput) == 0) - --in->buf; - else { + } else { assert(in->rfill == NULL); in->rfill = in->fill; in->fill = ungetfill; @@ -207,8 +116,11 @@ extern void unget(Input *in, int c) { /* get -- get a character, filter out nulls */ static int get(Input *in) { int c; + Boolean uf = (in->fill == ungetfill); while ((c = (in->buf < in->bufend ? *in->buf++ : (*in->fill)(in))) == '\0') warn("null character ignored"); + if (!uf && c != EOF) + addhistbuffer((char)c); return c; } @@ -239,8 +151,7 @@ static char *callreadline(char *prompt0) { char *r; if (prompt == NULL) prompt = ""; /* bug fix for readline 2.0 */ - if (reloadhistory) - reload_history(); + checkreloadhistory(); if (resetterminal) { rl_reset_terminal(NULL); resetterminal = FALSE; @@ -335,12 +246,9 @@ static int fdfill(Input *in) { do { rlinebuf = callreadline(prompt); } while (rlinebuf == NULL && errno == EINTR); - if (rlinebuf == NULL) nread = 0; else { - if (*rlinebuf != '\0') - add_history(rlinebuf); nread = strlen(rlinebuf) + 1; if (in->buflen < (unsigned int)nread) { while (in->buflen < (unsigned int)nread) @@ -370,9 +278,6 @@ static int fdfill(Input *in) { return EOF; } - if (in->runflags & run_interactive) - loghistory((char *) in->bufbegin, nread); - in->buf = in->bufbegin; in->bufend = &in->buf[nread]; return *in->buf++; @@ -414,6 +319,7 @@ extern Tree *parse(char *pr1, char *pr2) { error = NULL; fail("$&parse", "%s", e); } + #if LISPTREES if (input->runflags & run_lisptrees) eprint("%B\n", parsetree); @@ -610,6 +516,11 @@ extern Boolean isinteractive(void) { return input == NULL ? FALSE : ((input->runflags & run_interactive) != 0); } +/* isfromfd -- is the innermost input source reading from a file descriptor? */ +extern Boolean isfromfd(void) { + return input == NULL ? FALSE : (input->fill == fdfill); +} + /* * readline integration. @@ -720,14 +631,10 @@ extern void initinput(void) { input = NULL; /* declare the global roots */ - globalroot(&history); /* history file */ globalroot(&error); /* parse errors */ globalroot(&prompt); /* main prompt */ globalroot(&prompt2); /* secondary prompt */ - /* mark the historyfd as a file descriptor to hold back from forked children */ - registerfd(&historyfd, TRUE); - /* call the parser's initialization */ initparse(); diff --git a/input.h b/input.h index 5250b1dd..b5d96675 100644 --- a/input.h +++ b/input.h @@ -27,7 +27,6 @@ struct Input { extern Input *input; extern void unget(Input *in, int c); -extern Boolean disablehistory; extern Boolean ignoreeof; extern void yyerror(char *s); diff --git a/main.c b/main.c index 9f5abafa..ce297505 100644 --- a/main.c +++ b/main.c @@ -175,6 +175,9 @@ int main(int argc, char **argv0) { roothandler = &_localhandler; /* unhygeinic */ initinput(); +#if HAVE_READLINE + inithistory(); +#endif initprims(); initvars(); diff --git a/prim-etc.c b/prim-etc.c index 95ea607d..880a2016 100644 --- a/prim-etc.c +++ b/prim-etc.c @@ -148,19 +148,27 @@ PRIM(var) { return list; } -PRIM(sethistory) { - if (list == NULL) { - sethistory(NULL); - return NULL; - } - Ref(List *, lp, list); - sethistory(getstr(lp->term)); - RefReturn(lp); +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; - Tree *tree; Ref(char *, prompt1, NULL); Ref(char *, prompt2, NULL); Ref(List *, lp, list); @@ -170,12 +178,22 @@ PRIM(parse) { prompt2 = getstr(lp->term); } RefEnd(lp); - tree = parse(prompt1, prompt2); + newhistbuffer(); + + Ref(Tree *, tree, NULL); + ExceptionHandler + tree = parse(prompt1, prompt2); + CatchException (e) + loginput(dumphistbuffer()); + throw(e); + EndExceptionHandler + + loginput(dumphistbuffer()); result = (tree == NULL) ? NULL : mklist(mkterm(NULL, mkclosure(mk(nThunk, tree), NULL)), NULL); - RefEnd2(prompt2, prompt1); + RefEnd3(tree, prompt2, prompt1); return result; } @@ -283,6 +301,23 @@ PRIM(setmaxevaldepth) { } #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; @@ -319,7 +354,6 @@ extern Dict *initprims_etc(Dict *primdict) { X(dot); X(flatten); X(whatis); - X(sethistory); X(split); X(fsplit); X(var); @@ -336,6 +370,8 @@ extern Dict *initprims_etc(Dict *primdict) { X(noreturn); X(setmaxevaldepth); #if HAVE_READLINE + X(sethistory); + X(writehistory); X(resetterminal); X(setmaxhistorylength); #endif