Run Shell Script from Xcode button - swift

I'm trying to get a button in Xcode to run a shell script with clicked.
This works
#IBAction func test(_ sender: NSButton) {
let path = "/usr/bin/say"
let arguments = ["hello world"]
sender.isEnabled = false
let task = Process.launchedProcess(launchPath: path, arguments: arguments)
task.waitUntilExit()
sender.isEnabled = true
}
But when I try this it does not work to run a script from the Desktop
#IBAction func test(_ sender: NSButton) {
let path = "/bin/bash"
let arguments = ["~/Desktop/test.sh"]
sender.isEnabled = false
let task = Process.launchedProcess(launchPath: path, arguments: arguments)
task.waitUntilExit()
sender.isEnabled = true
}
I get this error output in Xcode
/bin/bash: ~/Desktop/test.sh: No such file or directory
If anyone can help me with some help or example that would great. Thank you.

Turn off Xcode sandbox mode, it will fix the issue

func shell(_ args: String) -> String {
var outstr = ""
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", args]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
outstr = output as String
}
task.waitUntilExit()
return outstr
}
This Function returns output of Bash Script you're trying to run
let cmd = "for i in $(ifconfig -lu); do if ifconfig $i | grep -q \"status: active\" ; then echo $i; fi; done"
Above Code Demonstrate how to use it.

Related

Redirect the output of Terminal command to TextView

I want to execute a Terminal command in my Application and redirect the Terminal output of this command to a TextView (content_scroller). If I run the Application with Apple+R from within Xcode the Progress of this Terminal command is refreshed as it should. But ... If I started the Application the normal way only the first line of terminal output is shown but there is no refresh/new lines anymore. But why? Is there a way to loop the request of the actual output? Here is mit Swift 5 Code:
func syncShellExec(path: String, args: [String] = []) {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let filelHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
filelHandler.readabilityHandler = { pipe in
let data = pipe.availableData
if let line = String(data: data, encoding: .utf8) {
DispatchQueue.main.sync {
self.content_scroller.string += line
self.content_scroller.scrollToEndOfDocument(nil)
}
}
process.waitUntilExit()
filelHandler.readabilityHandler = nil
}
Should be able to direct output straight to text view if I understand your question correctly. Something like the following outputs an error (I didn't test it.)
import Cocoa
func syncShellExec(path: String, args: [String] = []) {
var status : Int32
var dataRead : Data
var stringRead :String?
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let txtView = NSTextView()
let fileHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()
process.waitUntilExit()
status = process.terminationStatus
dataRead = fileHandler.readDataToEndOfFile()
stringRead = String.init(data: dataRead, encoding: String.Encoding.utf8)
if (status != 0) {
txtView.string.append("Terminated with error.\n")
txtView.string.append(stringRead!)
}
}

How to run shell command in Swift?

I want to run shell command using NSButton in Swift. The IBAaction and execute function I use cannot work, pls help me, thanks!
#IBAction func Openfolder(_ sender: NSButton) {
_ = execute(command:"open " + "~/Downloads/")
}
func execute(command: String) -> String {
var arguments:[String] = []
arguments.append("-c")
arguments.append( command )
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return(NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String)
}

Running terminal commands in in cocoa app

I facing a problem when running my code on cocoa app to run some command line scripts
This function run smoothly when using Command line tool but when using full cocoa app with some On Off UI it not working at all
My script should turn on/off the http & https proxy
Here is my function:
private func runTask(_ cmd: String) {
// Create a Task instance
let task = Process()
// Set the task parameters
task.launchPath = "/bin/sh"
task.arguments = ["-c", String(format:"%#", cmd)]
// Create a Pipe and make the task
// put all the output there
let pipe = Pipe()
task.standardOutput = pipe
// Launch the task
task.launch()
// Get the data
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return }
print(output)
}
And here is my full ViewController class:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func onButtonTapped(_ sender: NSButton) {
print("onButtonTapped")
let selected: Switch = .on
let listOfNetworkCommands: String = [
#"networksetup -setwebproxystate "Wi-fi" \#(selected)"#, // switch http proxy
#"networksetup -setsecurewebproxystate "Wi-fi" \#(selected)"#, // switch https proxy
#"networksetup -setpassiveftp "Wi-fi" \#(selected)"# // switch passive ftp
].joined(separator: " && ")
runTask(listOfNetworkCommands)
}
#IBAction func offButtonTapped(_ sender: NSButton) {
print("onButtonTapped")
let selected: Switch = .off
let listOfNetworkCommands: String = [
#"networksetup -setwebproxystate "Wi-fi" \#(selected)"#, // switch http proxy
#"networksetup -setsecurewebproxystate "Wi-fi" \#(selected)"#, // switch https proxy
#"networksetup -setpassiveftp "Wi-fi" \#(selected)"# // switch passive ftp
].joined(separator: " && ")
runTask(listOfNetworkCommands)
}
enum Switch: String {
case on, off
}
private func runTask(_ cmd: String) {
// Create a Task instance
let task = Process()
// Set the task parameters
task.launchPath = "/bin/sh"
task.arguments = ["-c", String(format:"%#", cmd)]
// Create a Pipe and make the task
// put all the output there
let pipe = Pipe()
task.standardOutput = pipe
// Launch the task
task.launch()
// Get the data
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return }
print(output)
}
}
Any idea why my function not triggered in the cocoa app?
Simple answer is found by disabling App Sandbox in your Cocoa Application (found under your Project app target > Capabilities tab > App Sandbox switch). You'll find that you're being blocked by a sandbox exception. Disabling sandboxing should fix your issue.
You can also see this in Console.app if you filter for your app name or the sandboxd process. You'll likely have an entry like this when sandboxing is enabled:
error 00:21:57.502273 +0000 sandboxd Sandbox: sh(17363) deny(1) file-read-data /dev/ttys003

SWIFT 3 - Can I "stream" the output from a bash command to an output window? [duplicate]

I'm using an NSTask to run rsync, and I'd like the status to show up in the text view of a scroll view inside a window. Right now I have this:
let pipe = NSPipe()
task2.standardOutput = pipe
task2.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: NSASCIIStringEncoding)! as String
textView.string = output
And that get's me the some of the statistics about the transfer, but I'd like to get the output in real time, like what get's printed out when I run the app in Xcode, and put it into the text view. Is there a way to do this?
Since macOS 10.7, there's also the readabilityHandler property on NSPipe which you can use to set a callback for when new data is available:
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.readabilityHandler = { pipe in
if let line = String(data: pipe.availableData, encoding: NSUTF8StringEncoding) {
// Update your view with the new text here
print("New ouput: \(line)")
} else {
print("Error decoding data: \(pipe.availableData)")
}
}
task.launch()
I'm surprised nobody mentioned this, as it's a lot simpler.
(See Patrick F.'s answer for an update to Swift 3/4.)
You can read asynchronously from a pipe, using notifications.
Here is a simple example demonstrating how it works, hopefully that
helps you to get started:
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NSNotificationCenter.defaultCenter().addObserverForName(NSFileHandleDataAvailableNotification,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.length > 0 {
if let str = NSString(data: data, encoding: NSUTF8StringEncoding) {
print("got output: \(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NSNotificationCenter.defaultCenter().removeObserver(obs1)
}
}
var obs2 : NSObjectProtocol!
obs2 = NSNotificationCenter.defaultCenter().addObserverForName(NSTaskDidTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NSNotificationCenter.defaultCenter().removeObserver(obs2)
}
task.launch()
Instead of print("got output: \(str)") you can append the received
string to your text view.
The above code assumes that a runloop is active (which is the case
in a default Cocoa application).
This is the update version of Martin's answer above for the latest version of Swift.
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "echo 1 ; sleep 1 ; echo 2 ; sleep 1 ; echo 3 ; sleep 1 ; echo 4"]
let pipe = Pipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
print("got output: \(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stdout from process")
NotificationCenter.default.removeObserver(obs1)
}
}
var obs2 : NSObjectProtocol!
obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: task, queue: nil) { notification -> Void in
print("terminated")
NotificationCenter.default.removeObserver(obs2)
}
task.launch()
I have an answer which I believe is more clean than the notification approach, based on a readabilityHandler. Here it is, in Swift 5:
class ProcessViewController: NSViewController {
var executeCommandProcess: Process!
func executeProcess() {
DispatchQueue.global().async {
self.executeCommandProcess = Process()
let pipe = Pipe()
self.executeCommandProcess.standardOutput = pipe
self.executeCommandProcess.launchPath = ""
self.executeCommandProcess.arguments = []
var bigOutputString: String = ""
pipe.fileHandleForReading.readabilityHandler = { (fileHandle) -> Void in
let availableData = fileHandle.availableData
let newOutput = String.init(data: availableData, encoding: .utf8)
bigOutputString.append(newOutput!)
print("\(newOutput!)")
// Display the new output appropriately in a NSTextView for example
}
self.executeCommandProcess.launch()
self.executeCommandProcess.waitUntilExit()
DispatchQueue.main.async {
// End of the Process, give feedback to the user.
}
}
}
}
Please note that the Process has to be a property, because in the above example, given that the command is executed in background, the process would be deallocated immediately if it was a local variable. Thanks for your attention.

How to create a button that can exit the application and restart the system when clicked in a Cocoa Application?

Our current application needs to reboot the system and exit the application cleanly on a button click however once either of the code runs i.e. code to restart or code to exit app , the other code will not run.Our application currently reloads after the reboot since it is not close properly before the system reboot.
Button code which needs to restart and exit app:
#IBAction func exit2(sender: AnyObject) {
let task = NSTask()
let pipe = NSPipe()
task.standardOutput = pipe
//Code to reboot the system
task.launchPath = "/bin/bash/"
task.arguments = ["-c", "osascript -e 'tell app \"System Events\" to restart'"]
let file:NSFileHandle = pipe.fileHandleForReading
task.launch()
task.waitUntilExit()
let data = file.readDataToEndOfFile()
datastring1 = NSString(data: data, encoding: NSUTF8StringEncoding)!
//Code to close the application
NSApplication.sharedApplication().terminate(self)
}
#IBAction func restartAppButton(_ sender: Any) {
if let path = Bundle.main.resourceURL?.deletingLastPathComponent().deletingLastPathComponent().absoluteString {
NSLog("restart \(path)")
_ = Process.launchedProcess(launchPath: "/usr/bin/open", arguments: [path])
NSApp.terminate(self)
}
}