repl: strip trailing CR when loading history (Windows CRLF) - #36510
repl: strip trailing CR when loading history (Windows CRLF)#36510robobun wants to merge 2 commits into
Conversation
WalkthroughThe REPL history loader strips trailing carriage returns from loaded lines. A non-Windows test verifies normalized recall, ChangesREPL history normalization
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/repl/repl.test.ts`:
- Around line 1287-1292: In the REPL subprocess test, move the stderr and
exitCode assertions before the Bun.file(...).text() call so subprocess failure
is reported first. Keep the saved-history content assertions unchanged and only
read the history file after confirming the subprocess succeeded.
- Around line 1280-1285: Add explicit presence assertions for both “old_one” and
“old_two” in historyLines before the carriage-return loop. Keep the existing
per-line CR checks, ensuring the test fails when either loaded history entry is
missing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 379bf9b6-0601-4db1-9ba5-6516a5de747f
📒 Files selected for processing (2)
src/runtime/cli/repl.rstest/js/bun/repl/repl.test.ts
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — one-line strip_suffix(b"\r") in History::load is the obvious fix, and the saved-file toEqual assertion in the new test independently catches the regression, so the inline nit about the possibly-empty loop is non-blocking.
Extended reasoning...
Overview
The PR adds a single line to History::load in src/runtime/cli/repl.rs: let line = line.strip_suffix(b"\r").unwrap_or(line);, so a CRLF history file (written on Windows or a CRLF editor) no longer leaves a trailing \r on every loaded entry. It also adds a regression test in test/js/bun/repl/repl.test.ts that seeds .bun_repl_history with CRLF, runs .history + one new entry + .exit, and asserts both the .history output and the re-saved file contain no \r.
Security risks
None. This is byte-level trimming of a locally-read history file inside the REPL's own history loader; no untrusted network input, no path handling, no privilege changes.
Level of scrutiny
Low. The Rust change is a one-liner using the idiomatic strip_suffix().unwrap_or() pattern on a &[u8]; a bare-\r line correctly becomes empty and is skipped by the existing !line.is_empty() guard, and save() already writes LF-only so the file normalizes after one round-trip. The test follows harness conventions (tempDir, bunEnv/bunExe(), await using, concurrent drain of stdout/stderr/exited, HOME+USERPROFILE for cross-platform), and the PR description shows it fails on the unfixed build.
Other factors
The one finding is a nit: the for (const line of historyLines) loop over a filtered array has no non-emptiness guard. It's non-blocking because the saved-file toEqual(["old_one","old_two","new_three"]) assertion at line 1289 independently proves the CR was stripped — that assertion would see ["old_one\r","old_two\r","new_three"] on the unfixed build regardless of whether .history output rendering ever changes. No prior reviews or unresolved comments on the PR.
There was a problem hiding this comment.
LGTM — the vacuous-loop guard from the earlier review is now in place (expect(historyLines).toHaveLength(2)), and all threads are resolved.
What was reviewed:
History::load:strip_suffix(b"\r")runs before the empty check, so a bare\rline is correctly dropped rather than stored as an empty entry.- Test: pipes drained concurrently,
HOME/USERPROFILEboth set,new_three's ReferenceError goes to stdout (not stderr) soexpect(stderr).toBe("")holds; history is added before evaluation so the entry persists regardless. - Saved-file
toEqual([...])assertion independently catches the regression, verified fails-without-fix in the PR description.
Extended reasoning...
Overview
One-line change to src/runtime/cli/repl.rs (History::load): each history line is passed through .strip_suffix(b"\r").unwrap_or(line) before the emptiness check and storage. A new test in test/js/bun/repl/repl.test.ts seeds a CRLF history file, runs .history + a new entry + .exit, and asserts both the .history output and the re-saved file contain no \r.
Security risks
None. This is purely local REPL history-file parsing with no network, auth, or untrusted-boundary implications. The strip is applied to data the user already controls (their own ~/.bun_repl_history).
Level of scrutiny
Low. The Rust change is a single idiomatic slice operation with no allocation, no unsafe, and no control-flow change beyond making a previously non-empty "\r" line empty (which is then correctly filtered out by the existing !line.is_empty() guard). save() already writes LF-only, so the round-trip normalizes.
Other factors
- My earlier inline nit (guard the possibly-empty
historyLinesfilter) was addressed in commit a15ec10 withexpect(historyLines).toHaveLength(2); the thread is resolved. - The other CodeRabbit suggestion (assertion ordering before file read) was reasonably declined by the author: the file is pre-seeded by
tempDir, so the read cannot throw, and a REPL crash would surface as the seeded CRLF content failingnot.toContain("\r")— matching the neighboring history-permissions tests and the repo's assert-exit-code-last convention. - Verified the test's
stderr === ""expectation holds despitenew_threebeing an undefined identifier: interactive-REPL errors route throughprint_js_error→Output::writer()(stdout), andhistory.add()runs beforeevaluate_and_print(), so the entry is persisted regardless of the ReferenceError. - PR description shows the test failing on the unfixed build and passing with the fix, satisfying the fails-for-the-right-reason requirement.
|
Status: rebased onto current main (f426a8e) as 8a4f8af. The only conflict was the loop header in Verified locally after the rebase:
CI on 8a4f8af (build 93728, finished): 179 of 181 jobs passed; While re-reviewing this I noticed a separate pre-existing issue in the same loader: a multi-line entry is saved as several physical lines and reloads as several entries. That is independent of CRLF handling and is being tracked separately, not in this PR. Previous status (a15ec10, before the rebase)CI on a15ec10: the diff was green. |
History.load split the history file on '\n' only, so a file written with CRLF line endings (Windows, or after editing in a CRLF editor) kept a trailing '\r' on every loaded entry. The stray CR then leaked into arrow-key recall, .history output, and the next save, compounding on every session. Strip a trailing '\r' from each split line before storing the entry. Adopts the fix from #30293 for the current Rust REPL (the repl.zig hunk in that PR no longer applies). Co-authored-by: Samuel <samuelpatro@users.noreply.github.com>
a15ec10 to
8a4f8af
Compare
There was a problem hiding this comment.
LGTM — the vacuous-loop guard from my earlier note is now in place (expect(historyLines).toHaveLength(2)), and both other review threads are resolved.
What was reviewed:
History::load:strip_suffix(b"\r")runs before the!line.is_empty()check, so a bare\rline becomes empty and is skipped;save()already writes LF-only.- Test: non-TTY mode does not echo input, so the
.historyoutput yields exactly 2 filtered lines;new_threeis added to history before eval so the saved-filetoEqualholds even though it throws a ReferenceError (which goes to stdout, keepingstderr === ""). - Cross-platform:
env_var::HOMEreadsUSERPROFILEon Windows and the test sets both, so the new describe block is correctly ungated.
Extended reasoning...
Overview
One-line fix in src/runtime/cli/repl.rs (History::load): strip a trailing \r from each line after splitting on \n, so CRLF history files (written on Windows or by CRLF editors) don't leave stray CRs on loaded entries. Accompanied by a new test in test/js/bun/repl/repl.test.ts that seeds a CRLF history file, runs .history + a new entry + .exit, and asserts no \r in either the .history output or the re-saved file.
Security risks
None. The change is a byte-suffix strip on a locally-read history file; no new syscalls, no untrusted-input parsing surface, no permission changes.
Level of scrutiny
Low. This is a mechanical string-normalization fix in a non-critical CLI path (REPL history loading), with clear before/after behavior and a targeted regression test. The Rust change is a single idiomatic strip_suffix().unwrap_or() inserted before the existing empty-line filter, so bare-CR lines correctly become empty and are skipped.
Other factors
- My earlier inline nit (possibly-empty
historyLinesloop) was addressed in commit 8a4f8af withexpect(historyLines).toHaveLength(2). - CodeRabbit's assertion-ordering concern was withdrawn after robobun pointed out the file is pre-seeded by
tempDir, so the read cannot mask a subprocess failure — matching neighboring history tests. - Verified the test is cross-platform:
env_var::HOMEmaps toUSERPROFILEon Windows (src/bun_core/env_var.rs:148) and the test sets both env vars; the newdescribeis correctly not Windows-gated. - Verified the
toHaveLength(2)guard is exact: in non-TTY moderefresh_linedoes not echo input, soold_one/old_twoappear only once each (fromcmd_historyoutput). - CI: robobun reports
repl.test.tspassed on every lane; red lanes are unrelated (R2 outage, pre-existing worker_threads flake).
|
Updated 8:55 PM PT - Aug 12th, 2026
❌ @robobun, your commit 8a4f8af has some failures in 🧪 To try this PR locally: bunx bun-pr 36510That installs a local version of the PR into your bun-36510 --bun |
|
Heads-up: #38023 (one line per history entry, so multi-line entries survive a restart) includes this same trailing-CR strip in |
The trailing-CR strip in History::load is the fix from #30293 (carried by #36510 until this PR superseded it). Add that PR's case as its own test: a history file of ordinary entries with CRLF line endings loads without a '\r' on any entry and is written back with LF endings. Co-authored-by: Samuel <samuelpatro@users.noreply.github.com>
|
Closing in favour of #38023, which now owns this code path. Both PRs change the same loop in What was carried over to #38023 (commit 718d951):
No further work is needed on this branch. |
Adopts #30293 by @samuelpatro onto current main. That PR also patches
src/runtime/cli/repl.zig, which no longer exists; this applies the fix torepl.rsonly.Repro
Arrow-up recall lands the cursor at column 0 after the recalled text, and the next save writes
old_one\r\nold_two\r\n…back, so the stray CR survives every session.Cause
History::loadinsrc/runtime/cli/repl.rssplits the history file on'\n'only:A history file with CRLF line endings (written on Windows, or edited in a CRLF-defaulting editor) therefore leaves a trailing
'\r'on every loaded entry. That CR leaks into arrow-key recall,.historyoutput, and the nextsave()(which writesentry + '\n'), so the corruption compounds.Fix
Strip a trailing
'\r'from each split line before storing it:save()already writes LF-only, so the file normalizes after one load/save cycle.Why this is the right behavior:
/\r?\n+/(see the vendoredsrc/js/internal/repl/history.jsused bynode:repl), so a CRLF history file loads cleanly there too.save()emitsentry + '\n', so an entry whose text ended in'\r'would be written as CRLF and be indistinguishable from a CRLF file anyway. The native format has no meaning for a trailing CR."\r") becomes empty and is skipped, the same as a blank LF line already is.Verification
New test in
test/js/bun/repl/repl.test.ts(REPL history file > strips CRLF when loading existing history) seeds.bun_repl_historywith"old_one\r\nold_two\r\n", runs.history+ a new entry +.exit, then asserts:.historyoutput lines contain no\r\rand splits to exactly["old_one","old_two","new_three"]Closes #30293.
no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/repl/repl.test.ts