diff --git a/src/compiler/scala/tools/nsc/CompilerCommand.scala b/src/compiler/scala/tools/nsc/CompilerCommand.scala index b2f027de5b24..b895d4438c63 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() - val file = Paths.get(arg.stripPrefix("@")) + 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 + 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("-") } 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""") } }