Save sh(1) history in ~/.sh_history.
This commit is contained in:
parent
f4152b3863
commit
3c69791078
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
@ -18,6 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <err.h>
|
||||||
|
#include <errno.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
@ -495,6 +498,161 @@ void edit_line_type_complete(struct edit_line* edit_state)
|
||||||
free(partial);
|
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 | \
|
#define SORTIX_LFLAGS (ISORTIX_KBKEY | ISORTIX_CHARS_DISABLE | ISORTIX_32BIT | \
|
||||||
ISORTIX_NONBLOCK | ISORTIX_TERMMODE)
|
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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_used;
|
||||||
size_t history_length;
|
size_t history_length;
|
||||||
size_t history_target;
|
size_t history_target;
|
||||||
|
size_t history_begun;
|
||||||
void* check_input_incomplete_context;
|
void* check_input_incomplete_context;
|
||||||
bool (*check_input_incomplete)(void*, const char*);
|
bool (*check_input_incomplete)(void*, const char*);
|
||||||
void* trap_eof_opportunity_context;
|
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);
|
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);
|
int edit_line_completion_sort(const void* a_ptr, const void* b_ptr);
|
||||||
void edit_line_type_complete(struct edit_line* edit_state);
|
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);
|
void edit_line(struct edit_line* edit_state);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
14
sh/sh.1
14
sh/sh.1
|
@ -61,7 +61,14 @@ argument before reading normally from the standard input
|
||||||
.Sh ENVIRONMENT
|
.Sh ENVIRONMENT
|
||||||
.Nm
|
.Nm
|
||||||
uses environment these variables:
|
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
|
.It Ev HOME
|
||||||
The user's home directory
|
The user's home directory
|
||||||
.Sq ( ~ ) .
|
.Sq ( ~ ) .
|
||||||
|
@ -94,6 +101,11 @@ The
|
||||||
environment variable takes precedence over this file if set.
|
environment variable takes precedence over this file if set.
|
||||||
.Xr dash 1
|
.Xr dash 1
|
||||||
is used by default if it is installed.
|
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
|
.El
|
||||||
.Sh EXIT STATUS
|
.Sh EXIT STATUS
|
||||||
.Nm
|
.Nm
|
||||||
|
|
63
sh/sh.c
63
sh/sh.c
|
@ -23,6 +23,7 @@
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
#include <err.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <error.h>
|
#include <error.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
#include "showline.h"
|
#include "showline.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
const char* builtin_commands[] =
|
static const char* builtin_commands[] =
|
||||||
{
|
{
|
||||||
"cd",
|
"cd",
|
||||||
"exit",
|
"exit",
|
||||||
|
@ -60,8 +61,9 @@ const char* builtin_commands[] =
|
||||||
(const char*) NULL,
|
(const char*) NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
int status = 0;
|
static bool foreground_shell;
|
||||||
bool foreground_shell;
|
static int status = 0;
|
||||||
|
static struct edit_line edit_state;
|
||||||
|
|
||||||
static bool is_proper_absolute_path(const char* path)
|
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();
|
update_env();
|
||||||
|
|
||||||
static struct edit_line edit_state; // static to preserve command history.
|
|
||||||
edit_state.in_fd = 0;
|
edit_state.in_fd = 0;
|
||||||
edit_state.out_fd = 1;
|
edit_state.out_fd = 1;
|
||||||
edit_state.check_input_incomplete_context = NULL;
|
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;
|
sh_read_command->command = command;
|
||||||
}
|
}
|
||||||
|
|
||||||
int run(FILE* fp,
|
static int run(FILE* fp,
|
||||||
const char* fp_name,
|
const char* fp_name,
|
||||||
bool interactive,
|
bool interactive,
|
||||||
bool exit_on_error,
|
bool exit_on_error,
|
||||||
bool* script_exited,
|
bool* script_exited,
|
||||||
int status)
|
int status)
|
||||||
{
|
{
|
||||||
// TODO: The interactive read code should cope when the input is not a
|
// TODO: The interactive read code should cope when the input is not a
|
||||||
// terminal; it should print the prompt and then read normally without
|
// terminal; it should print the prompt and then read normally without
|
||||||
|
@ -2064,6 +2065,38 @@ int run(FILE* fp,
|
||||||
return status;
|
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)
|
static void compact_arguments(int* argc, char*** argv)
|
||||||
{
|
{
|
||||||
for ( int i = 0; i < *argc; i++ )
|
for ( int i = 0; i < *argc; i++ )
|
||||||
|
@ -2243,7 +2276,7 @@ int main(int argc, char* argv[])
|
||||||
if ( !fp )
|
if ( !fp )
|
||||||
error(2, errno, "fmemopen");
|
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);
|
&script_exited, status);
|
||||||
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
@ -2254,7 +2287,7 @@ int main(int argc, char* argv[])
|
||||||
if ( flag_s_stdin )
|
if ( flag_s_stdin )
|
||||||
{
|
{
|
||||||
bool is_interactive = flag_i_interactive || isatty(fileno(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);
|
&script_exited, status);
|
||||||
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
||||||
exit(status);
|
exit(status);
|
||||||
|
@ -2270,7 +2303,7 @@ int main(int argc, char* argv[])
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_interactive = flag_i_interactive || isatty(fileno(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);
|
&script_exited, status);
|
||||||
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
||||||
exit(status);
|
exit(status);
|
||||||
|
@ -2288,7 +2321,7 @@ int main(int argc, char* argv[])
|
||||||
FILE* fp = fopen(path, "r");
|
FILE* fp = fopen(path, "r");
|
||||||
if ( !fp )
|
if ( !fp )
|
||||||
error(127, errno, "%s", path);
|
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);
|
status);
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
||||||
|
@ -2297,7 +2330,7 @@ int main(int argc, char* argv[])
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bool is_interactive = flag_i_interactive || isatty(fileno(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);
|
&script_exited, status);
|
||||||
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
if ( script_exited || (status != 0 && flag_e_exit_on_error) )
|
||||||
exit(status);
|
exit(status);
|
||||||
|
|
Loading…
Reference in New Issue