1313struct CommandParser {
1414 private var input : ByteScanner
1515 private var knownCommand : KnownCommand ?
16+ private var stopAtShellOperator = false
1617
1718 private init ( _ input: UnsafeBufferPointer < UInt8 > ) {
1819 self . input = ByteScanner ( input)
@@ -26,6 +27,8 @@ struct CommandParser {
2627 }
2728 }
2829
30+ /// Parse an arbitrary shell command, returning the first single invocation
31+ /// of a known command.
2932 static func parseKnownCommandOnly( _ input: String ) throws -> Command ? {
3033 var input = input
3134 return try input. withUTF8 { bytes in
@@ -35,6 +38,9 @@ struct CommandParser {
3538 ) else {
3639 return nil
3740 }
41+ // We're parsing an arbitrary shell command so stop if we hit a shell
42+ // operator like '&&'
43+ parser. stopAtShellOperator = true
3844 return Command ( executable: executable, args: try parser. consumeArguments ( ) )
3945 }
4046 }
@@ -62,7 +68,7 @@ struct CommandParser {
6268 ) throws -> AnyPath ? {
6369 var executable : AnyPath
6470 repeat {
65- guard let executableUTF8 = try input . consumeElement ( ) else {
71+ guard let executableUTF8 = try consumeElement ( ) else {
6672 return nil
6773 }
6874 executable = AnyPath ( String ( utf8: executableUTF8) )
@@ -119,17 +125,27 @@ fileprivate extension ByteScanner.Consumer {
119125 }
120126}
121127
122- fileprivate extension ByteScanner {
123- mutating func consumeElement( ) throws -> Bytes ? {
128+ extension CommandParser {
129+ mutating func consumeElement( ) throws -> ByteScanner . Bytes ? {
124130 // Eat any leading whitespace.
125- skip ( while: \. isSpaceOrTab)
131+ input . skip ( while: \. isSpaceOrTab)
126132
127133 // 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 {
133149 case \. isSpaceOrTab:
134150 return false
135151 case " \" " :
@@ -139,6 +155,9 @@ fileprivate extension ByteScanner {
139155 return consumer. consumeUnescaped ( )
140156 }
141157 } )
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
142161 }
143162}
144163
@@ -167,7 +186,7 @@ extension CommandParser {
167186 return makeOption ( spacing: . unspaced, String ( utf8: option. remaining) )
168187 }
169188 if spacing. contains ( . spaced) , !option. hasInput,
170- let value = try input . consumeElement ( ) {
189+ let value = try consumeElement ( ) {
171190 return makeOption ( spacing: . spaced, String ( utf8: value) )
172191 }
173192 return option. empty ? . flag( flag) : nil
@@ -188,7 +207,7 @@ extension CommandParser {
188207 }
189208
190209 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 }
192211 return try element. withUnsafeBytes { bytes in
193212 var option = ByteScanner ( bytes)
194213 var numDashes = 0
0 commit comments