From 12bdab620f29373c0db31a2be67be1bc16e02af0 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Mon, 9 Mar 2026 18:21:29 -0400 Subject: [PATCH] repl: fix history truncation of long lines Editline's `read_history` uses `fgets(buf, 256, fp)` internally, splitting any line longer than 255 characters into multiple history entries. This commit bypasses it with `FdSource::readLine` and per-line `add_history` calls, which handle arbitrary lengths. GNU readline's `read_history` has no such limit and is left as-is, and `write_history` already writes full lines via `fprintf` so it needs no change either. --- src/libcmd/repl-interacter.cc | 22 +++++++++++++++++++++- src/libutil/file-system.cc | 2 +- tests/repl-completion.nix | 25 ++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/libcmd/repl-interacter.cc b/src/libcmd/repl-interacter.cc index 8eebeec25b5e..50c3dcea3e42 100644 --- a/src/libcmd/repl-interacter.cc +++ b/src/libcmd/repl-interacter.cc @@ -22,6 +22,7 @@ extern "C" { #include "nix/util/finally.hh" #include "nix/cmd/repl-interacter.hh" #include "nix/util/file-system.hh" +#include "nix/util/serialise.hh" #include "nix/cmd/repl.hh" #include "nix/util/environment-variables.hh" @@ -124,8 +125,27 @@ ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleter } #if !USE_READLINE el_hist_size = 1000; -#endif + // editline's read_history uses a fixed 256-byte buffer (SCREEN_INC), + // which silently splits lines longer than 255 characters into separate + // history entries. Read the file ourselves to avoid the length limit. + auto fd = openFileReadonly(historyFile); + if (!fd) { + NativeSysError err("opening file %s", PathFmt(historyFile)); + if (!err.is(std::errc::no_such_file_or_directory) && !err.is(std::errc::not_a_directory)) + logWarning(err.info()); + } else { + try { + FdSource source(fd.get()); + while (true) + add_history(source.readLine().c_str()); + } catch (EndOfFile &) { + } catch (SystemError & e) { + logWarning(e.info()); + } + } +#else read_history(historyFile.string().c_str()); +#endif auto oldRepl = curRepl; curRepl = repl; Guard restoreRepl([oldRepl] { curRepl = oldRepl; }); diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index d2983ec7d321..16e4e6ce8dee 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -227,7 +227,7 @@ std::string readFile(const std::filesystem::path & path) { auto fd = openFileReadonly(path); if (!fd) - throw NativeSysError("opening file %1%", PathFmt(path)); + throw NativeSysError("opening file %s", PathFmt(path)); return readFile(fd.get()); } diff --git a/tests/repl-completion.nix b/tests/repl-completion.nix index 9ea45a7b1b03..853f87077df3 100644 --- a/tests/repl-completion.nix +++ b/tests/repl-completion.nix @@ -50,7 +50,9 @@ runCommand "repl-completion" } exit 0 ''; - passAsFile = [ "expectScript" ]; + passAsFile = [ + "expectScript" + ]; } '' export NIX_STORE=$TMPDIR/store @@ -60,5 +62,26 @@ runCommand "repl-completion" nix-store --init expect $expectScriptPath + + # Write a 300-char line to the history file, then run a REPL session + # that reads it back (read_history) and writes it out (write_history). + histFile=$HOME/.local/share/nix/repl-history + mkdir -p "$(dirname "$histFile")" + printf '%0300d\n' 0 | tr '0' 'a' > "$histFile" + echo "short" >> "$histFile" + + # unbuffer allocates a pty so nix repl runs the interactive + # ReadlineLikeInteracter path (read_history on init, write_history + # on exit). Plain piped input skips history entirely. + echo ":q" | unbuffer -p nix repl --offline --extra-experimental-features nix-command 2>/dev/null || true + + # Verify the long line survived the read/write cycle. + maxLen=$(awk '{ print length }' "$histFile" | sort -rn | head -1) + if [ "$maxLen" -lt 300 ]; then + echo "FAIL: long history line was truncated (max length: $maxLen)" + exit 1 + fi + echo "Long history line preserved (length: $maxLen)." + touch $out ''