Skip to content

Commit 416536e

Browse files
committed
process.Parser strips escaping backslash
`"""echo \"hello world\" """.!!` should behave like `echo \"hello world\"` in the shell, with result `"hello world"`. The two arguments are `"hello` and `world"`.
1 parent 3faf9a9 commit 416536e

2 files changed

Lines changed: 45 additions & 68 deletions

File tree

src/library/scala/sys/process/Parser.scala

Lines changed: 33 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
package scala.sys.process
1414

1515
import scala.annotation.tailrec
16+
import collection.mutable.ListBuffer
17+
import Character.isWhitespace
1618

1719
/** A simple enough command line parser using shell quote conventions.
1820
*/
@@ -21,87 +23,54 @@ private[scala] object Parser {
2123
private final val SQ = '\''
2224
private final val EOF = -1
2325

24-
/** Split the line into tokens separated by whitespace or quotes.
26+
/** Split the line into tokens separated by whitespace.
2527
*
26-
* @return either an error message or reverse list of tokens
28+
* Tokens may be surrounded by quotes and may contain whitespace or escaped quotes.
29+
*
30+
* @return list of tokens
2731
*/
2832
def tokenize(line: String, errorFn: String => Unit): List[String] = {
29-
import Character.isWhitespace
30-
import java.lang.{StringBuilder => Builder}
31-
import collection.mutable.ArrayBuffer
33+
val accum = ListBuffer.empty[String]
34+
val buf = new java.lang.StringBuilder
35+
var pos = 0
3236

33-
var accum: List[String] = Nil
34-
var pos = 0
35-
var start = 0
36-
val qpos = new ArrayBuffer[Int](16) // positions of paired quotes
37+
def cur: Int = if (done) EOF else line.charAt(pos)
38+
def bump() = pos += 1
39+
def put() = { buf.append(cur.toChar); bump() }
40+
def done = pos >= line.length
3741

38-
def cur: Int = if (done) EOF else line.charAt(pos)
39-
def bump() = pos += 1
40-
def done = pos >= line.length
42+
def skipWhitespace() = while (isWhitespace(cur)) bump()
4143

42-
// Skip to the next quote as given.
43-
def skipToQuote(q: Int): Boolean = {
44-
var escaped = false
45-
def terminal: Boolean = cur match {
46-
case _ if escaped => escaped = false ; false
47-
case '\\' => escaped = true ; false
48-
case `q` | EOF => true
49-
case _ => false
50-
}
51-
while (!terminal) bump()
52-
!done
53-
}
54-
// Skip to a word boundary, where words can be quoted and quotes can be escaped
55-
def skipToDelim(): Boolean = {
44+
// Collect to end of word, handling quotes. False for missing end quote.
45+
def word(): Boolean = {
5646
var escaped = false
57-
def quote() = { qpos += pos ; bump() }
47+
var Q = EOF
48+
var lastQ = 0
49+
def inQuote = Q != EOF
50+
def badquote() = errorFn(s"Unmatched quote [${lastQ}](${line.charAt(lastQ)})")
51+
def finish(): Boolean = if (!inQuote) !escaped else { badquote(); false}
5852
@tailrec def advance(): Boolean = cur match {
59-
case _ if escaped => escaped = false ; bump() ; advance()
60-
case '\\' => escaped = true ; bump() ; advance()
61-
case q @ (DQ | SQ) => { quote() ; skipToQuote(q) } && { quote() ; advance() }
62-
case EOF => true
63-
case c if isWhitespace(c) => true
64-
case _ => bump(); advance()
53+
case EOF => finish()
54+
case _ if escaped => escaped = false; put(); advance()
55+
case '\\' => escaped = true; bump(); advance()
56+
case q if q == Q => Q = EOF; bump(); advance()
57+
case q @ (DQ | SQ) if !inQuote => Q = q; lastQ = pos; bump(); advance()
58+
case c if isWhitespace(c) && !inQuote => finish()
59+
case _ => put(); advance()
6560
}
6661
advance()
6762
}
68-
def skipWhitespace() = while (isWhitespace(cur)) bump()
69-
def copyText() = {
70-
val buf = new Builder
71-
var p = start
72-
var i = 0
73-
while (p < pos) {
74-
if (i >= qpos.size) {
75-
buf.append(line, p, pos)
76-
p = pos
77-
} else if (p == qpos(i)) {
78-
buf.append(line, qpos(i)+1, qpos(i+1))
79-
p = qpos(i+1)+1
80-
i += 2
81-
} else {
82-
buf.append(line, p, qpos(i))
83-
p = qpos(i)
84-
}
85-
}
86-
buf.toString
87-
}
8863
def text() = {
89-
val res =
90-
if (qpos.isEmpty) line.substring(start, pos)
91-
else if (qpos(0) == start && qpos(1) == pos) line.substring(start+1, pos-1)
92-
else copyText()
93-
qpos.clear()
64+
val res = buf.toString
65+
buf.setLength(0)
9466
res
9567
}
96-
def badquote() = errorFn(s"Unmatched quote [${qpos.last}](${line.charAt(qpos.last)})")
97-
9868
@tailrec def loop(): List[String] = {
9969
skipWhitespace()
100-
start = pos
101-
if (done) accum.reverse
102-
else if (!skipToDelim()) { badquote() ; Nil }
70+
if (done) accum.toList
71+
else if (!word()) Nil
10372
else {
104-
accum ::= text()
73+
accum += text()
10574
loop()
10675
}
10776
}

test/junit/scala/sys/process/ParserTest.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,17 @@ class ParserTest {
5151
@Test def `leading quote is escaped`: Unit = {
5252
check("echo", "hello, world!")("""echo "hello, world!" """)
5353
check("echo", "hello, world!")("""echo hello,' 'world! """)
54-
check("echo", """\"hello,""", """world!\"""")("""echo \"hello, world!\" """)
55-
check("""a\"b\"c""")("""a\"b\"c""")
56-
check("a", "\\'b", "\\'", "c")("""a \'b \' c""")
57-
check("a", "\\\\b ", "c")("""a \\'b ' c""")
54+
check("echo", """"hello,""", """world!"""")("""echo \"hello, world!\" """)
55+
check("""a"b"c""")("""a\"b\"c""")
56+
check("a", "'b", "'", "c")("""a \'b \' c""")
57+
check("a", """\b """, "c")("""a \\'b ' c""")
58+
}
59+
/* backslash is stripped in normal shell usage.
60+
➜ ~ ls \"hello world\"
61+
ls: cannot access '"hello': No such file or directory
62+
ls: cannot access 'world"': No such file or directory
63+
*/
64+
@Test def `escaped quotes lose backslash`: Unit = {
65+
check("ls", "\"hello", "world\"")("""ls \"hello world\"""")
5866
}
5967
}

0 commit comments

Comments
 (0)