When I run following shell command either on terminal or as a shell script:
test -f test.dmg && rm test.dmg
Output: It deletes test.dmg, if presents in the current directory.
However, while running the same command in swift as below, its not working as expected:
#!/usr/bin/env xcrun swift
func shell(launchPath: String, arguments: [AnyObject] = []) -> String? {
let task = NSTask()
task.launchPath = launchPath
task.arguments = (arguments as! [String])
print(task.arguments)
let pipe = NSPipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: NSUTF8StringEncoding)
print(output)
return (output as! String)
}
let arguments = ["test", "-f", "test.dmg", "&&", "rm", "test.dmg"]
shell("/bin/sh", arguments)
Output with swift: test: test: is a directory
Can somebody help with this?
You have two problems here.
The reason for the error is because you have a test directory in the current directory and (because of the second error) the shell is trying to execute it as a script and failing.
The second error is that you don't pass commands to the shell to run directly as independent arguments.
That is you don't do this:
/bin/sh test -f test.dmg && rm test.dmg
What you do is you use the -c flag to the shell and pass the entire command as a single string to it:
/bin/sh -c 'test -f test.dmg && rm test.dmg'
Which would make your code something like this:
let arguments = ["-c" "test -f test.dmg && rm test.dmg"]
shell("/bin/sh", arguments)
Related
I'm trying to creating a macOS application that that involves allowing the user to run terminal commands. I am able to run a command, but it runs from a directory inside my app, as far as I can tell. Running pwd returns /Users/<me>/Library/Containers/<My app's bundle identifier>/Data.
How can I chose what directory the command runs from?
I'm also looking for a way to get cd to work, but if I can chose what directory to run the terminal command from, I can handle cd manually.
Here is the code that I'm currently using to run terminal commands:
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
I'm using Xcode 12 on Big Sur.
Thanks!
There is a deprecated property currentDirectoryPath on Process.
On the assumption you won't want to use a deprecated property, after reading its documentation head over to the FileManager and look at is provisions for managing the current directory and their implications.
Or just use cd as you've considered – you are launching a shell (zsh) with a shell command line as an argument. A command line can contain multiple commands separated by semicolons so you can prepend a cd to your command value.
The latter approach avoids changing your current process' current directory.
HTH
To add to CRD's answer, if using the cd approach, you may also consider separating your commands using && to wait for the previous commands to complete successfully before proceeding to the next command that depends on it.
Try the command you wish to run in the terminal and see if it completes as expected
Eg: /bin/bash -c "cd /source/code/ && git pull && swift build"
If everything works as expected you can go ahead and use it in your swift code as so:
shell("cd /source/code/ && git pull && swift build")
On the topic of deprecations, you may want to replace
launchPath with executableURL
and
launch() with run()
Sample implementation with updated code:
#discardableResult
func shell(_ args: String...) -> Int32 {
let task = Foundation.Process()
task.executableURL = URL(fileURLWithPath: "/bin/bash")
task.arguments = ["-c"]
task.arguments = task.arguments! + args
//Set environment variables
var environment = ProcessInfo.processInfo.environment
environment["PATH"]="/usr/bin/swift"
//environment["CREDENTIALS"] = "/path/to/credentials"
task.environment = environment
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch {
// handle errors
print("Error: \(error.localizedDescription)")
}
task.waitUntilExit()
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)
//Log or return output as desired
print(output)
print("Ran into error while running: \(error)")
return task.terminationStatus
}
Okay so I'm trying to run a Mosquitto publish command in bash from a Swift application for MacOS. Here is my code:
#IBAction func buttonClicked(_ sender: Any) {
let mosquittoCommand = "mosquitto_pub --cert blahblah.pem --key blahblah.key --cafile blahblah.pem -h 'blah.blah.com' -p 443 -t 'blah/blah/blah/blah' -m '{\"msg\": \"blahblahblah\", \"time\": \"2019-08-07T15:12:00Z\", \"id\": \"blah-blah-blah\", \"localpwd\": \"blahblahblah\"}' --tls-alpn x-amzn-mqtt-ca -i 'blahblahblah'"
print(shell("cd /Users/Me/Desktop/certs && " + mosquittoCommand))
}
func shell(_ command: String) -> String {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
I'm getting the following error:
/usr/bin/env: illegal option -- c
usage: env [-iv] [-P utilpath] [-S string] [-u name]
[name=value ...] [utility [argument ...]]
You'll have to trust me that running the command in a terminal window directly works as expected. The only difference is the escape characters in the mosquitto command to prevent the quotations messing up the command. Perhaps the escape chars are causing the problems?
I have no idea what the error is trying to tell me. Any advice would be greatly appreciated. Thanks.
EDIT - I've made sure chaining some basic commands (pwd, cd... etc) from Swift works. So it's definitely set up correctly to be able to run commands like this, I just don't know why it can't run the Mosquitto publish command.
The immediate reason for the error message is that /usr/bin/env does not have a -c option, apparently you mixed that up with /bin/bash -c "command ...". Also the env command can only start a single executable, not multiple commands chained together with &&.
The other problem is that the mosquitto_pub binary is not found when running your app from the Finder. As it turned out in the discussion, this program is installed (via Homebrew) in /usr/local/bin. That directory is usually in the search path of a Terminal shell, but not when an application is started from the Finder.
Using an absolute path for the program is one option:
let mosquittoCommand = "/usr/local/bin/mosquitto_pub --cert blahblah.pem ..."
As already said in the comments, it is easier to set the launch path (as an absolute path), the working directory where the command should be executed, and the command arguments as an array:
let task = Process()
task.launchPath = "/usr/local/bin/mosquitto_pub"
task.arguments = [
"--cert",
"blahblah.pem",
// ...
"-i",
"blahblahblah"
]
task.currentDirectoryPath = "/Users/Me/Desktop/certs"
This makes quoting the arguments and calling via the shell unnecessary.
Alternatively you can start the program via env (so that it is found at multiple possible locations) but then you have add "/usr/local/bin" to the search path:
// Add "/usr/local/bin" to search path:
var env = task.environment ?? [:]
if let path = env["PATH"] {
env["PATH"] = "/usr/local/bin:" + path
} else {
env["PATH"] = "/usr/local/bin"
}
task.environment = env
task.launchPath = "/usr/bin/env"
task.arguments = [
"mosquitto_pub",
"--cert",
"blahblah.pem",
// ...
"-i",
"blahblahblah"
]
task.currentDirectoryPath = "/Users/Me/Desktop/certs"
Finally, you can use
task.currentDirectoryURL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent("certs")
to make the working directory work with any user name.
I would like to run a shell command on many files that should match on a given filename regex. I found this code snippet that runs a shell command with arguments:
func shell(_ arguments: [String] = []) -> String {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? "unknown"
return output
}
It runs great, but it does not resolve the parameters:
shell(["ls", "~/Desktop/*.txt"])
Does not resolve the * to all txt files, it tries to only work on a file called *.txt. Is there some option I need to set on Process?
Thanks in advance for your help!
I just found out the answer! The resolving of * and other patterns is done by the shell, Process only runs a given command. So the solution is to create a shell and run the command in there: (will do some clean up in the code, but this works)
shell(["bash", "-c", "ls ~/Desktop/*.txt"])
I am trying to execute shell commands from swift program. below is my program
func executeCommand(command: String, args: [String]) -> String {
let task = Process()
task.launchPath = command
task.arguments = args
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String
return output
}
When I use the path as /bin and try to execute ls command it works and prints the output. but when i try to run
executeCommand(command: "export DEVELOPER_DIR=\"/Applications/Xcode.app/Contents/Developer/\"", args: [""])
it is not working. I tried to find the location of export but terminal says it is inbuilt. How do i execute the export DEVELOPER_DIR from my swift program?
I am making an OS X App that requires running an shell script. Here are my swift code:
func runTask(arguments: [String]) {
output.string = ""
task = NSTask()
task.launchPath = "/bin/bash"
task.arguments = arguments;
errorPipe = NSPipe()
outputPipe = NSPipe()
task.standardError = errorPipe
task.standardOutput = outputPipe
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didCompleteReadingFileHandle(_:)), name: NSFileHandleReadCompletionNotification, object: task.standardOutput!.fileHandleForReading)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didCompleteReadingFileHandle(_:)), name: NSFileHandleReadCompletionNotification, object: task.standardError!.fileHandleForReading)
errorPipe.fileHandleForReading.readInBackgroundAndNotify()
outputPipe.fileHandleForReading.readInBackgroundAndNotify()
task.launch()
}
func didCompleteReadingFileHandle(sender: NSNotification) {
let data: NSData = sender.userInfo![NSFileHandleNotificationDataItem] as! NSData;
let string = NSString(data: data, encoding: NSUTF8StringEncoding)!
// The output property is a NSTextView object
output.string?.appendContentsOf(String(string))
}
Now I tried calling the runTask method:
runTask(["/bin/echo", "1234"])
It says the following error:
/bin/echo: /bin/echo: cannot execute binary file
Now I went back into Terminal and typed in echo 1234 it runs perfectly without any trouble, now how do you get this to work? Thanks.
bash has three main modes of operation:
If you pass it -c "some command string", it'll execute that command string.
If you pass it a file path as an argument, it'll read commands from that file and execute them (i.e. execute the file as a shell script).
If you don't pass it any arguments, it'll read and execute commands from standard input.
Since you passed it the arguments "/bin/echo" and "1234", it's assuming you want mode 2, so it tries to read shell commands from /bin/echo, and fails. I'm not clear on exactly what you're trying to achieve, but I see several options that might be relevant:
If you're trying to execute a binary (e.g. /bin/echo), just execute that directly without using bash at all:
task.launchPath = "/bin/echo"
task.arguments = ["1234"]
If you need to execute a command string (i.e. if you need the shell to parse it before executing it, so e.g. wildcards get expanded, or there's more than one command, or...), use bash -c:
task.launchPath = "/bin/bash"
task.arguments = ["-c", "/bin/echo 1234; ls *"]
If you need to execute an actual script, i.e. a file with shell commands in it, then leave runTask alone, but pass it an actual script:
runTask(["/path/to/script", "scriptarg", "another argument"])
You're executing /bin/bash /bin/echo which doesn't work in Terminal.app either.
Remove /bin/bash
task.launchPath = "/bin/echo"
...
runTask(["1234"])