Launch Safari (not default browser) at URL in Swift? - swift

I need to open Safari specifically, not the default browser. If Chrome/Firefox/whatever is the default browser my app still needs to open the URL in Safari.
I've looked online but all I can see is for opening in default browser like this...
let url = URL(string: host)!
NSWorkspace.shared.open(url)
...which works for me only when Safari is the users default.
How can I open a URL in Safari specifically?
Not duplicate - the other question is not in Swift...

Okay I found a solution but it's a bit of a hack...
Shell function for making bash commands:
func shell(command: String) -> String {
var output = ""
var error = ""
do {
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
let outputPipe = Pipe()
task.standardOutput = outputPipe
let errorPipe = Pipe()
task.standardError = errorPipe
try task.run()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
output = NSString(data: outputData, encoding: String.Encoding.utf8.rawValue)! as String
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
error = NSString(data: errorData, encoding: String.Encoding.utf8.rawValue)! as String
}
catch let err as NSError{
output = err.localizedDescription
}
return error + "\n" + output + "\n"
}
In bash you can use: open -a safari www.blahblahblah.com
So in Swift I can implement this like so:
shell(command: "open -a safari " + host)
Bit of a hack but it works

Related

How to launch terminal and pass it a command using Swift

I want to programmatically open a terminal and paste a command into it like "cd /Users/...".
I can start a terminal using this code, but I don't know how to execute command
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { return }
let path = "/bin"
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = \[path\]
NSWorkspace.shared.openApplication(at: url, configuration: configuration, completionHandler: nil)
It is very important to use the sandbox, the Process command is not suitable.
If all you are trying to achieve is to open a terminal on a specific location all you have to do is to use this code:
let pathToOpen = "/Users/admin/Desktop"
let url = URL(string:"terminal://"+pathToOpen)!
NSWorkspace.shared.open(url)
If you are trying just to run a command in terminal and show the output in your app, here is another useful code snippet:
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
}
//Usage:
shell("yourCommandHere")
//please note that you are not allowed to use commands that require sudo

swift command line tool for git commands, but its no

I would like to create a tool for git commands using swift.
I'm getting git error usage: git [--version] [--help] [-C ]
[-c =].
Is there any way to distribute it as a command-line package and using in the swift file?
import Foundation
struct Task {
static let shared = Task()
func run(with args: String...){
let task = Process()
task.launchPath = "/usr/bin/git"
task.arguments = ["-c", args.joined(separator: " ")]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
print(output)
task.waitUntilExit()
}
}
let task = Task.shared
task.run(with: "status")
task.run(with: "fetch --all")
task.run(with: "add --all")
You are using deprecated methods in your code and there are some other things missing.
First we should set the shell to use
func run(with args: String...){
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
Then instead of using the deprecated launchPath we build a string with the full command and set it as the arguments for the task
let arguments = "/usr/bin/git \(args.joined(separator: " "))"
task.arguments = ["-c", arguments]
I also think it is a good idea to handle any errors by checking standard error
let pipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = pipe
task.standardError = errorPipe
Instead of using the deprecated launch method use run and read both standard out and standard error
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if !data.isEmpty {
if let output = String(data: data, encoding: .utf8) {
print(output)
}
}
let error = errorPipe.fileHandleForReading.readDataToEndOfFile()
if !error.isEmpty {
if let errorMessage = String(data: error, encoding: .utf8) {
print(errorMessage)
}
}
} catch {
print(error)
}
For a simple command it might be worth having the handling of standard output and standard error in an if/else so feel free to change that but for more complicated commands dealing for example with multiple files it might produce both output and errors

Launch sudo command from MacOS App Swift

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.

Running command from Cocoa freezes my app

I use this code to run a command from the Cocoa Application:
extension String {
func runAsCommand() -> String {
let pipe = NSPipe()
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", String(format:"%#", self)]
task.standardOutput = pipe
let file = pipe.fileHandleForReading
task.launch()
if let result = NSString(data: file.readDataToEndOfFile(), encoding: NSUTF8StringEncoding) {
return result as String
}
else {return "--- Unable to initialize string from file data ---"}
}
}
Example:
"command".runAsCommand()
The problem is, that this freezes my application. For example, if the command is to open some other app, such as Firefox, it freezes my host app until Firefox is exited. I don't want that behaviour. What should I do?

Command line "launch path not accessible"

I recently found out that I can create Swift command line scripts.
I decided to see if I could build my Xamarin project using it.
Unfortunately I am getting the following error and I don't know how to fix it.
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'launch path not accessible'
Here is my script:
#!/usr/bin/env swift
import Foundation
print("Building Script")
let fileManager = NSFileManager.defaultManager()
let path = fileManager.currentDirectoryPath
func shell(launchPath: String, arguments: [String] = []) -> NSString? {
let task = NSTask()
task.launchPath = launchPath
task.arguments = arguments
let pipe = NSPipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: NSUTF8StringEncoding)
return output
}
if let output = shell("/Applications/Xamarin\\ Studio.app/Contents/MacOS/mdtool", arguments: ["-v build", "\"--configuration:Beta|iPhone\"", "MyApp.iOS.sln"]) {
print(output)
}
Any thoughts?
I think the problem is that you actually want to execute the shell and have it execute the mdtool, rather than directly execute mdtool
Try passing "/bin/bash" as the launchpath, and then include the path to mdtool as part of the argument string.
Recently I encounter a similar problem, but my solution is different.
I fix the problem by changing the permission mode of the script. Specifically, chmod 777 xxx. The key is to give the execution permission.
Let's conduct a controlled experiment to verify it:
prepare a script with path /tmp/a.sh and permission 666
The content of /tmp/a.sh:
#!/bin/sh
echo aaa
prepare a swift script to launch the script:
import Foundation
let fileManager = FileManager.default
let path = fileManager.currentDirectoryPath
func shell(launchPath: String, arguments: [String] = []) -> NSString? {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return output
}
if let output = shell(launchPath: "/tmp/a.sh", arguments: []) {
print(output)
}
The output is:
... *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'launch path not accessible'
change the permission of /tmp/a.sh to 777 (by granting the execution permission) and then re-run the swift script:
aaa
My version that is intended to highlight all sorts of expected results. You can replace ParsingError.encodingFailure and OS.Error.processFailure with your own Error implementations.
/// Getting a `Result` from a shell command
public func process(_ command: String) -> Result<String, Swift.Error> {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", command]
let outputPipe = Pipe()
process.standardOutput = outputPipe
let errorPipe = Pipe()
process.standardError = errorPipe
process.launch()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: outputData, encoding: .utf8) else {
return .failure(ParsingError.encodingFailure(data: outputData))
}
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
guard let errorOutput = String(data: errorData, encoding: .utf8) else {
return .failure(ParsingError.encodingFailure(data: errorData))
}
process.waitUntilExit()
let status = Int(process.terminationStatus)
if status == 0, errorOutput.isEmpty {
return .success(output)
} else {
return .failure(OS.Error.processFailure(code: status, message: errorOutput))
}
}