How do I run a terminal command in a Swift script? (e.g. xcodebuild)

SwiftBashShellXcodebuild

Swift Problem Overview


I want to replace my CI bash scripts with swift. I can't figure out how to invoke normal terminal command such as ls or xcodebuild

#!/usr/bin/env xcrun swift

import Foundation // Works
println("Test") // Works
ls // Fails
xcodebuild -workspace myApp.xcworkspace // Fails

$ ./script.swift
./script.swift:5:1: error: use of unresolved identifier 'ls'
ls // Fails
^
... etc ....

Swift Solutions


Solution 1 - Swift

If you would like to use command line arguments "exactly" as you would in command line (without separating all the arguments), try the following.

(This answer improves off of LegoLess's answer and can be used in Swift 5)

import Foundation

func shell(_ command: String) -> String {
    let task = Process()
    let pipe = Pipe()
    
    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.launchPath = "/bin/zsh"
    task.standardInput = nil
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!
    
    return output
}

// Example usage:
shell("ls -la")

Updated / safer function calls 10/23/21: It's possible to run into a runtime error with the above shell command and if so, try swapping to the updated calls below. You'll need to use a do catch statement around the new shell command but hopefully this saves you some time searching for a way to catch unexpected error(s) too.

Explanation: Since task.launch() isn't a throwing function it cannot be caught and I was finding it to occasionally simply crash the app when called. After much internet searching, I found the Process class has deprecated task.launch() in favor of a newer function task.run() which does throw errors properly w/out crashing the app. To find out more about the updated methods, please see: https://eclecticlight.co/2019/02/02/scripting-in-swift-process-deprecations/

import Foundation

func safeShell(_ command: String) throws -> String {
    let task = Process()
    let pipe = Pipe()
    
    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated
    task.standardInput = nil

    try task.run() //<--updated
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!
    
    return output
}

// Example usage:
do {
    safeShell("ls -la")
}
catch {
    print("\(error)") //handle or silence the error here
}

Solution 2 - Swift

If you don't use command outputs in Swift code, following would be sufficient:

#!/usr/bin/env swift

import Foundation

@discardableResult
func shell(_ args: String...) -> Int32 {
    let task = Process()
    task.launchPath = "/usr/bin/env"
    task.arguments = args
    task.launch()
    task.waitUntilExit()
    return task.terminationStatus
}

shell("ls")
shell("xcodebuild", "-workspace", "myApp.xcworkspace")

Updated: for Swift3/Xcode8

Solution 3 - Swift

The problem here is that you cannot mix and match Bash and Swift. You already know how to run Swift script from command line, now you need to add the methods to execute Shell commands in Swift. In summary from PracticalSwift blog:

func shell(_ launchPath: String, _ arguments: [String]) -> String?
{
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments

    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: String.Encoding.utf8)

    return output
}

The following Swift code will execute xcodebuild with arguments and then output the result.

shell("xcodebuild", ["-workspace", "myApp.xcworkspace"]);

As for searching the directory contents (which is what ls does in Bash), I suggest using NSFileManager and scanning the directory directly in Swift, instead of Bash output, which can be a pain to parse.

Solution 4 - Swift

Utility function In Swift 3.0

This also returns the tasks termination status and waits for completion.

func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) {
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments

    let pipe = Pipe()
    task.standardOutput = pipe
    task.standardError = pipe
    task.launch()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)
    task.waitUntilExit()
    return (output, task.terminationStatus)
}

Solution 5 - Swift

If you'd like to use the bash environment for calling commands use the following bash function which uses a fixed up version of Legoless. I had to remove a trailing newline from the shell function's result.

Swift 3.0:(Xcode8)

import Foundation

func shell(launchPath: String, arguments: [String]) -> String
{
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments
    
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: String.Encoding.utf8)!
    if output.characters.count > 0 {
        //remove newline character.
        let lastIndex = output.index(before: output.endIndex)
        return output[output.startIndex ..< lastIndex]
    }
    return output
}

func bash(command: String, arguments: [String]) -> String {
    let whichPathForCommand = shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
    return shell(launchPath: whichPathForCommand, arguments: arguments)
}

For example to get the current working git branch of the current working directory:

let currentBranch = bash("git", arguments: ["describe", "--contains", "--all", "HEAD"])
print("current branch:\(currentBranch)")

Solution 6 - Swift

Full script based on Legoless's answer

#!/usr/bin/env swift

import Foundation

func printShell(launchPath: String, arguments: [String] = []) {
    let output = shell(launchPath: launchPath, arguments: arguments)

    if (output != nil) {
        print(output!)
    }
}

func shell(launchPath: String, arguments: [String] = []) -> String? {
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments

    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: String.Encoding.utf8)

    return output
}

// > ls
// > ls -a -g
printShell(launchPath: "/bin/ls")
printShell(launchPath: "/bin/ls", arguments:["-a", "-g"])

Solution 7 - Swift

Just to update this since Apple has deprecated both .launchPath and launch(), here's an updated utility function for Swift 4 that should be a little more future proof.

Note: Apple's documentation on the replacements (run(), executableURL, etc) are basically empty at this point.

import Foundation

// wrapper function for shell commands
// must provide full path to executable
func shell(_ launchPath: String, _ arguments: [String] = []) -> (String?, Int32) {
  let task = Process()
  task.executableURL = URL(fileURLWithPath: launchPath)
  task.arguments = arguments
  
  let pipe = Pipe()
  task.standardOutput = pipe
  task.standardError = pipe
  
  do {
    try task.run()
  } catch {
    // handle errors
    print("Error: \(error.localizedDescription)")
  }
  
  let data = pipe.fileHandleForReading.readDataToEndOfFile()
  let output = String(data: data, encoding: .utf8)
  
  task.waitUntilExit()
  return (output, task.terminationStatus)
}


// valid directory listing test
let (goodOutput, goodStatus) = shell("/bin/ls", ["-la"])
if let out = goodOutput { print("\(out)") }
print("Returned \(goodStatus)\n")

// invalid test
let (badOutput, badStatus) = shell("ls")

Should be able to paste this directly into a playground to see it in action.

Solution 8 - Swift

Updating for Swift 4.0 (dealing with changes to String)

func shell(launchPath: String, arguments: [String]) -> String
{
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments
    
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: String.Encoding.utf8)!
    if output.count > 0 {
        //remove newline character.
        let lastIndex = output.index(before: output.endIndex)
        return String(output[output.startIndex ..< lastIndex])
    }
    return output
}

func bash(command: String, arguments: [String]) -> String {
    let whichPathForCommand = shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
    return shell(launchPath: whichPathForCommand, arguments: arguments)
}

Solution 9 - Swift

After trying some of the solutions posted here, I found that the best way to execute commands was using the -c flag for the arguments.

@discardableResult func shell(_ command: String) -> (String?, Int32) {
    let task = Process()
    
    task.launchPath = "/bin/bash"
    task.arguments = ["-c", command]
    
    let pipe = Pipe()
    task.standardOutput = pipe
    task.standardError = pipe
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)
    task.waitUntilExit()
    return (output, task.terminationStatus)
}


let _ = shell("mkdir ~/Desktop/test")

Solution 10 - Swift

import Foundation

enum Commands {
  struct Result {
    public let statusCode: Int32
    public let output: String
  }
  
  static func run(_ command: String,
                  environment: [String: String]? = nil,
                  executableURL: String = "/bin/bash",
                  dashc: String = "-c") -> Result {
    // create process
    func create(_ executableURL: String,
                dashc: String,
                environment: [String: String]?) -> Process {
      let process = Process()
      if #available(macOS 10.13, *) {
        process.executableURL = URL(fileURLWithPath: executableURL)
      } else {
        process.launchPath = "/bin/bash"
      }
      if let environment = environment {
        process.environment = environment
      }
      process.arguments = [dashc, command]
      return process
    }
    // run process
    func run(_ process: Process) throws {
      if #available(macOS 10.13, *) {
        try process.run()
      } else {
        process.launch()
      }
      process.waitUntilExit()
    }
    // read data
    func fileHandleData(fileHandle: FileHandle) throws -> String? {
      var outputData: Data?
      if #available(macOS 10.15.4, *) {
        outputData = try fileHandle.readToEnd()
      } else {
        outputData = fileHandle.readDataToEndOfFile()
      }
      if let outputData = outputData {
        return String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
      }
      return nil
    }
    
    let process = create(executableURL, dashc: dashc, environment: environment)
    
    let outputPipe = Pipe()
    process.standardOutput = outputPipe
    
    let errorPipe = Pipe()
    process.standardError = errorPipe
    
    do {
      try run(process)
      
      let outputActual = try fileHandleData(fileHandle: outputPipe.fileHandleForReading) ?? ""
      let errorActual = try fileHandleData(fileHandle: errorPipe.fileHandleForReading) ?? ""
      
      if process.terminationStatus == EXIT_SUCCESS {
        return Result(statusCode: process.terminationStatus, output: outputActual)
      }
      return Result(statusCode: process.terminationStatus, output: errorActual)
    } catch let error {
      return Result(statusCode: process.terminationStatus, output: error.localizedDescription)
    }
  }
}

Usage

let result = Commands.run("ls")
debugPrint(result.output)
debugPrint(result.statusCode)

or using swift-commands

import Commands

Commands.Bash.system("ls")

Solution 11 - Swift

Mixing rintaro and Legoless's answers for Swift 3

@discardableResult
func shell(_ args: String...) -> String {
    let task = Process()
    task.launchPath = "/usr/bin/env"
    task.arguments = args
    
    let pipe = Pipe()
    task.standardOutput = pipe
    
    task.launch()
    task.waitUntilExit()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    
    guard let output: String = String(data: data, encoding: .utf8) else {
        return ""
    }
    return output
}

Solution 12 - Swift

Small improvement with the support for env variables:

func shell(launchPath: String,
           arguments: [String] = [],
           environment: [String : String]? = nil) -> (String , Int32) {
    let task = Process()
    task.launchPath = launchPath
    task.arguments = arguments
    if let environment = environment {
        task.environment = environment
    }

    let pipe = Pipe()
    task.standardOutput = pipe
    task.standardError = pipe
    task.launch()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8) ?? ""
    task.waitUntilExit()
    return (output, task.terminationStatus)
}

Solution 13 - Swift

Example of using Process class to run a Python script.

Also:

 - added basic exception handling
 - setting environment variables (in my case I had to do it to get Google SDK to authenticate correctly)
 - arguments 







 import Cocoa

func shellTask(_ url: URL, arguments:[String], environment:[String : String]) throws ->(String?, String?){
   let task = Process()
   task.executableURL = url
   task.arguments =  arguments
   task.environment = environment
   
   let outputPipe = Pipe()
   let errorPipe = Pipe()
   
   task.standardOutput = outputPipe
   task.standardError = errorPipe
   try task.run()
   
   let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
   let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
   
   let output = String(decoding: outputData, as: UTF8.self)
   let error = String(decoding: errorData, as: UTF8.self)
   
   return (output,error)
}

func pythonUploadTask()
{
   let url = URL(fileURLWithPath: "/usr/bin/python")
   let pythonScript =  "upload.py"
   
   let fileToUpload = "/CuteCat.mp4"
   let arguments = [pythonScript,fileToUpload]
   var environment = ProcessInfo.processInfo.environment
   environment["PATH"]="usr/local/bin"
   environment["GOOGLE_APPLICATION_CREDENTIALS"] = "/Users/j.chudzynski/GoogleCredentials/credentials.json"
   do {
      let result = try shellTask(url, arguments: arguments, environment: environment)
      if let output = result.0
      {
         print(output)
      }
      if let output = result.1
      {
         print(output)
      }
      
   } catch  {
      print("Unexpected error:\(error)")
   }
}

Solution 14 - Swift

I've built SwiftExec, a small library for running such commands:

import SwiftExec

var result: ExecResult
do {
	result = try exec(program: "/usr/bin/git", arguments: ["status"])
} catch {
	let error = error as! ExecError
	result = error.execResult
}

print(result.exitCode!)
print(result.stdout!)
print(result.stderr!)

It's a single-file library which can easily be copy-pasted into projects or installed using SPM. It's tested and simplifies error handling.

There's also ShellOut, which additionally supports a variety of pre-defined commands.

Solution 15 - Swift

I saw many apps running a terminal command like:

cd /Applications/Theirappname.app/Contents/Resources && do sth here

This command is not different from running a shell script and if the app is not in Applications folder, it won't be executed correctly because this error will occur: No such file or directory: /Applications/Theirappname.app. Therefore, if you want to run an executable file in your Resources folder, you should use this code:

func runExec() -> Int32 {
   let task = Process()
   task.arguments = [Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists/")!.path]
   //If it does not have an extension then you just leave it empty
   //You can remove subdirectory if it does not exist
   task.launch()
   task.waitUntilExit()
   return task.terminationStatus
}

If your executable file requires an/some argument(s), the code will look like this:

func runExec() -> Int32 {
        let task = Process()
        task.launchPath = "/bin/bash"
        task.launchPath = Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists")?.path
   //If it does not have an extension then you just leave it empty
   //You can remove subdirectory if it does not exist
        task.arguments = ["arg1","arg2"]
        task.launch()
        task.waitUntilExit()
        return task.terminationStatus
}

Solution 16 - Swift

I'm in the process of re-factoring some existing Objective-C code that used NSTask to Swift, and one key thing missing in other answers is how you should be handling large quantities of stdout/stderr output. Failure to do this seems to result in hangs in the launched process.

One of the commands I commonly launch can produce hundreds of KB of output to both stdout and stderr.

To deal with this, I buffer the output thusly:

import Foundation

struct ShellScriptExecutor {

    static func runScript(_ script: ShellScript) -> ShellScriptResult {
        var errors: String = ""
        let tempFile = copyToTempFile(script)
        let process = Process()
        let stdout = Pipe()
        let stderr = Pipe()
        var stdoutData = Data.init(capacity: 8192)
        var stderrData = Data.init(capacity: 8192)

        process.standardOutput = stdout
        process.standardError = stderr
        process.executableURL = URL(fileURLWithPath: "/bin/zsh")
        process.arguments = [tempFile]

        do {
            try process.run()

            // Buffer the data while running
            while process.isRunning {
                stdoutData.append(pipeToData(stdout))
                stderrData.append(pipeToData(stderr))
            }

            process.waitUntilExit()

            stdoutData.append(pipeToData(stdout))
            errors = dataToString(stderrData) + pipeToString(stderr)
        }

        catch {
            print("Process failed for " + tempFile + ": " + error.localizedDescription)
        }

        // Clean up
        if !tempFile.isEmpty {
            do {
                try FileManager.default.removeItem(atPath: tempFile)
            }

            catch {
                print("Unable to remove " + tempFile + ": " + error.localizedDescription)
            }
        }

        return ShellScriptResult(stdoutData, script.resultType, errors)
    }

    static private func copyToTempFile(_ script: ShellScript) -> String {
        let tempFile: String = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString + ".sh", isDirectory: false).path

        if FileManager.default.createFile(atPath: tempFile, contents: Data(script.script.utf8), attributes: nil) {
            return tempFile;
        }
        else {
            return ""
        }
    }

    static private func pipeToString(_ pipe: Pipe) -> String {
        return dataToString(pipeToData(pipe))
    }

    static private func dataToString(_ data: Data) -> String {
        return String(decoding: data, as: UTF8.self)
    }

    static private func pipeToData(_ pipe: Pipe) -> Data {
        return pipe.fileHandleForReading.readDataToEndOfFile()
    }
}

(ShellScript and ShellScriptResult are just simple wrapper classes)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobertView Question on Stackoverflow
Solution 1 - Swiftuser3064009View Answer on Stackoverflow
Solution 2 - SwiftrintaroView Answer on Stackoverflow
Solution 3 - SwiftLegolessView Answer on Stackoverflow
Solution 4 - SwiftArunView Answer on Stackoverflow
Solution 5 - SwiftPelletView Answer on Stackoverflow
Solution 6 - SwiftRobertView Answer on Stackoverflow
Solution 7 - SwiftanguscView Answer on Stackoverflow
Solution 8 - SwiftrougeExciterView Answer on Stackoverflow
Solution 9 - SwiftlojalsView Answer on Stackoverflow
Solution 10 - SwiftPhilView Answer on Stackoverflow
Solution 11 - SwiftrichyView Answer on Stackoverflow
Solution 12 - SwiftAliaksandr BialiauskiView Answer on Stackoverflow
Solution 13 - SwiftJanusz ChudzynskiView Answer on Stackoverflow
Solution 14 - SwiftBalebView Answer on Stackoverflow
Solution 15 - SwiftklgView Answer on Stackoverflow
Solution 16 - SwiftMushyMiddleView Answer on Stackoverflow