From 6d94d6db613d0ee2b3b191b97444c5a8e9e6d7bf Mon Sep 17 00:00:00 2001 From: PandaMan Date: Tue, 10 Feb 2026 00:13:46 -0500 Subject: [PATCH] [2.x] fix: lastGrep ignores ANSI escape sequences when matching (#8726) **Problem** When `last` output includes ANSI escape sequences (e.g. colored `[error]` lines), `lastGrep ` was matching against the raw string. The pattern could fail to match because it was compared to text like `\u001B[31merror\u001B[0m` instead of `error`, or matching was inconsistent. **Solution** - Strip ANSI from each line before running the regex, using `EscHelpers.stripColorsAndMoves` from `sbt.internal.util`. - **`Output.lastGrep`** (keys and file overloads): lines from the last run are stripped, then the pattern is applied to the stripped text; matching and printed lines are based on visible text. - **`Output.grep`**: each line is stripped before `showMatches(pattern)` so the pattern is applied only to visible content. --- main/src/main/scala/sbt/internal/Output.scala | 17 +++++++-- .../test/scala/sbt/internal/OutputSpec.scala | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 main/src/test/scala/sbt/internal/OutputSpec.scala diff --git a/main/src/main/scala/sbt/internal/Output.scala b/main/src/main/scala/sbt/internal/Output.scala index 3205b1747..4a0c8c21b 100644 --- a/main/src/main/scala/sbt/internal/Output.scala +++ b/main/src/main/scala/sbt/internal/Output.scala @@ -19,6 +19,7 @@ import Aggregation.{ KeyValue, Values } import Types.idFun import Highlight.{ bold, showMatches } import annotation.tailrec +import sbt.internal.util.EscHelpers import sbt.io.IO @@ -43,7 +44,12 @@ object Output { printLines: Seq[String] => Unit )(using display: Show[ScopedKey[?]]): Unit = { val pattern = Pattern.compile(patternString) - val lines = flatLines(lastLines(keys, streams))(_ flatMap showMatches(pattern)) + val lines = flatLines(lastLines(keys, streams)) { rawLines => + rawLines.flatMap { line => + val stripped = EscHelpers.stripColorsAndMoves(line) + showMatches(pattern)(stripped) + } + } printLines(lines) } @@ -55,8 +61,13 @@ object Output { ): Unit = printLines(grep(tailLines(file, tailDelim), patternString)) - def grep(lines: Seq[String], patternString: String): Seq[String] = - lines.flatMap(showMatches(Pattern.compile(patternString))) + def grep(lines: Seq[String], patternString: String): Seq[String] = { + val pattern = Pattern.compile(patternString) + lines.flatMap { line => + val stripped = EscHelpers.stripColorsAndMoves(line) + showMatches(pattern)(stripped) + } + } def flatLines(outputs: Values[Seq[String]])(f: Seq[String] => Seq[String])(using display: Show[ScopedKey[?]] diff --git a/main/src/test/scala/sbt/internal/OutputSpec.scala b/main/src/test/scala/sbt/internal/OutputSpec.scala new file mode 100644 index 000000000..3d9597fc0 --- /dev/null +++ b/main/src/test/scala/sbt/internal/OutputSpec.scala @@ -0,0 +1,35 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal + +import scala.Console.{ RED, RESET } +import verify.BasicTestSuite +import sbt.internal.Output.grep + +object OutputSpec extends BasicTestSuite { + + test( + "grep should match pattern against visible text when lines contain ANSI escape sequences (#4840)" + ) { + // Line with ANSI color around "error" - user searching for "error" should find it (strip before match) + val lineWithAnsi = s"${RED}error${RESET}: something failed" + val lines = Seq("info: ok", lineWithAnsi, "warn: deprecated") + val result = grep(lines, "error") + assert(result.size == 1, s"expected 1 match, got ${result.size}: $result") + // Pattern matched the visible "error" (ANSI was stripped before matching); result may have highlight from showMatches + assert(result.head.contains("error"), s"result should contain 'error': ${result.head}") + } + + test("grep should not match when pattern appears only inside ANSI sequence") { + // Line where "error" is not in the visible text (only in escape code - unrealistic but ensures we strip first) + val lines = Seq("info: ok", "something failed") + val result = grep(lines, "error") + assert(result.isEmpty, s"expected no match, got: $result") + } +}