Skip to content

Commit eeab9a6

Browse files
committed
Add subcommand handling based on the original driver
1 parent 0f431c1 commit eeab9a6

File tree

5 files changed

+123
-14
lines changed

5 files changed

+123
-14
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public struct Driver {
4343
enum Error: Swift.Error {
4444
case invalidDriverName(String)
4545
case invalidInput(String)
46+
case subcommandPassedToDriver
4647
}
4748

4849
/// The set of environment variables that are visible to the driver and
@@ -193,11 +194,15 @@ public struct Driver {
193194
diagnosticsHandler: @escaping DiagnosticsEngine.DiagnosticsHandler = Driver.stderrDiagnosticsHandler
194195
) throws {
195196
self.env = env
196-
197-
// FIXME: Determine if we should run as subcommand.
198197

199198
self.diagnosticEngine = DiagnosticsEngine(handlers: [diagnosticsHandler])
199+
200+
if case .subcommand = try Self.invocationRunMode(forArgs: args).mode {
201+
throw Error.subcommandPassedToDriver
202+
}
203+
200204
var args = try Self.expandResponseFiles(args, diagnosticsEngine: self.diagnosticEngine)[...]
205+
201206
self.driverKind = try Self.determineDriverKind(args: &args)
202207
self.optionTable = OptionTable()
203208
self.parsedOptions = try optionTable.parse(Array(args))
@@ -351,6 +356,54 @@ public struct Driver {
351356
}
352357
}
353358

359+
extension Driver {
360+
361+
public enum InvocationRunMode: Equatable {
362+
case normal(isRepl: Bool)
363+
case subcommand(String)
364+
}
365+
366+
/// Determines whether the given arguments constitute a normal invocation,
367+
/// or whether they invoke a subcommand.
368+
///
369+
/// Returns the invocation mode along with the arguments modified for that mode.
370+
public static func invocationRunMode(
371+
forArgs args: [String]
372+
) throws -> (mode: InvocationRunMode, args: [String]) {
373+
374+
assert(!args.isEmpty)
375+
376+
let execName = try VirtualPath(path: args[0]).basenameWithoutExt
377+
378+
// If we are not run as 'swift' or there are no program arguments, always invoke as normal.
379+
guard execName == "swift", args.count > 1 else { return (.normal(isRepl: false), args) }
380+
381+
// Otherwise, we have a program argument.
382+
let firstArg = args[1]
383+
384+
// If it looks like an option or a path, then invoke in interactive mode with the arguments as given.
385+
if firstArg.hasPrefix("-") || firstArg.hasPrefix("/") || firstArg.contains(".") {
386+
return (.normal(isRepl: false), args)
387+
}
388+
389+
// Otherwise, we should have some sort of subcommand.
390+
391+
var updatedArgs = args
392+
393+
// If it is the "built-in" 'repl', then use the normal driver.
394+
if firstArg == "repl" {
395+
updatedArgs.remove(at: 1)
396+
return (.normal(isRepl: true), updatedArgs)
397+
}
398+
399+
let subcommand = "swift-\(firstArg)"
400+
401+
updatedArgs.replaceSubrange(0...1, with: [subcommand])
402+
403+
return (.subcommand(subcommand), updatedArgs)
404+
}
405+
}
406+
354407
// MARK: - Response files.
355408
extension Driver {
356409
/// Tokenize a single line in a response file.

Sources/SwiftDriver/Driver/OutputFileMap.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,12 @@ public struct OutputFileMap: Equatable {
3131
return output
3232
}
3333

34-
// Create a temporary file
35-
let baseName: String
36-
switch inputFile {
37-
case .absolute(let path):
38-
baseName = path.basenameWithoutExt
39-
case .relative(let path), .temporary(let path):
40-
baseName = path.basenameWithoutExt
41-
case .standardInput:
42-
baseName = ""
43-
case .standardOutput:
34+
if inputFile == .standardOutput {
4435
fatalError("Standard output cannot be an input file")
4536
}
4637

4738
// Form the virtual path.
48-
return .temporary(RelativePath(baseName.appendingFileTypeExtension(outputType)))
39+
return .temporary(RelativePath(inputFile.basenameWithoutExt.appendingFileTypeExtension(outputType)))
4940
}
5041

5142
public func existingOutput(inputFile: VirtualPath, outputType: FileType) -> VirtualPath? {

Sources/SwiftDriver/Utilities/VirtualPath.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ public enum VirtualPath: Hashable {
7979
default: return nil
8080
}
8181
}
82+
83+
/// Retrieve the basename of the path without the extension.
84+
public var basenameWithoutExt: String {
85+
switch self {
86+
case .absolute(let path):
87+
return path.basenameWithoutExt
88+
case .relative(let path), .temporary(let path):
89+
return path.basenameWithoutExt
90+
case .standardInput, .standardOutput:
91+
return ""
92+
}
93+
}
8294
}
8395

8496
extension VirtualPath: Codable {

Sources/swift-driver/main.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,23 @@ do {
2323
processSet.terminate()
2424
}
2525

26-
var driver = try Driver(args: CommandLine.arguments)
26+
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments)
27+
28+
if case .subcommand(let subcommand) = mode {
29+
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
30+
// If we didn't find the tool there, let the OS search for it.
31+
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
32+
?? Process.findExecutable(subcommand)
33+
34+
if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
35+
fatalError("cannot find subcommand executable '\(subcommand)'")
36+
}
37+
38+
// Execute the subcommand.
39+
try exec(path: subcommandPath?.pathString ?? "", args: Array(arguments.dropFirst()))
40+
}
41+
42+
var driver = try Driver(args: arguments)
2743
let resolver = try ArgsResolver()
2844
try driver.run(resolver: resolver, processSet: processSet)
2945

Tests/SwiftDriverTests/SwiftDriverTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,43 @@ final class SwiftDriverTests: XCTestCase {
4747
}
4848
}
4949

50+
func testInvocationRunModes() throws {
51+
52+
let driver1 = try Driver.invocationRunMode(forArgs: ["swift"])
53+
XCTAssertEqual(driver1.mode, .normal(isRepl: false))
54+
XCTAssertEqual(driver1.args, ["swift"])
55+
56+
let driver2 = try Driver.invocationRunMode(forArgs: ["swift", "-buzz"])
57+
XCTAssertEqual(driver2.mode, .normal(isRepl: false))
58+
XCTAssertEqual(driver2.args, ["swift", "-buzz"])
59+
60+
let driver3 = try Driver.invocationRunMode(forArgs: ["swift", "/"])
61+
XCTAssertEqual(driver3.mode, .normal(isRepl: false))
62+
XCTAssertEqual(driver3.args, ["swift", "/"])
63+
64+
let driver4 = try Driver.invocationRunMode(forArgs: ["swift", "./foo"])
65+
XCTAssertEqual(driver4.mode, .normal(isRepl: false))
66+
XCTAssertEqual(driver4.args, ["swift", "./foo"])
67+
68+
let driver5 = try Driver.invocationRunMode(forArgs: ["swift", "repl"])
69+
XCTAssertEqual(driver5.mode, .normal(isRepl: true))
70+
XCTAssertEqual(driver5.args, ["swift"])
71+
72+
let driver6 = try Driver.invocationRunMode(forArgs: ["swift", "foo", "bar"])
73+
XCTAssertEqual(driver6.mode, .subcommand("swift-foo"))
74+
XCTAssertEqual(driver6.args, ["swift-foo", "bar"])
75+
}
76+
77+
func testSubcommandsHandling() throws {
78+
79+
XCTAssertNoThrow(try Driver(args: ["swift"]))
80+
XCTAssertNoThrow(try Driver(args: ["swift", "-I=foo"]))
81+
XCTAssertNoThrow(try Driver(args: ["swift", ".foo"]))
82+
XCTAssertNoThrow(try Driver(args: ["swift", "/foo"]))
83+
84+
XCTAssertThrowsError(try Driver(args: ["swift", "foo"]))
85+
}
86+
5087
func testDriverKindParsing() throws {
5188
func assertArgs(
5289
_ args: String...,

0 commit comments

Comments
 (0)