Skip to content

Commit 59f5dc3

Browse files
authored
Merge pull request #16 from JhonnyBillM/tokenize-response-files
[Driver] - Tokenization for response files
2 parents fe68e4e + df12b4e commit 59f5dc3

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,26 +297,70 @@ public struct Driver {
297297
}
298298
}
299299

300-
// Response files.
300+
// MARK: - Response files.
301301
extension Driver {
302-
/// Tokenize a single line in a response file
303-
private static func tokenizeResponseFileLine(_ line: String) -> String {
304-
// FIXME: This is wrong. We need to do proper shell escaping.
305-
return line.replacingOccurrences(of: "\\ ", with: " ")
302+
/// Tokenize a single line in a response file.
303+
///
304+
/// This method supports response files with:
305+
/// 1. Double slash comments at the beginning of a line.
306+
/// 2. Backslash escaping.
307+
/// 3. Space character (U+0020 SPACE).
308+
///
309+
/// - Returns: One line String ready to be used in the shell, if any.
310+
///
311+
/// - Complexity: O(*n*), where *n* is the length of the line.
312+
private static func tokenizeResponseFileLine<S: StringProtocol>(_ line: S) -> String? {
313+
if line.isEmpty { return nil }
314+
315+
// Support double dash comments only if they start at the beginning of a line.
316+
if line.hasPrefix("//") { return nil }
317+
318+
var result: String = ""
319+
/// Indicates if we just parsed an escaping backslash.
320+
var isEscaping = false
321+
322+
for char in line {
323+
if char.isNewline { return result }
324+
325+
// Backslash escapes to the next character.
326+
if char == #"\"#, !isEscaping {
327+
isEscaping = true
328+
continue
329+
} else if isEscaping {
330+
// Disable escaping and keep parsing.
331+
isEscaping = false
332+
}
333+
334+
// Ignore spacing characters, except by the space character.
335+
if char.isWhitespace && char != " " { continue }
336+
337+
result.append(char)
338+
}
339+
return result.isEmpty ? nil : result
306340
}
307341

342+
/// Tokenize each line of the response file, omitting empty lines.
343+
///
344+
/// - Parameter content: response file's content to be tokenized.
345+
private static func tokenizeResponseFile(_ content: String) -> [String] {
346+
return content
347+
.split(separator: "\n")
348+
.compactMap { tokenizeResponseFileLine($0) }
349+
}
350+
351+
/// Recursively expands the response files.
352+
/// - Parameter visitedResponseFiles: Set containing visited response files to detect recursive parsing.
308353
private static func expandResponseFiles(
309354
_ args: [String],
310355
diagnosticsEngine: DiagnosticsEngine,
311356
visitedResponseFiles: inout Set<AbsolutePath>
312357
) throws -> [String] {
313-
// FIXME: This is very very prelimary. Need to look at how Swift compiler expands response file.
314-
315358
var result: [String] = []
316359

317360
// Go through each arg and add arguments from response files.
318361
for arg in args {
319362
if arg.first == "@", let responseFile = try? AbsolutePath(validating: String(arg.dropFirst())) {
363+
// Guard against infinite parsing loop.
320364
guard visitedResponseFiles.insert(responseFile).inserted else {
321365
diagnosticsEngine.emit(.warn_recursive_response_file(responseFile))
322366
continue
@@ -326,7 +370,7 @@ extension Driver {
326370
}
327371

328372
let contents = try localFileSystem.readFileContents(responseFile).cString
329-
let lines = contents.split(separator: "\n", omittingEmptySubsequences: true).map { tokenizeResponseFileLine(String($0)) }
373+
let lines = tokenizeResponseFile(contents)
330374
result.append(contentsOf: try expandResponseFiles(lines, diagnosticsEngine: diagnosticsEngine, visitedResponseFiles: &visitedResponseFiles))
331375
} else {
332376
result.append(arg)

Tests/SwiftDriverTests/SwiftDriverTests.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,50 @@ final class SwiftDriverTests: XCTestCase {
335335
XCTAssert(diags.diagnostics.first!.description.contains("is recursively expanded"))
336336
}
337337
}
338-
338+
339+
/// Tests how response files tokens such as spaces, comments, escaping characters and quotes, get parsed and expanded.
340+
func testResponseFileTokenization() throws {
341+
try withTemporaryDirectory { path in
342+
let diags = DiagnosticsEngine()
343+
let fooPath = path.appending(component: "foo.rsp")
344+
let barPath = path.appending(component: "bar.rsp")
345+
let escapingPath = path.appending(component: "escaping.rsp")
346+
347+
try localFileSystem.writeFileContents(fooPath) {
348+
$0 <<< #"""
349+
Command1 --kkc
350+
//This is a comment
351+
// this is another comment
352+
but this is \\\\\a command
353+
@\#(barPath.pathString)
354+
@YouAren'tAFile
355+
"""#
356+
}
357+
358+
try localFileSystem.writeFileContents(barPath) {
359+
$0 <<< #"""
360+
swift
361+
"rocks!"
362+
compiler
363+
-Xlinker
364+
365+
@loader_path
366+
mkdir "Quoted Dir"
367+
cd Unquoted \\Dir
368+
// Bye!
369+
"""#
370+
}
371+
372+
try localFileSystem.writeFileContents(escapingPath) {
373+
$0 <<< "swift\n--driver-mode=swift\tc\n-v\r\n//comment\n\"the end\""
374+
}
375+
let args = try Driver.expandResponseFiles(["@" + fooPath.pathString], diagnosticsEngine: diags)
376+
XCTAssertEqual(args, [#"Command1 --kkc"#, #"but this is \\a command"#, #"swift"#, #""rocks!""# ,#"compiler"#, #"-Xlinker"#, #"@loader_path"#, #"mkdir "Quoted Dir""#, #"cd Unquoted \Dir"#, #"@YouAren'tAFile"#])
377+
let escapingArgs = try Driver.expandResponseFiles(["@" + escapingPath.pathString], diagnosticsEngine: diags)
378+
XCTAssertEqual(escapingArgs, ["swift", "--driver-mode=swiftc", "-v","\"the end\""])
379+
}
380+
}
381+
339382
func testLinking() throws {
340383
let commonArgs = ["swiftc", "foo.swift", "bar.swift", "-module-name", "Test"]
341384
do {

0 commit comments

Comments
 (0)