1
1
package dotty .tools .dotc .config
2
2
3
- import scala .annotation .tailrec
4
- import scala .collection .mutable .ArrayBuffer
5
3
import java .lang .Character .isWhitespace
6
4
import java .nio .file .{Files , Paths }
7
- import scala .jdk .CollectionConverters ._
5
+ import scala .annotation .tailrec
6
+ import scala .collection .mutable .ArrayBuffer
7
+ import scala .jdk .CollectionConverters .*
8
8
9
- /** A simple enough command line parser .
9
+ /** Split a line of text using shell conventions .
10
10
*/
11
11
object CommandLineParser :
12
12
inline private val DQ = '"'
13
13
inline private val SQ = '\' '
14
14
inline private val EOF = - 1
15
15
16
- /** Split the line into tokens separated by whitespace or quotes.
16
+ /** Split the line into tokens separated by whitespace.
17
+ *
18
+ * Single or double quotes can be embedded to preserve internal whitespace:
17
19
*
18
- * Invoke `errorFn` with message on bad quote.
20
+ * `""" echo "hello, world!" """` => "echo" :: "hello, world!" :: Nil
21
+ * `""" echo hello,' 'world! """` => "echo" :: "hello, world!" :: Nil
22
+ * `""" echo \"hello, world!\" """` => "echo" :: "\"hello," :: "world!\"" :: Nil
23
+ *
24
+ * The embedded quotes are stripped. Escaping backslash is not stripped.
25
+ *
26
+ * Invoke `errorFn` with a descriptive message if an end quote is missing.
19
27
*/
20
28
def tokenize (line : String , errorFn : String => Unit ): List [String ] =
21
29
22
30
var accum : List [String ] = Nil
23
31
24
32
var pos = 0
25
33
var start = 0
26
- val qpos = new ArrayBuffer [Int ](16 ) // positions of paired quotes
34
+ val qpos = new ArrayBuffer [Int ](16 ) // positions of paired quotes in current token
27
35
28
36
inline def cur = if done then EOF else line.charAt(pos): Int
29
37
inline def bump () = pos += 1
30
38
inline def done = pos >= line.length
31
39
32
- def skipToQuote (q : Int ): Boolean =
40
+ // Skip to the given unescaped end quote; false on no more input.
41
+ def skipToEndQuote (q : Int ): Boolean =
33
42
var escaped = false
34
43
def terminal = cur match
35
44
case _ if escaped => escaped = false ; false
@@ -39,13 +48,18 @@ object CommandLineParser:
39
48
while ! terminal do bump()
40
49
! done
41
50
42
- @ tailrec def skipToDelim (): Boolean =
51
+ // Skip to the next whitespace word boundary; record unescaped embedded quotes; false on missing quote.
52
+ def skipToDelim (): Boolean =
53
+ var escaped = false
43
54
inline def quote () = { qpos += pos ; bump() }
44
- cur match
45
- case q @ (DQ | SQ ) => { quote() ; skipToQuote(q) } && { quote() ; skipToDelim() }
46
- case - 1 => true
55
+ @ tailrec def advance (): Boolean = cur match
56
+ case _ if escaped => escaped = false ; bump() ; advance()
57
+ case '\\ ' => escaped = true ; bump() ; advance()
58
+ case q @ (DQ | SQ ) => { quote() ; skipToEndQuote(q) } && { quote() ; advance() }
59
+ case EOF => true
47
60
case c if isWhitespace(c) => true
48
- case _ => bump(); skipToDelim()
61
+ case _ => bump(); advance()
62
+ advance()
49
63
50
64
def copyText (): String =
51
65
val buf = new java.lang.StringBuilder
@@ -64,6 +78,7 @@ object CommandLineParser:
64
78
p = qpos(i)
65
79
buf.toString
66
80
81
+ // the current token, stripped of any embedded quotes.
67
82
def text (): String =
68
83
val res =
69
84
if qpos.isEmpty then line.substring(start, pos)
@@ -74,7 +89,7 @@ object CommandLineParser:
74
89
75
90
inline def badquote () = errorFn(s " Unmatched quote [ ${qpos.last}]( ${line.charAt(qpos.last)}) " )
76
91
77
- inline def skipWhitespace () = while isWhitespace(cur) do pos += 1
92
+ inline def skipWhitespace () = while isWhitespace(cur) do bump()
78
93
79
94
@ tailrec def loop (): List [String ] =
80
95
skipWhitespace()
@@ -85,12 +100,11 @@ object CommandLineParser:
85
100
badquote()
86
101
Nil
87
102
else
88
- accum = text() :: accum
103
+ accum :: = text()
89
104
loop()
90
105
end loop
91
106
92
107
loop()
93
-
94
108
end tokenize
95
109
96
110
def tokenize (line : String ): List [String ] = tokenize(line, x => throw new ParseException (x))
0 commit comments