diff --git a/sh/editline.c b/sh/editline.c index 31c89b97..28d951b7 100644 --- a/sh/editline.c +++ b/sh/editline.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jonas 'Sortie' Termansen. + * Copyright (c) 2011-2016, 2022 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -18,6 +18,9 @@ */ #include +#include +#include +#include #include #include #include @@ -495,6 +498,161 @@ void edit_line_type_complete(struct edit_line* edit_state) free(partial); } +static size_t get_histsize(void) +{ + const char* histfile = getenv("HISTSIZE"); + if ( histfile && isdigit(*histfile) ) + { + errno = 0; + char* end; + size_t value = strtoul(histfile, &end, 10); + // Enforce a reasonable upper limit to avoid OOM on misconfigurations, + // when users try to have unlimited history size, but the below saving + // will allocate an array of this size. + if ( 1048576 <= value ) + value = 1048576; + if ( !errno && !*end ) + return value; + } + return 500; +} + +bool edit_line_history_load(struct edit_line* edit_state, const char* path) +{ + if ( !path ) + return true; + FILE* fp = fopen(path, "r"); + if ( !fp ) + { + if ( errno == ENOENT ) + return true; + warn("%s", path); + return false; + } + char* line = NULL; + size_t line_size; + ssize_t line_length; + while ( 0 < (line_length = getline(&line, &line_size, fp)) ) + { + if ( line[line_length - 1] == '\n' ) + line[--line_length] = '\0'; + edit_line_append_history(edit_state, line); + } + edit_state->history_begun = edit_state->history_used; + if ( ferror(fp) ) + warn("read: %s", path); + free(line); + fclose(fp); + return true; +} + +bool edit_line_history_save(struct edit_line* edit_state, const char* path) +{ + size_t histsize = get_histsize(); + if ( !path || !histsize ) + return true; + // Avoid replacing the null device if used to disable the history. + if ( !strcmp(path, "/dev/null") ) + return true; + // TODO: File locking is a better alternative to replacing the actual file. + // A temporary file rename is used to atomically replace the contents + // but may lose the race with other processes. + char* tmp; + if ( asprintf(&tmp, "%s.XXXXXXXXX", path) < 0 ) + { + warn("malloc"); + return false; + } + int fd = mkstemp(tmp); + if ( fd < 0 ) + { + if ( errno == EROFS ) + return true; + warn("%s", path); + return false; + } + FILE* fpout = fdopen(fd, "w"); + if ( !fpout ) + { + warn("%s", path); + close(fd); + unlink(tmp); + free(tmp); + return false; + } + // Merge with any updated history. + bool success = true; + char** history = calloc(sizeof(char*), histsize); + if ( !history ) + warn("malloc"), success = false; + size_t first = 0; + size_t used = 0; + FILE* fpin; + if ( success && (fpin = fopen(path, "r")) ) + { + char* line = NULL; + size_t line_size; + ssize_t line_length; + while ( 0 < (line_length = getline(&line, &line_size, fpin)) ) + { + if ( line[line_length - 1] == '\n' ) + line[--line_length] = '\0'; + size_t n = (first + used) % histsize; + if ( history[n] ) + free(history[n]); + history[n] = line; + if ( used == histsize ) + first = (first + 1) % histsize; + else + used++; + line = NULL; + } + if ( ferror(fpin) ) + warn("read: %s", path), success = false; + fclose(fpin); + } + else if ( errno != ENOENT ) + warn("%s", path); + for ( size_t i = edit_state->history_begun; + success && i < edit_state->history_used; + i++ ) + { + char* line = strdup(edit_state->history[i]); + if ( !line ) + { + warn("malloc"); + success = false; + break; + } + size_t n = (first + used) % histsize; + if ( history[n] ) + free(history[n]); + history[n] = line; + if ( used == histsize ) + first = (first + 1) % histsize; + else + used++; + line = NULL; + } + for ( size_t i = 0; i < used; i++ ) + { + size_t n = (first + i) % histsize; + char* line = history[n]; + if ( success && fprintf(fpout, "%s\n", line) < 0 ) + warn("%s", tmp), success = false; + free(line); + } + int ret = fclose(fpout); + if ( success && ret == EOF ) + warn("%s", path), success = false; + if ( success && rename(tmp, path) < 0 ) + warn("rename: %s -> %s", tmp, path), success = false; + if ( !success ) + unlink(tmp); + free(tmp); + return success; +} + #define SORTIX_LFLAGS (ISORTIX_KBKEY | ISORTIX_CHARS_DISABLE | ISORTIX_32BIT | \ ISORTIX_NONBLOCK | ISORTIX_TERMMODE) diff --git a/sh/editline.h b/sh/editline.h index d299a9b8..fbea3c23 100644 --- a/sh/editline.h +++ b/sh/editline.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jonas 'Sortie' Termansen. + * Copyright (c) 2011-2016, 2022 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -38,6 +38,7 @@ struct edit_line size_t history_used; size_t history_length; size_t history_target; + size_t history_begun; void* check_input_incomplete_context; bool (*check_input_incomplete)(void*, const char*); void* trap_eof_opportunity_context; @@ -81,6 +82,8 @@ void edit_line_type_clear(struct edit_line* edit_state); void edit_line_type_delete_word_before(struct edit_line* edit_state); int edit_line_completion_sort(const void* a_ptr, const void* b_ptr); void edit_line_type_complete(struct edit_line* edit_state); +bool edit_line_history_load(struct edit_line* edit_state, const char* path); +bool edit_line_history_save(struct edit_line* edit_state, const char* path); void edit_line(struct edit_line* edit_state); #endif diff --git a/sh/sh.1 b/sh/sh.1 index 86945bd8..282efa51 100644 --- a/sh/sh.1 +++ b/sh/sh.1 @@ -61,7 +61,14 @@ argument before reading normally from the standard input .Sh ENVIRONMENT .Nm uses environment these variables: -.Bl -tag -width "SHLVL" +.Bl -tag -width "HISTFILE" +.It Ev HISTFILE +Save the shell history in this file. +The default is +.Pa ~/.sh_history . +.It Ev HISTSIZE +Maximum number of commands in the saved shell history. +The default is 500. .It Ev HOME The user's home directory .Sq ( ~ ) . @@ -94,6 +101,11 @@ The environment variable takes precedence over this file if set. .Xr dash 1 is used by default if it is installed. +.It Pa ~/.sh_history +The saved shell history. +This location is controlled by the +.Ev HISTFILE +environment variable. .El .Sh EXIT STATUS .Nm diff --git a/sh/sh.c b/sh/sh.c index fdcd573b..278333ce 100644 --- a/sh/sh.c +++ b/sh/sh.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +52,7 @@ #include "showline.h" #include "util.h" -const char* builtin_commands[] = +static const char* builtin_commands[] = { "cd", "exit", @@ -60,8 +61,9 @@ const char* builtin_commands[] = (const char*) NULL, }; -int status = 0; -bool foreground_shell; +static bool foreground_shell; +static int status = 0; +static struct edit_line edit_state; static bool is_proper_absolute_path(const char* path) { @@ -1852,7 +1854,6 @@ void read_command_interactive(struct sh_read_command* sh_read_command) { update_env(); - static struct edit_line edit_state; // static to preserve command history. edit_state.in_fd = 0; edit_state.out_fd = 1; edit_state.check_input_incomplete_context = NULL; @@ -2010,12 +2011,12 @@ void read_command_non_interactive(struct sh_read_command* sh_read_command, sh_read_command->command = command; } -int run(FILE* fp, - const char* fp_name, - bool interactive, - bool exit_on_error, - bool* script_exited, - int status) +static int run(FILE* fp, + const char* fp_name, + bool interactive, + bool exit_on_error, + bool* script_exited, + int status) { // TODO: The interactive read code should cope when the input is not a // terminal; it should print the prompt and then read normally without @@ -2064,6 +2065,38 @@ int run(FILE* fp, return status; } +static int top(FILE* fp, + const char* fp_name, + bool interactive, + bool exit_on_error, + bool* script_exited, + int status) +{ + if ( interactive ) + { + const char* home; + const char* histfile = getenv("HISTFILE"); + if ( !histfile && (home = getenv("HOME")) ) + { + char* path; + if ( asprintf(&path, "%s/.sh_history", home) < 0 || + setenv("HISTFILE", path, 1) < 0 ) + err(1, "malloc"); + free(path); + histfile = getenv("HISTFILE"); + } + + edit_line_history_load(&edit_state, histfile); + } + + int r = run(fp, fp_name, interactive, exit_on_error, script_exited, status); + + if ( interactive ) + edit_line_history_save(&edit_state, getenv("HISTFILE")); + + return r; +} + static void compact_arguments(int* argc, char*** argv) { for ( int i = 0; i < *argc; i++ ) @@ -2243,7 +2276,7 @@ int main(int argc, char* argv[]) if ( !fp ) error(2, errno, "fmemopen"); - status = run(fp, "", false, flag_e_exit_on_error, + status = top(fp, "", false, flag_e_exit_on_error, &script_exited, status); fclose(fp); @@ -2254,7 +2287,7 @@ int main(int argc, char* argv[]) if ( flag_s_stdin ) { bool is_interactive = flag_i_interactive || isatty(fileno(stdin)); - status = run(stdin, "", is_interactive, flag_e_exit_on_error, + status = top(stdin, "", is_interactive, flag_e_exit_on_error, &script_exited, status); if ( script_exited || (status != 0 && flag_e_exit_on_error) ) exit(status); @@ -2270,7 +2303,7 @@ int main(int argc, char* argv[]) } bool is_interactive = flag_i_interactive || isatty(fileno(stdin)); - status = run(stdin, "", is_interactive, flag_e_exit_on_error, + status = top(stdin, "", is_interactive, flag_e_exit_on_error, &script_exited, status); if ( script_exited || (status != 0 && flag_e_exit_on_error) ) exit(status); @@ -2288,7 +2321,7 @@ int main(int argc, char* argv[]) FILE* fp = fopen(path, "r"); if ( !fp ) error(127, errno, "%s", path); - status = run(fp, path, false, flag_e_exit_on_error, &script_exited, + status = top(fp, path, false, flag_e_exit_on_error, &script_exited, status); fclose(fp); if ( script_exited || (status != 0 && flag_e_exit_on_error) ) @@ -2297,7 +2330,7 @@ int main(int argc, char* argv[]) else { bool is_interactive = flag_i_interactive || isatty(fileno(stdin)); - status = run(stdin, "", is_interactive, flag_e_exit_on_error, + status = top(stdin, "", is_interactive, flag_e_exit_on_error, &script_exited, status); if ( script_exited || (status != 0 && flag_e_exit_on_error) ) exit(status);