Skip to content

Commit 8503ce0

Browse files
committed
[xcodegen] Stop at shell operator in parseKnownCommandOnly
We're dealing with a potentially arbitary shell command here, so make sure we don't continue parsing after a shell operator such as `&&`.
1 parent 71d367a commit 8503ce0

File tree

2 files changed

+55
-11
lines changed

2 files changed

+55
-11
lines changed

utils/swift-xcodegen/Sources/SwiftXcodeGen/Command/CommandParser.swift

+30-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
struct 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

utils/swift-xcodegen/Tests/SwiftXcodeGenTest/CompileCommandsTests.swift

+25
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ class CompileCommandsTests: XCTestCase {
5454
knownCommandOnly: true
5555
)
5656

57+
for op in ["&&", "||", ">", "<", ">>", ";", "(", ")"] {
58+
assertParse(
59+
"x y x/y/clang -DX -I \(op) ignored",
60+
executable: "x/y/clang",
61+
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)],
62+
knownCommandOnly: true
63+
)
64+
assertParse(
65+
"x y x/y/clang -DX -I x\(op) ignored",
66+
executable: "x/y/clang",
67+
args: [
68+
.option(.D, spacing: .unspaced, value: "X"),
69+
.option(.I, spacing: .spaced, value: "x")
70+
],
71+
knownCommandOnly: true
72+
)
73+
}
74+
75+
assertParse(
76+
#"x/y/clang \< x\< "<""#,
77+
executable: "x/y/clang",
78+
args: [.value("<"), .value("x<"), .value("<")],
79+
knownCommandOnly: true
80+
)
81+
5782
assertParse(
5883
"clang -DX -I",
5984
args: [.option(.D, spacing: .unspaced, value: "X"), .flag(.I)]

0 commit comments

Comments
 (0)