diff --git a/modules/build/src/main/scala/scala/build/preprocessing/SheBang.scala b/modules/build/src/main/scala/scala/build/preprocessing/SheBang.scala index 6da5192c69..3d1a0c04fc 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/SheBang.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/SheBang.scala @@ -1,37 +1,40 @@ package scala.build.preprocessing -import scala.util.matching.Regex - object SheBang { - private val sheBangRegex: Regex = s"""(^(#!.*(\\X)?)+(\\X*!#.*)?)""".r - - def isShebangScript(content: String): Boolean = sheBangRegex.unanchored.matches(content) + def isShebangScript(content: String): Boolean = content.startsWith("#!") /** Returns the shebang section and the content without the shebang section */ def partitionOnShebangSection(content: String): (String, String) = if (content.startsWith("#!")) { - val regexMatch = sheBangRegex.findFirstMatchIn(content) - regexMatch match { - case Some(firstMatch) => - (firstMatch.toString(), content.replaceFirst(firstMatch.toString(), "")) - case None => ("", content) + val (header, body) = content.indexOf("\n!#") match { + case -1 => + val line1 = content.take(content.indexOf("\n") + 1) + val remaining = content.drop(line1.length) + val header = if remaining.startsWith("#!") then + val l2eol = remaining.indexOf("\n") + val line2 = remaining.take(l2eol + 1) + line1 + line2 + else + line1 + val body = content.drop(header.length) + (header, body) + case index => + var i = index + 1 // skip over leading newline + while (i < content.length && content(i) != '\n') i += 1 + // skip over newline + while (i < content.length && (content(i) == '\r' || content(i) == '\n')) i += 1 + content.splitAt(i) // split at start of subsequent line } + (header, body) } else ("", content) def ignoreSheBangLines(content: String): (String, Boolean) = - if (content.startsWith("#!")) { - val regexMatch = sheBangRegex.findFirstMatchIn(content) - regexMatch match { - case Some(firstMatch) => - content.replace( - firstMatch.toString(), - System.lineSeparator() * firstMatch.toString().split(System.lineSeparator()).length - ) -> true - case None => (content, false) - } - } + if (content.startsWith("#!")) + val (header, body) = partitionOnShebangSection(content) + val blankHeader = "\n" * (header.split("\\R", -1).length - 1) + (blankHeader + body, true) else (content, false) diff --git a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala index b5e0b49c97..16f6ab416c 100644 --- a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala @@ -308,24 +308,31 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { |#! nix-shell -i scala-cli | |println("Hello World")""".stripMargin, - os.rel / "something4.scala" -> + os.rel / "something4.sc" -> """#!/usr/bin/scala-cli |#! nix-shell -i scala-cli | |!# | |println("Hello World")""".stripMargin, - os.rel / "something5.scala" -> + os.rel / "something5.sc" -> """#!/usr/bin/scala-cli | |println("Hello World #!")""".stripMargin, - os.rel / "multiline.scala" -> + os.rel / "multiline.sc" -> """#!/usr/bin/scala-cli |# comment |VAL=1 |!# | - |println("Hello World #!")""".stripMargin + |println("Hello World #!")""".stripMargin, + os.rel / "hasBangHashInComment.sc" -> + """#!/usr/bin/scala-cli + | + | + | + | + |println("Hello World !#")""".stripMargin ) val expectedParsedCodes = Seq( """ @@ -349,7 +356,13 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { | | | - |println("Hello World #!")""".stripMargin + |println("Hello World #!")""".stripMargin, + """ + | + | + | + | + |println("Hello World !#")""".stripMargin ) testInputs.withInputs { (root, inputs) => @@ -375,10 +388,26 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { sources.inMemory.map(_.content).map(s => new String(s, StandardCharsets.UTF_8)) parsedCodes.zip(expectedParsedCodes).foreach { case (parsedCode, expectedCode) => + showDiff(parsedCode, expectedCode) expect(parsedCode.contains(expectedCode)) } } } + def showDiff(parsed: String, expected: String): Unit = { + if (!parsed.contains(expected)) { + def c2s(c: Char): String = c match { + case '\r' => "\\r" + case '\n' => "\\n" + case s => s"$s" + } + for (((p, e), i) <- (parsed zip expected).zipWithIndex) { + val ps = c2s(p) + val es = c2s(e) + if (ps != es) + System.err.printf("%2d: [%s]!=[%s]\n", i, ps, es) + } + } + } test("dependencies in .sc - using") { val testInputs = TestInputs( diff --git a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeBlockTests.scala b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeBlockTests.scala index fc5c089508..32add4d8b5 100644 --- a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeBlockTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeBlockTests.scala @@ -66,6 +66,30 @@ class MarkdownCodeBlockTests extends TestUtil.ScalaCliBuildSuite { ) val Right(Seq(actualResult: MarkdownCodeBlock)) = MarkdownCodeBlock.findCodeBlocks(os.sub / "Example.md", markdown) + showDiffs(actualResult, expectedResult) + expect(actualResult == expectedResult) + } + + test("shebang closing line allowed in scala code") { + val code = """println("Hello !#")""".stripMargin + val markdown = + s"""# Some snippet + | + |```scala + |#!/usr/bin/env -S scala-cli shebang + |$code + |``` + |""".stripMargin + val expectedResult = + MarkdownCodeBlock( + info = PlainScalaInfo, + body = "\n" + code, + startLine = 3, + endLine = 4 + ) + val Right(Seq(actualResult: MarkdownCodeBlock)) = + MarkdownCodeBlock.findCodeBlocks(os.sub / "Example.md", markdown) + showDiffs(actualResult, expectedResult) expect(actualResult == expectedResult) } @@ -114,8 +138,24 @@ class MarkdownCodeBlockTests extends TestUtil.ScalaCliBuildSuite { ) val Right(Seq(actualResult: MarkdownCodeBlock)) = MarkdownCodeBlock.findCodeBlocks(os.sub / "Example.md", markdown) + showDiffs(actualResult, expectedResult) expect(actualResult == expectedResult) } + def showDiffs(actual: MarkdownCodeBlock, expect: MarkdownCodeBlock): Unit = { + if actual.toString != expect.toString then + for (((a, b), i) <- (actual.body zip expect.body).zipWithIndex) + if (a != b) { + def c2s(c: Char): String = c match { + case '\r' => "\\r" + case '\n' => "\\n" + case _ => s"$c" + } + val aa = c2s(a) + val bb = c2s(b) + System.err.printf("== index %d: [%s]!=[%s]\n", i, aa, bb) + } + System.err.printf("actual[%s]\nexpect[%s]\n", actual, expect) + } test("a test Scala snippet is extracted correctly from markdown") { val code = diff --git a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala index c8284580d7..760fd327d4 100644 --- a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala @@ -35,8 +35,24 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } + def showDiffs(actual: String, expect: String): Unit = { + if actual.toString != expect.toString then + for (((a, b), i) <- (actual zip expect).zipWithIndex) + if (a != b) { + def c2s(c: Char): String = c match { + case '\r' => "\\r" + case '\n' => "\\n" + case _ => s"$c" + } + val aa = c2s(a) + val bb = c2s(b) + System.err.printf("== index %d: [%s]!=[%s]\n", i, aa, bb) + } + System.err.printf("actual[%s]\nexpect[%s]\n", actual, expect) + } test("multiple plain Scala code blocks are wrapped correctly") { val snippet1 = """println("Hello")""" @@ -65,6 +81,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } @@ -95,6 +112,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |}}""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (Some(expectedScala), None, None)) } @@ -114,6 +132,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (None, Some(expectedScala), None)) } @@ -138,6 +157,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (None, Some(expectedScala), None)) } @@ -158,6 +178,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (None, None, Some(expectedScala))) } @@ -185,6 +206,7 @@ class MarkdownCodeWrapperTests extends TestUtil.ScalaCliBuildSuite { |""".stripMargin ) val result = MarkdownCodeWrapper(os.sub / "Example.md", markdown) + showDiffs(result._1.get.code, expectedScala.code) expect(result == (None, None, Some(expectedScala))) } }