I am trying to use Process() to start a task in Swift, but I need to see what is being sent to the shell for debugging purposes.
I am trying to send the following command:
gswin64c.exe -q -dNODISPLAY -dNOSAFER -c "(input.pdf) (r) file runpdfbegin pdfpagecount = quit"
If I run the very same command in an environment that uses a UNIX shell (bash, zsh, etc.), it runs fine. In Windows using cmd.exe, however, it fails, giving the following error message:
Error: /undefined in ".
I suspect that Swift is inserting slashes as “escape” characters. Is there a way to see the string that Swift is sending to the shell?
Here is a sample:
import Foundation
let inputFile = URL(fileURLWithPath: "input.pdf")
let task = Process()
// In MacOS or Linux, obviously, we would use the appropriate path to 'gs'.
// Use gswin32c.exe if you have the 32-bit version of Ghostscript in Windows.
task.executableURL = URL(fileURLWithPath: #"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#)
// The following works when the shell is bash, zsh, or similar, but not with cmd
task.arguments = ["-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"\"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit\""]
let stdout = Pipe()
let stderr = Pipe()
task.standardOutput = stdout
task.standardError = stderr
do {
try task.run()
} catch {
print(error)
exit(1)
}
task.waitUntilExit()
extension String {
init?(pipe: Pipe) {
guard let data = try? pipe.fileHandleForReading.readToEnd() else {
return nil
}
guard let result = String(data: data, encoding: .utf8) else {
return nil
}
self = result
}
}
if let stdoutText = String(pipe: stdout) {
print(stdoutText)
}
if let stderrText = String(pipe: stderr) {
print(stderrText)
}
As a follow up, can the command be written in Swift so that it gets passed on to GhostScript correctly?
Follow up:
There does not appear to be a straightforward way to see what Swift sends to the shell.
However, I was able to solve my immediate problem. It seems that the sanitizer sending the code to the Windows command shell inserts slashes in front of the spaces. I was able to work around the issue by removing the quotation marks on either side of the PostScript instructions (it turns out they are not necessary), and placing each element in a separate member of the array:
task.arguments = [ "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path))",
"(r)",
"file",
"runpdfbegin",
"pdfpagecount",
"=",
"quit" ]
Or else, if you prefer to see the entire working example:
import Foundation
let inputFile = URL(fileURLWithPath: "input.pdf")
let task = Process()
// In MacOS or Linux, obviously, we would use the appropriate path to 'gs'.
// Use gswin32c.exe if you have the 32-bit version of Ghostscript in Windows.
task.executableURL = URL(fileURLWithPath: #"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#)
print(inputFile.path)
task.arguments = [ "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path))",
"(r)",
"file",
"runpdfbegin",
"pdfpagecount",
"=",
"quit" ]
let stdout = Pipe()
let stderr = Pipe()
task.standardOutput = stdout
task.standardError = stderr
do {
try task.run()
} catch {
print(error)
exit(1)
}
task.waitUntilExit()
extension String {
init?(pipe: Pipe) {
guard let data = try? pipe.fileHandleForReading.readToEnd() else {
return nil
}
guard let result = String(data: data, encoding: .utf8) else {
return nil
}
self = result
}
}
if let stdoutText = String(pipe: stdout) {
print(stdoutText)
}
if let stderrText = String(pipe: stderr) {
print(stderrText)
}
After checking the code in swift-corelibs-foundation, I think I found how it modifies your arguments for Windows under the hood.
In Process.run, it first constructs a command: [String] (Line 495):
var command: [String] = [launchPath]
if let arguments = self.arguments {
command.append(contentsOf: arguments)
}
In your case, it would be:
let command = [#"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#, "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"\"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit\""]
Then after a whole bunch of code, it calls quoteWindowsCommandLine to create a command for the Windows shell (Line 656):
try quoteWindowsCommandLine(command).withCString(encodedAs: UTF16.self) { wszCommandLine in
try FileManager.default._fileSystemRepresentation(withPath: workingDirectory) { wszCurrentDirectory in
try szEnvironment.withCString(encodedAs: UTF16.self) { wszEnvironment in
if !CreateProcessW(nil, UnsafeMutablePointer<WCHAR>(mutating: wszCommandLine),
quoteWindowsCommandLine is declared here (I've removed the comments for brevity):
private func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
func quoteWindowsCommandArg(arg: String) -> String {
if !arg.contains(where: {" \t\n\"".contains($0)}) {
return arg
}
var quoted = "\""
var unquoted = arg.unicodeScalars
while !unquoted.isEmpty {
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
let backslashCount = unquoted.count
quoted.append(String(repeating: "\\", count: backslashCount * 2))
break
}
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
if (unquoted[firstNonBackslash] == "\"") {
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
quoted.append(String(unquoted[firstNonBackslash]))
} else {
quoted.append(String(repeating: "\\", count: backslashCount))
quoted.append(String(unquoted[firstNonBackslash]))
}
unquoted.removeFirst(backslashCount + 1)
}
quoted.append("\"")
return quoted
}
return commandLine.map(quoteWindowsCommandArg).joined(separator: " ")
}
You can copy-paste this into a playground, and play around with it. It turns out that your string got turned into:
"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "\"(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit\""
Apparently the last argument doesn't need to be quoted on Windows. quoteWindowsCommandLine does the quoting for you already. If you just say:
let command = [#"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#, "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit"]
print(quoteWindowsCommandLine(command))
Not quoting the last argument seems to work on macOS too.
Another mistake is that you used inputFile.path, which always produces paths with / (see this). You should use the "file system representation" of the URL:
inputFile.withUnsafeFileSystemRepresentation { pointer in
task.arguments = ["-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(String(cString: pointer!)) (r) file runpdfbegin pdfpagecount = quit"]
}
Then it seems to produce something that looks right:
"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit"
(I don't have a Windows machine)
So I am trying to execute a command using sudo from Swift using the following code (as suggested here:
func doTask(_ password:String) {
let taskOne = Process()
taskOne.launchPath = "/bin/echo"
taskOne.arguments = [password]
let taskTwo = Process()
taskTwo.launchPath = "/usr/bin/sudo"
taskTwo.arguments = ["-S", "/usr/bin/xattr", "-d", "-r", "com.test.exemple", " /Desktop/file.extension"]
//taskTwo.arguments = ["-S", "/usr/bin/touch", "/tmp/foo.bar.baz"]
let pipeBetween:Pipe = Pipe()
taskOne.standardOutput = pipeBetween
taskTwo.standardInput = pipeBetween
let pipeToMe = Pipe()
taskTwo.standardOutput = pipeToMe
taskTwo.standardError = pipeToMe
taskOne.launch()
taskTwo.launch()
let data = pipeToMe.fileHandleForReading.readDataToEndOfFile()
let output : String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String
print(output)
}
It works just fine for passwords such as "test", "password#123" etc. But when I try a password containing an umlaut such as "ä","ü" or "ö" in doesn't work. Any ideas why?
I'm not sure why the answer to the other question piped through echo... seems to introduce unnecessary complications and unknowns.
The following more direct approach is tested and working:
import Foundation
let password = "äëïöü"
let passwordWithNewline = password + "\n"
let sudo = Process()
sudo.launchPath = "/usr/bin/sudo"
sudo.arguments = ["-S", "/bin/ls"]
let sudoIn = Pipe()
let sudoOut = Pipe()
sudo.standardOutput = sudoOut
sudo.standardError = sudoOut
sudo.standardInput = sudoIn
sudo.launch()
// Show the output as it is produced
sudoOut.fileHandleForReading.readabilityHandler = { fileHandle in
let data = fileHandle.availableData
if (data.count == 0) { return }
print("read \(data.count)")
print("\(String(bytes: data, encoding: .utf8) ?? "<UTF8 conversion failed>")")
}
// Write the password
sudoIn.fileHandleForWriting.write(passwordWithNewline.data(using: .utf8)!)
// Close the file handle after writing the password; avoids a
// hang for incorrect password.
try? sudoIn.fileHandleForWriting.close()
// Make sure we don't disappear while output is still being produced.
sudo.waitUntilExit()
print("Process did exit")
The crux is that you must add a newline after the password. (I suppose in some ways echo is just an overly complicated way of doing that!)
I need to launch a terminal command to xcode.
This is the command:
sudo xattr -d -r com.test.exemple /Desktop/file.extension
I tried so
let task = Process()
task.launchPath = "/usr/sbin/xattr"
task.arguments = ["-d","-r", "com.test.exemple"," /Desktop/file.extension"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output : String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String
print(output)
Here's one way to do it using a pipe between commands. I verified that when I use the arguments in the commented out line that the file gets created by the super user.
What it is doing is this:
echo 'password' | sudo -S /usr/bin/xattr -d -r com.test.exemple
/Desktop/file.extension
func doTask(_ password:String) {
let taskOne = Process()
taskOne.launchPath = "/bin/echo"
taskOne.arguments = [password]
let taskTwo = Process()
taskTwo.launchPath = "/usr/bin/sudo"
taskTwo.arguments = ["-S", "/usr/bin/xattr", "-d", "-r", "com.test.exemple", " /Desktop/file.extension"]
//taskTwo.arguments = ["-S", "/usr/bin/touch", "/tmp/foo.bar.baz"]
let pipeBetween:Pipe = Pipe()
taskOne.standardOutput = pipeBetween
taskTwo.standardInput = pipeBetween
let pipeToMe = Pipe()
taskTwo.standardOutput = pipeToMe
taskTwo.standardError = pipeToMe
taskOne.launch()
taskTwo.launch()
let data = pipeToMe.fileHandleForReading.readDataToEndOfFile()
let output : String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String
print(output)
}
I came across this question after reading this newer question. Just in case somebody arrives here via search, here's the code from my answer to that question.
There's no real need to pipe through echo; the following works just fine:
The following more direct approach is tested and working:
import Foundation
let password = "äëïöü"
let passwordWithNewline = password + "\n"
let sudo = Process()
sudo.launchPath = "/usr/bin/sudo"
sudo.arguments = ["-S", "/bin/ls"]
let sudoIn = Pipe()
let sudoOut = Pipe()
sudo.standardOutput = sudoOut
sudo.standardError = sudoOut
sudo.standardInput = sudoIn
sudo.launch()
// Show the output as it is produced
sudoOut.fileHandleForReading.readabilityHandler = { fileHandle in
let data = fileHandle.availableData
if (data.count == 0) { return }
print("read \(data.count)")
print("\(String(bytes: data, encoding: .utf8) ?? "<UTF8 conversion failed>")")
}
// Write the password
sudoIn.fileHandleForWriting.write(passwordWithNewline.data(using: .utf8)!)
// Close the file handle after writing the password; avoids a
// hang for incorrect password.
try? sudoIn.fileHandleForWriting.close()
// Make sure we don't disappear while output is still being produced.
sudo.waitUntilExit()
print("Process did exit")
The crux is that you must add a newline after the password.
In Xcode's "Signing & Capabilities" tab, disable "App Sandbox"
Use AppleScript:
func runScriptThatNeedsSudo() {
let myAppleScript = """
do shell script \"sudo touch /Library/hello
sudo launchctl kickstart -k system/com.apple.audio.coreaudiod" with administrator privileges
"""
var error: NSDictionary?
let scriptObject = NSAppleScript(source: myAppleScript)!
scriptObject.executeAndReturnError(&error)
}
This will prompt the user for their password.
Consider this a security issue because it will indiscriminately run any tool or application, severely increasing the user's security risk. Always inform the user about what your application is about to do. You should avoid the use of this functionality if possible.
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 ....
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
#discardableResult // Add to suppress warnings when you don't want/need a result
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
}
Examples:
// Example usage capturing error:
do {
try safeShell("ls -la")
}
catch {
print("\(error)") //handle or silence the error here
}
// Example usage where you don't care about the error and want a nil back instead
let result = try? safeShell("ls -la")
// Example usage where you don't care about the error or the return value
try? safeShell("ls -la")
Note: For the last case where you are using try? and aren't using the result, for some reason the compiler still warns you even though it's marked as #discardableResult. This only happens with try?, not try within a do-try-catch block or from within a throwing function. Either way, you can safely ignore it.
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
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.
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)
}
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)")
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.
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"])
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)
}
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")
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")
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
}
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)
}
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)")
}
}
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.
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
}
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)