lineedit: add support for history saving on exit

Based on the patch by Dennis Groenen <tj.groenen@gmail.com>

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/include/libbb.h b/include/libbb.h
index 63d0419..91343a9 100644
--- a/include/libbb.h
+++ b/include/libbb.h
@@ -1421,6 +1421,12 @@
 	int cur_history;
 	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
+	 * in on-disk history"
+	 * if FEATURE_EDITING_SAVE_ON_EXIT: "how many in-memory lines are
+	 * also in on-disk history (and thus need to be skipped on save)"
+	 */
 	unsigned cnt_history_in_file;
 	const char *hist_file;
 #  endif
@@ -1446,6 +1452,9 @@
  * >0 length of input string, including terminating '\n'
  */
 int read_line_input(line_input_t *st, const char *prompt, char *command, int maxsize, int timeout) FAST_FUNC;
+# if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+void save_history(line_input_t *st);
+# endif
 #else
 #define MAX_HISTORY 0
 int read_line_input(const char* prompt, char* command, int maxsize) FAST_FUNC;
diff --git a/libbb/Config.src b/libbb/Config.src
index aa44236..f6f88b9 100644
--- a/libbb/Config.src
+++ b/libbb/Config.src
@@ -94,6 +94,13 @@
 	help
 	  Enable history saving in shells.
 
+config FEATURE_EDITING_SAVE_ON_EXIT
+	bool "Save history on shell exit, not after every command"
+	default n
+	depends on FEATURE_EDITING_SAVEHISTORY
+	help
+	  Save history on shell exit, not after every command.
+
 config FEATURE_REVERSE_SEARCH
 	bool "Reverse history search"
 	default y
diff --git a/libbb/lineedit.c b/libbb/lineedit.c
index 5d13904..0786f9a 100644
--- a/libbb/lineedit.c
+++ b/libbb/lineedit.c
@@ -1351,7 +1351,9 @@
 
 		/* fill temp_h[], retaining only last MAX_HISTORY lines */
 		memset(temp_h, 0, sizeof(temp_h));
-		st_parm->cnt_history_in_file = idx = 0;
+		idx = 0;
+		if (!ENABLE_FEATURE_EDITING_SAVE_ON_EXIT)
+			st_parm->cnt_history_in_file = 0;
 		while ((line = xmalloc_fgetline(fp)) != NULL) {
 			if (line[0] == '\0') {
 				free(line);
@@ -1359,7 +1361,8 @@
 			}
 			free(temp_h[idx]);
 			temp_h[idx] = line;
-			st_parm->cnt_history_in_file++;
+			if (!ENABLE_FEATURE_EDITING_SAVE_ON_EXIT)
+				st_parm->cnt_history_in_file++;
 			idx++;
 			if (idx == st_parm->max_history)
 				idx = 0;
@@ -1389,15 +1392,66 @@
 			st_parm->history[i++] = line;
 		}
 		st_parm->cnt_history = i;
+		if (ENABLE_FEATURE_EDITING_SAVE_ON_EXIT)
+			st_parm->cnt_history_in_file = i;
 	}
 }
 
-/* state->flags is already checked to be nonzero */
+#  if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+void save_history(line_input_t *st)
+{
+	FILE *fp;
+
+	if (!(st->flags & SAVE_HISTORY))
+		return;
+	if (!st->hist_file)
+		return;
+	if (st->cnt_history <= st->cnt_history_in_file)
+		return;
+
+	fp = fopen(st->hist_file, "a");
+	if (fp) {
+		int i, fd;
+		char *new_name;
+		line_input_t *st_temp;
+
+		for (i = st->cnt_history_in_file; i < st->cnt_history; i++)
+			fprintf(fp, "%s\n", st->history[i]);
+		fclose(fp);
+
+		/* we may have concurrently written entries from others.
+		 * load them */
+		st_temp = new_line_input_t(st->flags);
+		st_temp->hist_file = st->hist_file;
+		st_temp->max_history = st->max_history;
+		load_history(st_temp);
+
+		/* write out temp file and replace hist_file atomically */
+		new_name = xasprintf("%s.%u.new", st->hist_file, (int) getpid());
+		fd = open(new_name, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+		if (fd >= 0) {
+			fp = xfdopen_for_write(fd);
+			for (i = 0; i < st_temp->cnt_history; i++)
+				fprintf(fp, "%s\n", st_temp->history[i]);
+			fclose(fp);
+			if (rename(new_name, st->hist_file) == 0)
+				st->cnt_history_in_file = st_temp->cnt_history;
+		}
+		free(new_name);
+		free_line_input_t(st_temp);
+	}
+}
+#  else
 static void save_history(char *str)
 {
 	int fd;
 	int len, len2;
 
+	if (!(state->flags & SAVE_HISTORY))
+		return;
+	if (!state->hist_file)
+		return;
+
 	fd = open(state->hist_file, O_WRONLY | O_CREAT | O_APPEND, 0600);
 	if (fd < 0)
 		return;
@@ -1441,6 +1495,7 @@
 		free_line_input_t(st_temp);
 	}
 }
+#  endif
 # else
 #  define load_history(a) ((void)0)
 #  define save_history(a) ((void)0)
@@ -1469,15 +1524,16 @@
 		for (i = 0; i < state->max_history-1; i++)
 			state->history[i] = state->history[i+1];
 		/* i == state->max_history-1 */
+		if (ENABLE_FEATURE_EDITING_SAVE_ON_EXIT && state->cnt_history_in_file)
+			state->cnt_history_in_file--;
 	}
 	/* i <= state->max_history-1 */
 	state->history[i++] = xstrdup(str);
 	/* i <= state->max_history */
 	state->cur_history = i;
 	state->cnt_history = i;
-# if ENABLE_FEATURE_EDITING_SAVEHISTORY
-	if ((state->flags & SAVE_HISTORY) && state->hist_file)
-		save_history(str);
+# if ENABLE_FEATURE_EDITING_SAVEHISTORY && !ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+	save_history(str);
 # endif
 	IF_FEATURE_EDITING_FANCY_PROMPT(num_ok_lines++;)
 }
diff --git a/shell/ash.c b/shell/ash.c
index bf376bd..14472cb 100644
--- a/shell/ash.c
+++ b/shell/ash.c
@@ -12888,6 +12888,10 @@
 	char *p;
 	int status;
 
+#if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+	save_history(line_input_state);
+#endif
+
 	status = exitstatus;
 	TRACE(("pid %d, exitshell(%d)\n", getpid(), status));
 	if (setjmp(loc.loc)) {
diff --git a/shell/hush.c b/shell/hush.c
index 42143fd..a9e2dd3 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -1541,6 +1541,10 @@
 static void hush_exit(int exitcode) NORETURN;
 static void hush_exit(int exitcode)
 {
+#if ENABLE_FEATURE_EDITING_SAVE_ON_EXIT
+	save_history(G.line_input_state);
+#endif
+
 	fflush_all();
 	if (G.exiting <= 0 && G.traps && G.traps[0] && G.traps[0][0]) {
 		char *argv[3];