From 02f583d99fa5e2201d592c6be9aaa4886da69675 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:25:47 +0000 Subject: [PATCH] [patch] End output lines only at CR, LF and CRLF LineOutputHandler also split on NEL (U+0085), LS (U+2028), PS (U+2029) and form feed, a set carried over from the old ReplaceLineEndings code. Tools emit LS and PS raw inside JSON strings, so NDJSON records were cut in half, and Latin-1 output split at every 0x85 byte. Lines now end where Process.OutputDataReceived and StreamReader.ReadLine end them. Behaviour change: a caller that relied on FF or NEL splitting now gets those characters inside the line. Fixes ktsu-dev/RunCommand#133 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5RAKCxdwrsVmygQDP --- README.md | 2 +- RunCommand.Test/LineOutputHandlerTests.cs | 41 +++++++++++++++++++++++ RunCommand/LineOutputHandler.cs | 8 +++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6388281..2bad560 100644 --- a/README.md +++ b/README.md @@ -394,7 +394,7 @@ Processes output in raw, undelimited chunks as they arrive from the process. ### `LineOutputHandler` -Inherits from `OutputHandler` and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives. +Inherits from `OutputHandler` and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives. Lines end at LF, CR or CRLF, the same as `Process.OutputDataReceived` and `StreamReader.ReadLine`; NEL (U+0085), LS (U+2028), PS (U+2029) and form feed stay inside the line, so line-delimited JSON that carries them raw arrives one record per call. #### Constructor diff --git a/RunCommand.Test/LineOutputHandlerTests.cs b/RunCommand.Test/LineOutputHandlerTests.cs index b21b819..3b84635 100644 --- a/RunCommand.Test/LineOutputHandlerTests.cs +++ b/RunCommand.Test/LineOutputHandlerTests.cs @@ -164,6 +164,47 @@ public void HandleStandardOutputDataShouldSplitOnEveryLineEndingKind() Assert.AreEqual("e", handler.outputBuffer.ToString()); } + [TestMethod] + [DataRow('\u2028', DisplayName = "LS")] + [DataRow('\u2029', DisplayName = "PS")] + [DataRow('\u0085', DisplayName = "NEL")] + [DataRow('\f', DisplayName = "FF")] + public void HandleStandardOutputDataShouldKeepAnUnusualSeparatorInsideTheLine(char separator) + { + // Arrange + string record = $"{{\"a\":\"x{separator}y\"}}"; + List lines = []; + LineOutputHandler handler = new(onStandardOutput: lines.Add); + + // Act + handler.HandleStandardOutputData($"{record}\n{record}\n"); + + // Assert + string[] expected = [record, record]; + Assert.AreSequenceEqual(expected, lines); + } + + [TestMethod] + [DataRow('\u2028', DisplayName = "LS")] + [DataRow('\u2029', DisplayName = "PS")] + [DataRow('\u0085', DisplayName = "NEL")] + [DataRow('\f', DisplayName = "FF")] + public void HandleStandardErrorDataShouldKeepAnUnusualSeparatorOnAChunkBoundaryInsideTheLine(char separator) + { + // Arrange + List lines = []; + LineOutputHandler handler = new(onStandardError: lines.Add); + + // Act + handler.HandleStandardErrorData($"x{separator}"); + handler.HandleStandardErrorData($"y\nz{separator}"); + handler.HandleStandardErrorData("\n"); + + // Assert + string[] expected = [$"x{separator}y", $"z{separator}"]; + Assert.AreSequenceEqual(expected, lines); + } + [TestMethod] public void CompleteShouldDeliverTheFinalUnterminatedLine() { diff --git a/RunCommand/LineOutputHandler.cs b/RunCommand/LineOutputHandler.cs index d04223a..fcf7212 100644 --- a/RunCommand/LineOutputHandler.cs +++ b/RunCommand/LineOutputHandler.cs @@ -200,9 +200,11 @@ private static void EmitLine(StringBuilder buffer, string data, int start, int e } /// - /// Determines whether a character other than CR ends a line, matching the set string.ReplaceLineEndings recognises. + /// Determines whether a character other than CR ends a line. Only LF does, matching + /// Process.OutputDataReceived and StreamReader.ReadLine: NEL, LS, PS and FF are ordinary characters, + /// since tools emit them raw inside JSON strings and a line-delimited record must arrive whole. /// /// The character to test. - /// if is LF, NEL, LS, PS or FF; otherwise . - private static bool IsLineBreak(char c) => c is '\n' or '\u0085' or '\u2028' or '\u2029' or '\f'; + /// if is LF; otherwise . + private static bool IsLineBreak(char c) => c == '\n'; }