Save sh(1) history in ~/.sh_history.

This commit is contained in:
Jonas 'Sortie' Termansen 2022-11-06 00:06:32 +01:00
parent f4152b3863
commit 3c69791078
4 changed files with 224 additions and 18 deletions

View File

@ -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 <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
@ -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)

View File

@ -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

14
sh/sh.1
View File

@ -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

63
sh/sh.c
View File

@ -23,6 +23,7 @@
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
@ -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, "<command-line>", false, flag_e_exit_on_error,
status = top(fp, "<command-line>", 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, "<stdin>", is_interactive, flag_e_exit_on_error,
status = top(stdin, "<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, "<stdin>", is_interactive, flag_e_exit_on_error,
status = top(stdin, "<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, "<stdin>", is_interactive, flag_e_exit_on_error,
status = top(stdin, "<stdin>", is_interactive, flag_e_exit_on_error,
&script_exited, status);
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
exit(status);