Save sh(1) history in ~/.sh_history.
This commit is contained in:
parent
f4152b3863
commit
3c69791078
4 changed files with 224 additions and 18 deletions
160
sh/editline.c
160
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 <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)
|
||||
|
||||
|
|
|
@ -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
14
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
|
||||
|
|
63
sh/sh.c
63
sh/sh.c
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue