13
13
package scala .sys .process
14
14
15
15
import scala .annotation .tailrec
16
- import collection .mutable .ListBuffer
17
- import Character .isWhitespace
18
16
19
17
/** A simple enough command line parser using shell quote conventions.
20
18
*/
@@ -23,54 +21,87 @@ private[scala] object Parser {
23
21
private final val SQ = '\' '
24
22
private final val EOF = - 1
25
23
26
- /** Split the line into tokens separated by whitespace.
24
+ /** Split the line into tokens separated by whitespace or quotes .
27
25
*
28
- * Tokens may be surrounded by quotes and may contain whitespace or escaped quotes.
29
- *
30
- * @return list of tokens
26
+ * @return either an error message or reverse list of tokens
31
27
*/
32
28
def tokenize (line : String , errorFn : String => Unit ): List [String ] = {
33
- val accum = ListBuffer .empty[ String ]
34
- val buf = new java.lang.StringBuilder
35
- var pos = 0
29
+ import Character . isWhitespace
30
+ import java .lang .{ StringBuilder => Builder }
31
+ import collection . mutable . ArrayBuffer
36
32
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
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
41
37
42
- def skipWhitespace () = while (isWhitespace(cur)) bump()
38
+ def cur : Int = if (done) EOF else line.charAt(pos)
39
+ def bump () = pos += 1
40
+ def done = pos >= line.length
43
41
44
- // Collect to end of word, handling quotes. False for missing end quote.
45
- def word (): Boolean = {
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 = {
46
56
var escaped = false
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 }
57
+ def quote () = { qpos += pos ; bump() }
52
58
@ tailrec def advance (): Boolean = cur match {
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()
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()
60
65
}
61
66
advance()
62
67
}
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
+ }
63
88
def text () = {
64
- val res = buf.toString
65
- buf.setLength(0 )
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()
66
94
res
67
95
}
96
+ def badquote () = errorFn(s " Unmatched quote [ ${qpos.last}]( ${line.charAt(qpos.last)}) " )
97
+
68
98
@ tailrec def loop (): List [String ] = {
69
99
skipWhitespace()
70
- if (done) accum.toList
71
- else if (! word()) Nil
100
+ start = pos
101
+ if (done) accum.reverse
102
+ else if (! skipToDelim()) { badquote() ; Nil }
72
103
else {
73
- accum + = text()
104
+ accum :: = text()
74
105
loop()
75
106
}
76
107
}
0 commit comments