@@ -2,67 +2,100 @@ package dotty.tools.dotc
2
2
package config
3
3
4
4
import scala .annotation .tailrec
5
- import scala .annotation .internal .sharable
6
5
7
- /** A simple (overly so) command line parser.
8
- * !!! This needs a thorough test suite to make sure quoting is
9
- * done correctly and portably.
6
+ /** A simple enough command line parser.
10
7
*/
11
8
object CommandLineParser {
12
- // splits a string into a quoted prefix and the rest of the string,
13
- // taking escaping into account (using \)
14
- // `"abc"def` will match as `DoubleQuoted(abc, def)`
15
- private class QuotedExtractor (quote : Char ) {
16
- def unapply (in : String ): Option [(String , String )] = {
17
- val del = quote.toString
18
- if (in startsWith del) {
19
- var escaped = false
20
- val (quoted, next) = (in substring 1 ) span {
21
- case `quote` if ! escaped => false
22
- case '\\ ' if ! escaped => escaped = true ; true
23
- case _ => escaped = false ; true
9
+ private final val DQ = '"'
10
+ private final val SQ = '\' '
11
+
12
+ /** Split the line into tokens separated by whitespace or quotes.
13
+ *
14
+ * @return either an error message or reverse list of tokens
15
+ */
16
+ private def tokens (in : String ) = {
17
+ import Character .isWhitespace
18
+ import java .lang .{StringBuilder => Builder }
19
+ import collection .mutable .ArrayBuffer
20
+
21
+ var accum : List [String ] = Nil
22
+ var pos = 0
23
+ var start = 0
24
+ val qpos = new ArrayBuffer [Int ](16 ) // positions of paired quotes
25
+
26
+ def cur : Int = if (done) - 1 else in.charAt(pos)
27
+ def bump () = pos += 1
28
+ def done = pos >= in.length
29
+
30
+ def skipToQuote (q : Int ) = {
31
+ var escaped = false
32
+ def terminal = in.charAt(pos) match {
33
+ case _ if escaped => escaped = false ; false
34
+ case '\\ ' => escaped = true ; false
35
+ case `q` => true
36
+ case _ => false
37
+ }
38
+ while (! done && ! terminal) pos += 1
39
+ ! done
40
+ }
41
+ @ tailrec
42
+ def skipToDelim (): Boolean =
43
+ cur match {
44
+ case q @ (DQ | SQ ) => { qpos += pos; bump(); skipToQuote(q) } && { qpos += pos; bump(); skipToDelim() }
45
+ case - 1 => true
46
+ case c if isWhitespace(c) => true
47
+ case _ => bump(); skipToDelim()
48
+ }
49
+ def skipWhitespace () = while (isWhitespace(cur)) pos += 1
50
+ def copyText () = {
51
+ val buf = new Builder
52
+ var p = start
53
+ var i = 0
54
+ while (p < pos) {
55
+ if (i >= qpos.size) {
56
+ buf.append(in, p, pos)
57
+ p = pos
58
+ } else if (p == qpos(i)) {
59
+ buf.append(in, qpos(i)+ 1 , qpos(i+ 1 ))
60
+ p = qpos(i+ 1 )+ 1
61
+ i += 2
62
+ } else {
63
+ buf.append(in, p, qpos(i))
64
+ p = qpos(i)
24
65
}
25
- // the only way to get out of the above loop is with an empty next or !escaped
26
- // require(next.isEmpty || !escaped)
27
- if (next startsWith del) Some ((quoted, next substring 1 ))
28
- else None
29
66
}
30
- else None
67
+ buf.toString
31
68
}
32
- }
33
- private object DoubleQuoted extends QuotedExtractor ('"' )
34
- private object SingleQuoted extends QuotedExtractor ('\' ' )
35
- @ sharable private val Word = """ (\S+)(.*)""" .r
36
-
37
- // parse `in` for an argument, return it and the remainder of the input (or an error message)
38
- // (argument may be in single/double quotes, taking escaping into account, quotes are stripped)
39
- private def argument (in : String ): Either [String , (String , String )] = in match {
40
- case DoubleQuoted (arg, rest) => Right ((arg, rest))
41
- case SingleQuoted (arg, rest) => Right ((arg, rest))
42
- case Word (arg, rest) => Right ((arg, rest))
43
- case _ => Left (s " Illegal argument: $in" )
44
- }
69
+ def text () = {
70
+ val res =
71
+ if (qpos.isEmpty) in.substring(start, pos)
72
+ else if (qpos(0 ) == start && qpos(1 ) == pos) in.substring(start+ 1 , pos- 1 )
73
+ else copyText()
74
+ qpos.clear()
75
+ res
76
+ }
77
+ def badquote = Left (" Unmatched quote" )
45
78
46
- // parse a list of whitespace-separated arguments (ignoring whitespace in quoted arguments)
47
- @ tailrec private def commandLine (in : String , accum : List [String ] = Nil ): Either [String , (List [String ], String )] = {
48
- val trimmed = in.trim
49
- if (trimmed.isEmpty) Right ((accum.reverse, " " ))
50
- else argument(trimmed) match {
51
- case Right ((arg, next)) =>
52
- (next span Character .isWhitespace) match {
53
- case (" " , rest) if rest.nonEmpty => Left (" Arguments should be separated by whitespace." ) // TODO: can this happen?
54
- case (ws, rest) => commandLine(rest, arg :: accum)
55
- }
56
- case Left (msg) => Left (msg)
79
+ @ tailrec def loop (): Either [String , List [String ]] = {
80
+ skipWhitespace()
81
+ start = pos
82
+ if (done) Right (accum)
83
+ else if (! skipToDelim()) badquote
84
+ else {
85
+ accum = text() :: accum
86
+ loop()
87
+ }
57
88
}
89
+ loop()
58
90
}
59
91
60
92
class ParseException (msg : String ) extends RuntimeException (msg)
61
93
62
- def tokenize (line : String ): List [String ] = tokenize(line, x => throw new ParseException (x))
63
94
def tokenize (line : String , errorFn : String => Unit ): List [String ] =
64
- commandLine (line) match {
65
- case Right (( args, _)) => args
66
- case Left (msg) => errorFn(msg) ; Nil
95
+ tokens (line) match {
96
+ case Right (args) => args.reverse
97
+ case Left (msg) => errorFn(msg) ; Nil
67
98
}
99
+
100
+ def tokenize (line : String ): List [String ] = tokenize(line, x => throw new ParseException (x))
68
101
}
0 commit comments