From 864148d9f0ffa01e835e866a9e0803bb9ea7d037 Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Thu, 1 Sep 2022 12:23:34 -0700 Subject: [PATCH 1/3] Revert "process.Parser strips escaping backslash" This reverts commit 416536e7fdb211c0e840199e8de39fe124846399. --- src/library/scala/sys/process/Parser.scala | 97 ++++++++++++------- test/junit/scala/sys/process/ParserTest.scala | 16 +-- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/library/scala/sys/process/Parser.scala b/src/library/scala/sys/process/Parser.scala index 19b82c905091..89a2dd56db32 100644 --- a/src/library/scala/sys/process/Parser.scala +++ b/src/library/scala/sys/process/Parser.scala @@ -13,8 +13,6 @@ package scala.sys.process import scala.annotation.tailrec -import collection.mutable.ListBuffer -import Character.isWhitespace /** A simple enough command line parser using shell quote conventions. */ @@ -23,54 +21,87 @@ private[scala] object Parser { private final val SQ = '\'' private final val EOF = -1 - /** Split the line into tokens separated by whitespace. + /** Split the line into tokens separated by whitespace or quotes. * - * Tokens may be surrounded by quotes and may contain whitespace or escaped quotes. - * - * @return list of tokens + * @return either an error message or reverse list of tokens */ def tokenize(line: String, errorFn: String => Unit): List[String] = { - val accum = ListBuffer.empty[String] - val buf = new java.lang.StringBuilder - var pos = 0 + import Character.isWhitespace + import java.lang.{StringBuilder => Builder} + import collection.mutable.ArrayBuffer - def cur: Int = if (done) EOF else line.charAt(pos) - def bump() = pos += 1 - def put() = { buf.append(cur.toChar); bump() } - def done = pos >= line.length + var accum: List[String] = Nil + var pos = 0 + var start = 0 + val qpos = new ArrayBuffer[Int](16) // positions of paired quotes - def skipWhitespace() = while (isWhitespace(cur)) bump() + def cur: Int = if (done) EOF else line.charAt(pos) + def bump() = pos += 1 + def done = pos >= line.length - // Collect to end of word, handling quotes. False for missing end quote. - def word(): Boolean = { + // Skip to the next quote as given. + def skipToQuote(q: Int): Boolean = { + var escaped = false + def terminal: Boolean = cur match { + case _ if escaped => escaped = false ; false + case '\\' => escaped = true ; false + case `q` | EOF => true + case _ => false + } + while (!terminal) bump() + !done + } + // Skip to a word boundary, where words can be quoted and quotes can be escaped + def skipToDelim(): Boolean = { var escaped = false - var Q = EOF - var lastQ = 0 - def inQuote = Q != EOF - def badquote() = errorFn(s"Unmatched quote [${lastQ}](${line.charAt(lastQ)})") - def finish(): Boolean = if (!inQuote) !escaped else { badquote(); false} + def quote() = { qpos += pos ; bump() } @tailrec def advance(): Boolean = cur match { - case EOF => finish() - case _ if escaped => escaped = false; put(); advance() - case '\\' => escaped = true; bump(); advance() - case q if q == Q => Q = EOF; bump(); advance() - case q @ (DQ | SQ) if !inQuote => Q = q; lastQ = pos; bump(); advance() - case c if isWhitespace(c) && !inQuote => finish() - case _ => put(); advance() + case _ if escaped => escaped = false ; bump() ; advance() + case '\\' => escaped = true ; bump() ; advance() + case q @ (DQ | SQ) => { quote() ; skipToQuote(q) } && { quote() ; advance() } + case EOF => true + case c if isWhitespace(c) => true + case _ => bump(); advance() } advance() } + def skipWhitespace() = while (isWhitespace(cur)) bump() + def copyText() = { + val buf = new Builder + var p = start + var i = 0 + while (p < pos) { + if (i >= qpos.size) { + buf.append(line, p, pos) + p = pos + } else if (p == qpos(i)) { + buf.append(line, qpos(i)+1, qpos(i+1)) + p = qpos(i+1)+1 + i += 2 + } else { + buf.append(line, p, qpos(i)) + p = qpos(i) + } + } + buf.toString + } def text() = { - val res = buf.toString - buf.setLength(0) + val res = + if (qpos.isEmpty) line.substring(start, pos) + else if (qpos(0) == start && qpos(1) == pos) line.substring(start+1, pos-1) + else copyText() + qpos.clear() res } + def badquote() = errorFn(s"Unmatched quote [${qpos.last}](${line.charAt(qpos.last)})") + @tailrec def loop(): List[String] = { skipWhitespace() - if (done) accum.toList - else if (!word()) Nil + start = pos + if (done) accum.reverse + else if (!skipToDelim()) { badquote() ; Nil } else { - accum += text() + accum ::= text() loop() } } diff --git a/test/junit/scala/sys/process/ParserTest.scala b/test/junit/scala/sys/process/ParserTest.scala index 0417d42c50d7..5673d9068878 100644 --- a/test/junit/scala/sys/process/ParserTest.scala +++ b/test/junit/scala/sys/process/ParserTest.scala @@ -51,17 +51,9 @@ class ParserTest { @Test def `leading quote is escaped`: Unit = { check("echo", "hello, world!")("""echo "hello, world!" """) check("echo", "hello, world!")("""echo hello,' 'world! """) - check("echo", """"hello,""", """world!"""")("""echo \"hello, world!\" """) - check("""a"b"c""")("""a\"b\"c""") - check("a", "'b", "'", "c")("""a \'b \' c""") - check("a", """\b """, "c")("""a \\'b ' c""") - } - /* backslash is stripped in normal shell usage. - ➜ ~ ls \"hello world\" - ls: cannot access '"hello': No such file or directory - ls: cannot access 'world"': No such file or directory - */ - @Test def `escaped quotes lose backslash`: Unit = { - check("ls", "\"hello", "world\"")("""ls \"hello world\"""") + check("echo", """\"hello,""", """world!\"""")("""echo \"hello, world!\" """) + check("""a\"b\"c""")("""a\"b\"c""") + check("a", "\\'b", "\\'", "c")("""a \'b \' c""") + check("a", "\\\\b ", "c")("""a \\'b ' c""") } } From 362c5d1a52060374450274ff6bcd8773af8ddd07 Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Thu, 1 Sep 2022 12:24:00 -0700 Subject: [PATCH 2/3] Revert "Trim and filter empties in arg files" This reverts commit fbd722326d509e95f37f929aa423b528cb58b0cb. --- src/compiler/scala/tools/nsc/CompilerCommand.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/scala/tools/nsc/CompilerCommand.scala b/src/compiler/scala/tools/nsc/CompilerCommand.scala index b2f027de5b24..ca931b7d933b 100644 --- a/src/compiler/scala/tools/nsc/CompilerCommand.scala +++ b/src/compiler/scala/tools/nsc/CompilerCommand.scala @@ -126,11 +126,11 @@ class CompilerCommand(arguments: List[String], val settings: Settings) { def expandArg(arg: String): List[String] = { import java.nio.file.{Files, Paths} import scala.jdk.CollectionConverters._ - def stripComment(s: String) = s.takeWhile(_ != '#').trim() + def stripComment(s: String) = s.takeWhile(_ != '#') val file = Paths.get(arg.stripPrefix("@")) if (!Files.exists(file)) throw new java.io.FileNotFoundException(s"argument file $file could not be found") - Files.readAllLines(file).asScala.map(stripComment).filter(_ != "").toList + Files.readAllLines(file).asScala.map(stripComment).toList } // override this if you don't want arguments processed here From e5fe9193d520f8e611244101e95a85a51067f79d Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Thu, 1 Sep 2022 12:24:11 -0700 Subject: [PATCH 3/3] Revert "Args files are 1 arg per line, fix -Vprint-args -" This reverts commit b433e3cdf8fac87212cdd1eb43cf2d130945a701. --- src/compiler/scala/tools/nsc/CompilerCommand.scala | 4 ++-- src/compiler/scala/tools/nsc/settings/MutableSettings.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compiler/scala/tools/nsc/CompilerCommand.scala b/src/compiler/scala/tools/nsc/CompilerCommand.scala index ca931b7d933b..b895d4438c63 100644 --- a/src/compiler/scala/tools/nsc/CompilerCommand.scala +++ b/src/compiler/scala/tools/nsc/CompilerCommand.scala @@ -127,10 +127,10 @@ class CompilerCommand(arguments: List[String], val settings: Settings) { import java.nio.file.{Files, Paths} import scala.jdk.CollectionConverters._ def stripComment(s: String) = s.takeWhile(_ != '#') - val file = Paths.get(arg.stripPrefix("@")) + val file = Paths.get(arg stripPrefix "@") if (!Files.exists(file)) throw new java.io.FileNotFoundException(s"argument file $file could not be found") - Files.readAllLines(file).asScala.map(stripComment).toList + settings.splitParams(Files.readAllLines(file).asScala.map(stripComment).mkString(" ")) } // override this if you don't want arguments processed here diff --git a/src/compiler/scala/tools/nsc/settings/MutableSettings.scala b/src/compiler/scala/tools/nsc/settings/MutableSettings.scala index 314b5393138f..49e4a8f405b5 100644 --- a/src/compiler/scala/tools/nsc/settings/MutableSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/MutableSettings.scala @@ -973,5 +973,5 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory) } private object Optionlike { - def unapply(s: String): Boolean = s.startsWith("-") && s != "-" + def unapply(s: String): Boolean = s.startsWith("-") }