13
13
struct CommandParser {
14
14
private var input : ByteScanner
15
15
private var knownCommand : KnownCommand ?
16
+ private var stopAtShellOperator = false
16
17
17
18
private init ( _ input: UnsafeBufferPointer < UInt8 > ) {
18
19
self . input = ByteScanner ( input)
@@ -26,6 +27,8 @@ struct CommandParser {
26
27
}
27
28
}
28
29
30
+ /// Parse an arbitrary shell command, returning the first single invocation
31
+ /// of a known command.
29
32
static func parseKnownCommandOnly( _ input: String ) throws -> Command ? {
30
33
var input = input
31
34
return try input. withUTF8 { bytes in
@@ -35,6 +38,9 @@ struct CommandParser {
35
38
) else {
36
39
return nil
37
40
}
41
+ // We're parsing an arbitrary shell command so stop if we hit a shell
42
+ // operator like '&&'
43
+ parser. stopAtShellOperator = true
38
44
return Command ( executable: executable, args: try parser. consumeArguments ( ) )
39
45
}
40
46
}
@@ -62,7 +68,7 @@ struct CommandParser {
62
68
) throws -> AnyPath ? {
63
69
var executable : AnyPath
64
70
repeat {
65
- guard let executableUTF8 = try input . consumeElement ( ) else {
71
+ guard let executableUTF8 = try consumeElement ( ) else {
66
72
return nil
67
73
}
68
74
executable = AnyPath ( String ( utf8: executableUTF8) )
@@ -119,17 +125,27 @@ fileprivate extension ByteScanner.Consumer {
119
125
}
120
126
}
121
127
122
- fileprivate extension ByteScanner {
123
- mutating func consumeElement( ) throws -> Bytes ? {
128
+ extension CommandParser {
129
+ mutating func consumeElement( ) throws -> ByteScanner . Bytes ? {
124
130
// Eat any leading whitespace.
125
- skip ( while: \. isSpaceOrTab)
131
+ input . skip ( while: \. isSpaceOrTab)
126
132
127
133
// If we're now at the end of the input, nothing can be parsed.
128
- guard hasInput else { return nil }
129
-
130
- // Consume the element, stopping at the first space.
131
- return try consume ( using: { consumer in
132
- switch consumer. peek {
134
+ guard input. hasInput else { return nil }
135
+
136
+ // Consume the element, stopping at the first space or shell operator.
137
+ let start = input. cursor
138
+ let elt = try input. consume ( using: { consumer in
139
+ guard let char = consumer. peek else { return false }
140
+ if stopAtShellOperator {
141
+ switch char {
142
+ case " < " , " > " , " ( " , " ) " , " | " , " & " , " ; " :
143
+ return false
144
+ default :
145
+ break
146
+ }
147
+ }
148
+ switch char {
133
149
case \. isSpaceOrTab:
134
150
return false
135
151
case " \" " :
@@ -139,6 +155,9 @@ fileprivate extension ByteScanner {
139
155
return consumer. consumeUnescaped ( )
140
156
}
141
157
} )
158
+ // Note that we may have an empty element while still moving the cursor
159
+ // for e.g '-I ""', which is an option with an empty value.
160
+ return start != input. cursor ? elt : nil
142
161
}
143
162
}
144
163
@@ -167,7 +186,7 @@ extension CommandParser {
167
186
return makeOption ( spacing: . unspaced, String ( utf8: option. remaining) )
168
187
}
169
188
if spacing. contains ( . spaced) , !option. hasInput,
170
- let value = try input . consumeElement ( ) {
189
+ let value = try consumeElement ( ) {
171
190
return makeOption ( spacing: . spaced, String ( utf8: value) )
172
191
}
173
192
return option. empty ? . flag( flag) : nil
@@ -188,7 +207,7 @@ extension CommandParser {
188
207
}
189
208
190
209
mutating func consumeArgument( ) throws -> Command . Argument ? {
191
- guard let element = try input . consumeElement ( ) else { return nil }
210
+ guard let element = try consumeElement ( ) else { return nil }
192
211
return try element. withUnsafeBytes { bytes in
193
212
var option = ByteScanner ( bytes)
194
213
var numDashes = 0
0 commit comments