Skip to content

Handle subcommands #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions Sources/SwiftDriver/Driver/Driver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public struct Driver {
enum Error: Swift.Error {
case invalidDriverName(String)
case invalidInput(String)
case subcommandPassedToDriver
}

/// The set of environment variables that are visible to the driver and
Expand Down Expand Up @@ -193,11 +194,15 @@ public struct Driver {
diagnosticsHandler: @escaping DiagnosticsEngine.DiagnosticsHandler = Driver.stderrDiagnosticsHandler
) throws {
self.env = env

// FIXME: Determine if we should run as subcommand.

self.diagnosticEngine = DiagnosticsEngine(handlers: [diagnosticsHandler])

if case .subcommand = try Self.invocationRunMode(forArgs: args).mode {
throw Error.subcommandPassedToDriver
}

var args = try Self.expandResponseFiles(args, diagnosticsEngine: self.diagnosticEngine)[...]

self.driverKind = try Self.determineDriverKind(args: &args)
self.optionTable = OptionTable()
self.parsedOptions = try optionTable.parse(Array(args))
Expand Down Expand Up @@ -351,6 +356,54 @@ public struct Driver {
}
}

extension Driver {

public enum InvocationRunMode: Equatable {
case normal(isRepl: Bool)
case subcommand(String)
}

/// Determines whether the given arguments constitute a normal invocation,
/// or whether they invoke a subcommand.
///
/// Returns the invocation mode along with the arguments modified for that mode.
public static func invocationRunMode(
forArgs args: [String]
) throws -> (mode: InvocationRunMode, args: [String]) {

assert(!args.isEmpty)

let execName = try VirtualPath(path: args[0]).basenameWithoutExt

// If we are not run as 'swift' or there are no program arguments, always invoke as normal.
guard execName == "swift", args.count > 1 else { return (.normal(isRepl: false), args) }

// Otherwise, we have a program argument.
let firstArg = args[1]

// If it looks like an option or a path, then invoke in interactive mode with the arguments as given.
if firstArg.hasPrefix("-") || firstArg.hasPrefix("/") || firstArg.contains(".") {
return (.normal(isRepl: false), args)
}

// Otherwise, we should have some sort of subcommand.

var updatedArgs = args

// If it is the "built-in" 'repl', then use the normal driver.
if firstArg == "repl" {
updatedArgs.remove(at: 1)
return (.normal(isRepl: true), updatedArgs)
}

let subcommand = "swift-\(firstArg)"

updatedArgs.replaceSubrange(0...1, with: [subcommand])

return (.subcommand(subcommand), updatedArgs)
}
}

// MARK: - Response files.
extension Driver {
/// Tokenize a single line in a response file.
Expand Down
13 changes: 2 additions & 11 deletions Sources/SwiftDriver/Driver/OutputFileMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,12 @@ public struct OutputFileMap: Equatable {
return output
}

// Create a temporary file
let baseName: String
switch inputFile {
case .absolute(let path):
baseName = path.basenameWithoutExt
case .relative(let path), .temporary(let path):
baseName = path.basenameWithoutExt
case .standardInput:
baseName = ""
case .standardOutput:
if inputFile == .standardOutput {
fatalError("Standard output cannot be an input file")
}

// Form the virtual path.
return .temporary(RelativePath(baseName.appendingFileTypeExtension(outputType)))
return .temporary(RelativePath(inputFile.basenameWithoutExt.appendingFileTypeExtension(outputType)))
}

public func existingOutput(inputFile: VirtualPath, outputType: FileType) -> VirtualPath? {
Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftDriver/Utilities/VirtualPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ public enum VirtualPath: Hashable {
default: return nil
}
}

/// Retrieve the basename of the path without the extension.
public var basenameWithoutExt: String {
switch self {
case .absolute(let path):
return path.basenameWithoutExt
case .relative(let path), .temporary(let path):
return path.basenameWithoutExt
case .standardInput, .standardOutput:
return ""
}
}
}

extension VirtualPath: Codable {
Expand Down
18 changes: 17 additions & 1 deletion Sources/swift-driver/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,23 @@ do {
processSet.terminate()
}

var driver = try Driver(args: CommandLine.arguments)
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments)

if case .subcommand(let subcommand) = mode {
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
// If we didn't find the tool there, let the OS search for it.
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
?? Process.findExecutable(subcommand)

if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
fatalError("cannot find subcommand executable '\(subcommand)'")
}

// Execute the subcommand.
try exec(path: subcommandPath?.pathString ?? "", args: Array(arguments.dropFirst()))
}

var driver = try Driver(args: arguments)
let resolver = try ArgsResolver()
try driver.run(resolver: resolver, processSet: processSet)

Expand Down
37 changes: 37 additions & 0 deletions Tests/SwiftDriverTests/SwiftDriverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,43 @@ final class SwiftDriverTests: XCTestCase {
}
}

func testInvocationRunModes() throws {

let driver1 = try Driver.invocationRunMode(forArgs: ["swift"])
XCTAssertEqual(driver1.mode, .normal(isRepl: false))
XCTAssertEqual(driver1.args, ["swift"])

let driver2 = try Driver.invocationRunMode(forArgs: ["swift", "-buzz"])
XCTAssertEqual(driver2.mode, .normal(isRepl: false))
XCTAssertEqual(driver2.args, ["swift", "-buzz"])

let driver3 = try Driver.invocationRunMode(forArgs: ["swift", "/"])
XCTAssertEqual(driver3.mode, .normal(isRepl: false))
XCTAssertEqual(driver3.args, ["swift", "/"])

let driver4 = try Driver.invocationRunMode(forArgs: ["swift", "./foo"])
XCTAssertEqual(driver4.mode, .normal(isRepl: false))
XCTAssertEqual(driver4.args, ["swift", "./foo"])

let driver5 = try Driver.invocationRunMode(forArgs: ["swift", "repl"])
XCTAssertEqual(driver5.mode, .normal(isRepl: true))
XCTAssertEqual(driver5.args, ["swift"])

let driver6 = try Driver.invocationRunMode(forArgs: ["swift", "foo", "bar"])
XCTAssertEqual(driver6.mode, .subcommand("swift-foo"))
XCTAssertEqual(driver6.args, ["swift-foo", "bar"])
}

func testSubcommandsHandling() throws {

XCTAssertNoThrow(try Driver(args: ["swift"]))
XCTAssertNoThrow(try Driver(args: ["swift", "-I=foo"]))
XCTAssertNoThrow(try Driver(args: ["swift", ".foo"]))
XCTAssertNoThrow(try Driver(args: ["swift", "/foo"]))

XCTAssertThrowsError(try Driver(args: ["swift", "foo"]))
}

func testDriverKindParsing() throws {
func assertArgs(
_ args: String...,
Expand Down