Skip to content

Commit

Permalink
Add code
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmeuli committed Mar 23, 2020
1 parent 65a1ef2 commit d12e87b
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 5 deletions.
155 changes: 153 additions & 2 deletions Sources/SwiftExec/SwiftExec.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,154 @@
struct SwiftExec {
var text = "Hello, World!"
import Foundation

/// Custom error returned by the `exec` function
public struct ExecError: LocalizedError, Equatable {
public var errorDescription: String? {
execResult.message ?? "Process failed: Unknown error"
}

public var execResult: ExecResult

public init(execResult: ExecResult) {
self.execResult = execResult
}
}

/// Options for customizing the behavior of the `exec` function
public struct ExecOptions {
/// Current working directory of the process
public let cwd: URL?

/// Whether to remove the final newline character from output
public let stripFinalNewline: Bool

public init(cwd: URL? = nil, stripFinalNewline: Bool = true) {
self.cwd = cwd
self.stripFinalNewline = stripFinalNewline
}
}

/// Results returned by the `exec` function
public struct ExecResult: Equatable {
/// Status of the executed command. It is considered to be failed if either the execution failed
/// or the process returned a non-zero exit code
public let failed: Bool

/// Error message for failed commands. Contains `stderr` if a non-zero exit code is the reason
/// for the failure
public let message: String?

/// Exit code of the process that was executed
public let exitCode: Int32?

/// `stout` of the process that was executed
public let stdout: String?

/// `stderr` of the process that was executed
public let stderr: String?

public init(
failed: Bool,
message: String? = nil,
exitCode: Int32? = nil,
stdout: String? = nil,
stderr: String? = nil
) {
self.failed = failed
self.message = message
self.exitCode = exitCode
self.stdout = stdout
self.stderr = stderr
}
}

/// The `exec` function invokes the specified program in a new process using the provided (optional)
/// arguments. The process result is returned (`stdout` and `stderr` are converted to strings). If
/// the execution fails or the process returns a non-zero exit code, an error is thrown
public func exec(
program programPath: String,
arguments: [String] = [],
options: ExecOptions = ExecOptions()
) throws -> ExecResult {
guard FileManager.default.fileExists(atPath: programPath) else {
throw ExecError(execResult: ExecResult(
failed: true,
message: "Program with URL \"\(programPath)\" was not found"
))
}
let programUrl = URL(fileURLWithPath: programPath)

// Create new process for the provided program and arguments
let process = Process()
process.executableURL = programUrl
process.arguments = arguments
if options.cwd != nil {
process.currentDirectoryURL = options.cwd
}

// Create pipes for `stdout` and `stderr` so their content can be read later
let outPipe = Pipe()
let errPipe = Pipe()
process.standardOutput = outPipe
process.standardError = errPipe

// Execute the process
do {
try process.run()
} catch {
throw ExecError(execResult: ExecResult(
failed: true,
message: "Process failed: \(error.localizedDescription)"
))
}

process.waitUntilExit()

// Create strings with the contents of `stdout` and `stderr`
let stdoutData = outPipe.fileHandleForReading.readDataToEndOfFile()
let stderrData = errPipe.fileHandleForReading.readDataToEndOfFile()
var stdoutString = String(data: stdoutData, encoding: .utf8) ?? ""
var stderrString = String(data: stderrData, encoding: .utf8) ?? ""
if options.stripFinalNewline {
if stdoutString.hasSuffix("\n") {
stdoutString.removeLast()
}
if stderrString.hasSuffix("\n") {
stderrString.removeLast()
}
}

// Read, format and return the process result
let exitCode = process.terminationStatus
let failed = exitCode != 0
if failed {
var message = "Command returned non-zero exit code (\(exitCode))"
if !stderrString.isEmpty {
message.append(":\n\n\(stderrString)")
}
throw ExecError(execResult: ExecResult(
failed: failed,
message: message,
exitCode: exitCode,
stdout: stdoutString,
stderr: stderrString
))
}
return ExecResult(
failed: failed,
exitCode: exitCode,
stdout: stdoutString,
stderr: stderrString
)
}

/// The `execBash` function runs the provided command using Bash
public func execBash(
_ command: String,
options: ExecOptions = ExecOptions()
) throws -> ExecResult {
try exec(
program: "/bin/bash",
arguments: ["-c", command],
options: options
)
}
99 changes: 96 additions & 3 deletions Tests/SwiftExecTests/SwiftExecTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,104 @@
import XCTest

final class SwiftExecTests: XCTestCase {
func testExample() {
XCTAssertEqual(SwiftExec().text, "Hello, World!")
func testExecSuccess() throws {
let result = try exec(
program: "/bin/echo",
arguments: ["hello", "world"]
)
XCTAssertEqual(result, ExecResult(
failed: false,
exitCode: 0,
stdout: "hello world",
stderr: ""
))
}

func testExecFailure() throws {
// Call `cp` without arguments to get a non-zero exit code and output in `stderr`. The
// `exec` function should throw an error
let expectedStderr = """
usage: cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file target_file
cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file ... target_directory
"""
let expectedErrorMessage = "Command returned non-zero exit code (64):\n\n\(expectedStderr)"
XCTAssertThrowsError(try exec(program: "/bin/cp")) { error in
let execError = error as? ExecError
XCTAssertEqual(
execError?.localizedDescription,
expectedErrorMessage
)
XCTAssertEqual(execError, ExecError(execResult: ExecResult(
failed: true,
message: expectedErrorMessage,
exitCode: 64,
stdout: "",
stderr: expectedStderr
)))
}
}

func testExecMissingProgram() {
// Error should be thrown if the specified program cannot be found
XCTAssertThrowsError(
try exec(program: "/bin/something", arguments: ["hello", "world"])
) { error in
let execError = error as? ExecError
let expectedErrorMessage = "Program with URL \"/bin/something\" was not found"
XCTAssertEqual(
execError?.localizedDescription,
expectedErrorMessage
)
XCTAssertEqual(execError, ExecError(execResult: ExecResult(
failed: true,
message: expectedErrorMessage
)))
}
}

func testExecCwd() throws {
// Run `pwd` in home directory, should return home directory path
let homeDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
let result = try exec(program: "/bin/pwd", options: ExecOptions(cwd: homeDirectoryURL))
XCTAssertEqual(result, ExecResult(
failed: false,
exitCode: 0,
stdout: homeDirectoryURL.path,
stderr: ""
))
}

func testExecWithFinalNewline() throws {
// Final newline character should not be removed if the corresponding option is set
let result = try exec(
program: "/bin/echo",
arguments: ["hello", "world"],
options: ExecOptions(stripFinalNewline: false)
)
XCTAssertEqual(result, ExecResult(
failed: false,
exitCode: 0,
stdout: "hello world\n",
stderr: ""
))
}

func testExecBash() throws {
let result = try execBash("echo hello world")
XCTAssertEqual(result, ExecResult(
failed: false,
exitCode: 0,
stdout: "hello world",
stderr: ""
))
}

static var allTests = [
("testExample", testExample),
("testExecSuccess", testExecSuccess),
("testExecFailure", testExecFailure),
("testExecMissingProgram", testExecMissingProgram),
("testExecCwd", testExecCwd),
("testExecWithFinalNewline", testExecWithFinalNewline),
("textExecBash", testExecBash),
]
}

0 comments on commit d12e87b

Please sign in to comment.