[git commit] shell: update HISTFILESIZE code to be actually useful

Denys Vlasenko vda.linux at googlemail.com
Thu Jul 3 17:10:42 UTC 2025


commit: https://git.busybox.net/busybox/commit/?id=9c46a0688576dd8c67d2fc24b68c07402da14fc8
branch: https://git.busybox.net/busybox/commit/?id=refs/heads/master

"HISTFILESIZE=0" in profile wasn't working as intended,
"unset HISTFILE" wasn't preventing creation of history files
Now:
HISTSIZE=n      allows to reduce in-memory history buffer
HISTFILESIZE=n  allows to reduce history file size (0: truncate it)
unset HISTFILE  allows to not save history file at all

function                                             old     new   delta
exitshell                                            138     194     +56
hush_exit                                             97     143     +46
save_history                                         266     296     +30
hush_main                                           1170    1186     +16
.rodata                                           105762  105771      +9
load_history                                         246     254      +8
size_from_HISTFILESIZE                                44      41      -3
read_line_input                                     2746    2712     -34
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 6/2 up/down: 165/-37)           Total: 128 bytes

Signed-off-by: Denys Vlasenko <vda.linux at googlemail.com>
---
 include/libbb.h  |  2 +-
 libbb/Config.src |  4 +--
 libbb/lineedit.c | 98 +++++++++++++++++++++++++++++++-------------------------
 shell/Config.src |  7 ++--
 shell/ash.c      | 26 +++++++++++++--
 shell/hush.c     | 41 ++++++++++++++++++++----
 6 files changed, 120 insertions(+), 58 deletions(-)

diff --git a/include/libbb.h b/include/libbb.h
index 801f072fa..e765e18eb 100644
--- a/include/libbb.h
+++ b/include/libbb.h
@@ -1989,7 +1989,7 @@ typedef struct line_input_t {
 # if MAX_HISTORY
 	int cnt_history;
 	int cur_history;
-	int max_history; /* must never be <= 0 */
+	int max_history; /* must never be < 0 */
 #  if ENABLE_FEATURE_EDITING_SAVEHISTORY
 	/* meaning of this field depends on FEATURE_EDITING_SAVE_ON_EXIT:
 	 * if !FEATURE_EDITING_SAVE_ON_EXIT: "how many lines are
diff --git a/libbb/Config.src b/libbb/Config.src
index b980f19a9..55e670dcd 100644
--- a/libbb/Config.src
+++ b/libbb/Config.src
@@ -182,8 +182,8 @@ config FEATURE_EDITING_VI
 config FEATURE_EDITING_HISTORY
 	int "History size"
 	# Don't allow way too big values here, code uses fixed "char *history[N]" struct member
-	range 0 9999
-	default 255
+	range 0 2000
+	default 200
 	depends on FEATURE_EDITING
 	help
 	Specify command history size (0 - disable).
diff --git a/libbb/lineedit.c b/libbb/lineedit.c
index 10a83bcb7..fe0dbc5b8 100644
--- a/libbb/lineedit.c
+++ b/libbb/lineedit.c
@@ -1406,8 +1406,8 @@ unsigned FAST_FUNC size_from_HISTFILESIZE(const char *hp)
 	int size = MAX_HISTORY;
 	if (hp) {
 		size = atoi(hp);
-		if (size <= 0)
-			return 1;
+		if (size < 0)
+			return 0;
 		if (size > MAX_HISTORY)
 			return MAX_HISTORY;
 	}
@@ -1501,18 +1501,21 @@ static void load_history(line_input_t *st_parm)
 	/* NB: do not trash old history if file can't be opened */
 
 	fp = fopen_for_read(st_parm->hist_file);
-	if (fp) {
-		/* clean up old history */
-		for (idx = st_parm->cnt_history; idx > 0;) {
-			idx--;
-			free(st_parm->history[idx]);
-			st_parm->history[idx] = NULL;
-		}
+	if (!fp)
+		return;
 
-		/* fill temp_h[], retaining only last MAX_HISTORY lines */
-		memset(temp_h, 0, sizeof(temp_h));
-		idx = 0;
-		st_parm->cnt_history_in_file = 0;
+	/* clean up old history */
+	for (idx = st_parm->cnt_history; idx > 0;) {
+		idx--;
+		free(st_parm->history[idx]);
+		st_parm->history[idx] = NULL;
+	}
+
+	/* fill temp_h[], retaining only last max_history lines */
+	memset(temp_h, 0, sizeof(temp_h));
+	idx = 0;
+	st_parm->cnt_history_in_file = 0;
+	if (st_parm->max_history != 0) {
 		while ((line = xmalloc_fgetline(fp)) != NULL) {
 			if (line[0] == '\0') {
 				free(line);
@@ -1525,34 +1528,34 @@ static void load_history(line_input_t *st_parm)
 			if (idx == st_parm->max_history)
 				idx = 0;
 		}
-		fclose(fp);
-
-		/* find first non-NULL temp_h[], if any */
-		if (st_parm->cnt_history_in_file) {
-			while (temp_h[idx] == NULL) {
-				idx++;
-				if (idx == st_parm->max_history)
-					idx = 0;
-			}
-		}
+	}
+	fclose(fp);
 
-		/* copy temp_h[] to st_parm->history[] */
-		for (i = 0; i < st_parm->max_history;) {
-			line = temp_h[idx];
-			if (!line)
-				break;
+	/* find first non-NULL temp_h[], if any */
+	if (st_parm->cnt_history_in_file != 0) {
+		while (temp_h[idx] == NULL) {
 			idx++;
 			if (idx == st_parm->max_history)
 				idx = 0;
-			line_len = strlen(line);
-			if (line_len >= MAX_LINELEN)
-				line[MAX_LINELEN-1] = '\0';
-			st_parm->history[i++] = line;
 		}
-		st_parm->cnt_history = i;
-		if (ENABLE_FEATURE_EDITING_SAVE_ON_EXIT)
-			st_parm->cnt_history_in_file = i;
 	}
+
+	/* copy temp_h[] to st_parm->history[] */
+	for (i = 0; i < st_parm->max_history;) {
+		line = temp_h[idx];
+		if (!line)
+			break;
+		idx++;
+		if (idx == st_parm->max_history)
+			idx = 0;
+		line_len = strlen(line);
+		if (line_len >= MAX_LINELEN)
+			line[MAX_LINELEN-1] = '\0';
+		st_parm->history[i++] = line;
+	}
+	st_parm->cnt_history = i;
+	if (ENABLE_FEATURE_EDITING_SAVE_ON_EXIT)
+		st_parm->cnt_history_in_file = i;
 }
 
 #  if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
@@ -1563,14 +1566,23 @@ void FAST_FUNC save_history(line_input_t *st)
 	if (!st || !st->hist_file)
 		return;
 	if (st->cnt_history <= st->cnt_history_in_file)
-		return;
+		return; /* no new entries were added */
+	/* note: if st->max_history is 0, we do not abort: we truncate the history to 0 lines */
 
-	fp = fopen(st->hist_file, "a");
+	fp = fopen(st->hist_file, (st->max_history == 0 ? "w" : "a"));
 	if (fp) {
 		int i, fd;
 		char *new_name;
 		line_input_t *st_temp;
 
+		/* max_history==0 needs special-casing in general code,
+		 * just handle it in a simpler way: */
+		if (st->max_history == 0) {
+			/* fopen("w") already truncated it */
+			fclose(fp);
+			return;
+		}
+
 		for (i = st->cnt_history_in_file; i < st->cnt_history; i++)
 			fprintf(fp, "%s\n", st->history[i]);
 		fclose(fp);
@@ -1580,6 +1592,8 @@ void FAST_FUNC save_history(line_input_t *st)
 		st_temp = new_line_input_t(st->flags);
 		st_temp->hist_file = st->hist_file;
 		st_temp->max_history = st->max_history;
+		/* load no more than max_history last lines */
+		/* (in unlikely case that file disappeared, st_temp gets empty history) */
 		load_history(st_temp);
 
 		/* write out temp file and replace hist_file atomically */
@@ -1609,7 +1623,6 @@ static void save_history(char *str)
 	fd = open(state->hist_file, O_WRONLY | O_CREAT | O_APPEND, 0600);
 	if (fd < 0)
 		return;
-	xlseek(fd, 0, SEEK_END); /* paranoia */
 	len = strlen(str);
 	str[len] = '\n'; /* we (try to) do atomic write */
 	len2 = full_write(fd, str, len + 1);
@@ -1664,13 +1677,10 @@ static void remember_in_history(char *str)
 	if (str[0] == '\0')
 		return;
 	i = state->cnt_history;
-	/* Don't save dupes */
-	if (i && strcmp(state->history[i-1], str) == 0)
+	/* Don't save dups */
+	if (i != 0 && strcmp(state->history[i-1], str) == 0)
 		return;
 
-	free(state->history[state->max_history]); /* redundant, paranoia */
-	state->history[state->max_history] = NULL; /* redundant, paranoia */
-
 	/* If history[] is full, remove the oldest command */
 	/* we need to keep history[state->max_history] empty, hence >=, not > */
 	if (i >= state->max_history) {
@@ -1683,7 +1693,7 @@ static void remember_in_history(char *str)
 			state->cnt_history_in_file--;
 # endif
 	}
-	/* i <= state->max_history-1 */
+	/* i < state->max_history */
 	state->history[i++] = xstrdup(str);
 	/* i <= state->max_history */
 	state->cur_history = i;
diff --git a/shell/Config.src b/shell/Config.src
index 5efbf9995..5b3fe08f3 100644
--- a/shell/Config.src
+++ b/shell/Config.src
@@ -166,9 +166,10 @@ config FEATURE_SH_HISTFILESIZE
 	default y
 	depends on SHELL_ASH || SHELL_HUSH
 	help
-	This option makes busybox shells to use $HISTFILESIZE variable
-	to set shell history size. Note that its max value is capped
-	by "History size" setting in library tuning section.
+	This option makes busybox shells to use $HISTSIZE and
+	$HISTFILESIZE variables to set shell history size.
+	Note that its max value is capped by "History size" setting
+	in library tuning section.
 
 config FEATURE_SH_EMBEDDED_SCRIPTS
 	bool "Embed scripts in the binary"
diff --git a/shell/ash.c b/shell/ash.c
index 16eb88a7b..18344767a 100644
--- a/shell/ash.c
+++ b/shell/ash.c
@@ -14525,8 +14525,25 @@ exitshell(void)
 	char *p;
 
 #if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
-	save_history(line_input_state); /* may be NULL */
+	if (line_input_state) {
+		const char *hp;
+# if ENABLE_FEATURE_SH_HISTFILESIZE
+// in bash:
+// HISTFILESIZE controls the on-disk history file size (in lines, 0=no history):
+// "When this variable is assigned a value, the history file is truncated, if necessary"
+// but we do it only at exit, not on assignment:
+		/* Use HISTFILESIZE to limit file size */
+		hp = lookupvar("HISTFILESIZE");
+		if (hp)
+			line_input_state->max_history = size_from_HISTFILESIZE(hp);
+# endif
+		/* HISTFILE: "If unset, the command history is not saved when a shell exits." */
+		hp = lookupvar("HISTFILE");
+		line_input_state->hist_file = hp;
+		save_history(line_input_state); /* no-op if hist_file is NULL */
+	}
 #endif
+
 	savestatus = exitstatus;
 	TRACE(("pid %d, exitshell(%d)\n", getpid(), savestatus));
 	if (setjmp(loc.loc))
@@ -14867,7 +14884,12 @@ int ash_main(int argc UNUSED_PARAM, char **argv)
 			if (hp)
 				line_input_state->hist_file = xstrdup(hp);
 # if ENABLE_FEATURE_SH_HISTFILESIZE
-			hp = lookupvar("HISTFILESIZE");
+			hp = lookupvar("HISTSIZE");
+			/* Using HISTFILESIZE above to limit max_history would be WRONG:
+			 * users may set HISTFILESIZE=0 in their profile scripts
+			 * to prevent _saving_ of history files, but still want to have
+			 * non-zero history limit for in-memory list.
+			 */
 			line_input_state->max_history = size_from_HISTFILESIZE(hp);
 # endif
 		}
diff --git a/shell/hush.c b/shell/hush.c
index 70b730f67..68aca53a3 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -2099,11 +2099,29 @@ static sighandler_t pick_sighandler(unsigned sig)
 	return handler;
 }
 
+static const char* FAST_FUNC get_local_var_value(const char *name);
+
 /* Restores tty foreground process group, and exits. */
 static void hush_exit(int exitcode)
 {
 #if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
-	save_history(G.line_input_state); /* may be NULL */
+	if (G.line_input_state) {
+		const char *hp;
+# if ENABLE_FEATURE_SH_HISTFILESIZE
+// in bash:
+// HISTFILESIZE controls the on-disk history file size (in lines, 0=no history):
+// "When this variable is assigned a value, the history file is truncated, if necessary"
+// but we do it only at exit, not on every assignment:
+		/* Use HISTFILESIZE to limit file size */
+		hp = get_local_var_value("HISTFILESIZE");
+		if (hp)
+			G.line_input_state->max_history = size_from_HISTFILESIZE(hp);
+# endif
+		/* HISTFILE: "If unset, the command history is not saved when a shell exits." */
+		hp = get_local_var_value("HISTFILE");
+		G.line_input_state->hist_file = hp;
+		save_history(G.line_input_state); /* no-op if hist_file is NULL */
+	}
 #endif
 
 	fflush_all();
@@ -10427,7 +10445,7 @@ int hush_main(int argc, char **argv)
 	if (!get_local_var_value("PATH"))
 		set_local_var_from_halves("PATH", bb_default_root_path);
 
-	/* PS1/PS2 are set later, if we determine that we are interactive */
+	/* PS1/PS2/HISTFILE are set later, if we determine that we are interactive */
 
 	/* bash also exports SHLVL and _,
 	 * and sets (but doesn't export) the following variables:
@@ -10449,7 +10467,6 @@ int hush_main(int argc, char **argv)
 	 * BASH_SOURCE=()
 	 * DIRSTACK=()
 	 * PIPESTATUS=([0]="0")
-	 * HISTFILE=/<xxx>/.bash_history
 	 * HISTFILESIZE=500
 	 * HISTSIZE=500
 	 * MAILCHECK=60
@@ -10809,18 +10826,30 @@ int hush_main(int argc, char **argv)
 			const char *hp = get_local_var_value("HISTFILE");
 			if (!hp) {
 				hp = get_local_var_value("HOME");
-				if (hp)
+				if (hp) {
 					hp = concat_path_file(hp, ".hush_history");
+					/* Make HISTFILE set on exit (else history won't be saved) */
+					set_local_var_from_halves("HISTFILE", hp);
+				}
 			} else {
 				hp = xstrdup(hp);
 			}
 			if (hp) {
 				G.line_input_state->hist_file = hp;
-				//set_local_var(xasprintf("HISTFILE=%s", ...));
 			}
 #  if ENABLE_FEATURE_SH_HISTFILESIZE
-			hp = get_local_var_value("HISTFILESIZE");
+			hp = get_local_var_value("HISTSIZE");
+			/* Using HISTFILESIZE above to limit max_history would be WRONG:
+			 * users may set HISTFILESIZE=0 in their profile scripts
+			 * to prevent _saving_ of history files, but still want to have
+			 * non-zero history limit for in-memory list.
+			 */
+// in bash, runtime history size is controlled by HISTSIZE (0=no history),
+// HISTFILESIZE controls on-disk history file size (in lines, 0=no history):
 			G.line_input_state->max_history = size_from_HISTFILESIZE(hp);
+// HISTFILESIZE: "The shell sets the default value to the value of HISTSIZE after reading any startup files."
+// HISTSIZE: "The shell sets the default value to 500 after reading any startup files."
+// (meaning: if the value wasn't set after startup files, the default value is set as described above)
 #  endif
 		}
 # endif


More information about the busybox-cvs mailing list