diff --git a/src/runtime/cli/repl.rs b/src/runtime/cli/repl.rs index 4c6a360049dd..eabd17ad4ca0 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -254,7 +254,10 @@ impl History { sys::Result::Err(_) => return Ok(()), }; - for line in content.split(|b: &u8| *b == b'\n') { + for mut line in content.split(|b: &u8| *b == b'\n') { + if line.last() == Some(&b'\r') { + line = &line[..line.len() - 1]; + } if !line.is_empty() { self.entries.push(Box::<[u8]>::from(line)); } diff --git a/src/runtime/cli/repl.zig b/src/runtime/cli/repl.zig index dab34551a85f..bdba85c0a512 100644 --- a/src/runtime/cli/repl.zig +++ b/src/runtime/cli/repl.zig @@ -196,7 +196,8 @@ const History = struct { defer self.allocator.free(content); var lines = std.mem.splitScalar(u8, content, '\n'); - while (lines.next()) |line| { + while (lines.next()) |line_| { + const line = if (line_.len > 0 and line_[line_.len - 1] == '\r') line_[0 .. line_.len - 1] else line_; if (line.len > 0) { const entry = try self.allocator.dupe(u8, line); try self.entries.append(entry); diff --git a/test/js/bun/repl/repl.test.ts b/test/js/bun/repl/repl.test.ts index 88357637142a..0322fd04b979 100644 --- a/test/js/bun/repl/repl.test.ts +++ b/test/js/bun/repl/repl.test.ts @@ -910,6 +910,40 @@ describe.concurrent("Bun REPL", () => { expect(exitCode).toBe(0); }); }); + + describe("history file", () => { + // A history file written on Windows can contain CRLF line endings. Loading + // it must strip the trailing '\r' so entries don't carry a stray CR through + // recall, .history output, or the next save. + test("strips CRLF when loading existing history", async () => { + using dir = tempDir("repl-history-crlf", { + ".bun_repl_history": "old_one\r\nold_two\r\n", + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "repl"], + stdin: Buffer.from("new_three\n.exit\n"), + stdout: "pipe", + stderr: "pipe", + env: { + ...bunEnv, + TERM: "dumb", + NO_COLOR: "1", + HOME: String(dir), + USERPROFILE: String(dir), // Windows fallback + }, + }); + const exitCode = await proc.exited; + + const saved = await Bun.file(path.join(String(dir), ".bun_repl_history")).text(); + // Saved history must not preserve carriage returns from the original file. + expect(saved).not.toContain("\r"); + expect(saved).toContain("old_one\n"); + expect(saved).toContain("old_two\n"); + expect(saved).toContain("new_three\n"); + expect(exitCode).toBe(0); + }); + }); }); // Interactive terminal-based REPL tests