Swift mac app, run terminal command without knowing the path (so it looks in every path in $PATH)?
You can execute the command via env
:
env utility argument ...
Example:
let path = "/usr/bin/env"
let arguments = ["ls", "-l", "/"]
let task = Process.launchedProcess(launchPath: path, arguments: arguments)
task.waitUntilExit()
env
locates the given utility using the $PATH
variable and
then executes it with the given arguments. It has additional
options to specify a different search path and additional
environment variables.
(This is not a feature of Swift but of macOS and many other operating systems.)
When started from the Finder (double-click) the PATH
may be different
from the PATH
in your shell environment. If necessary, you can add
additional directories:
var env = task.environment ?? [:]
if let path = env["PATH"] {
env["PATH"] = "/usr/local/bin:" + path
} else {
env["PATH"] = "/usr/local/bin"
}
task.environment = env
execlp
and friends also locate the executable using $PATH
but offer only the "raw" C interface.
How to access the Terminal's $PATH variable from within my mac app, it seems to uses a different $PATH
Adding bash Shell Path
The default shell paths can be found in /etc/paths
and /etc/path.d/
. One way to read the shell paths is to use the path_helper
command. Extending the code example above and using bash as the shell:
let taskShell = Process()
var envShell = ProcessInfo.processInfo.environment
taskShell.launchPath = "/usr/bin/env"
taskShell.arguments = ["/bin/bash","-c","eval $(/usr/libexec/path_helper -s) ; echo $PATH"]
let pipeShell = Pipe()
taskShell.standardOutput = pipeShell
taskShell.standardError = pipeShell
taskShell.launch()
taskShell.waitUntilExit()
let dataShell = pipeShell.fileHandleForReading.readDataToEndOfFile()
var outputShell: String = NSString(data: dataShell, encoding: String.Encoding.utf8.rawValue) as! String
outputShell = outputShell.replacingOccurrences(of: "\n", with: "", options: .literal, range: nil)
print(outputShell)
let task = Process()
var env = ProcessInfo.processInfo.environment
var path = env["PATH"]! as String
path = outputShell + ":" + path
env["PATH"] = path
task.environment = env
task.launchPath = "/usr/bin/env"
task.arguments = ["/bin/bash", "-c", "echo $PATH"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
var output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String
output = output.replacingOccurrences(of: "\n", with: "", options: .literal, range: nil)
print(output)
Note:
- This code example calls the shell in non-interactive mode. This means that it won't execute any user specific profiles; such as
/Users/*userid*/.bash_profile
. - The paths can be listed multiple times in the
PATH
environment variable. They will be traversed from left to right.
References
There are a couple of threads on application and shell PATH's for OS X which provide more context
How to set system wide environment variables on OS X Mavericks
and
Setting the system wide path environment variable in mavericks
How to access path sent by `open` command to Swift GUI app
The reason you can't find the arguments is because you didn't pass any arguments to the app in your command.
Unlike MyApp myFileURL
, open -a MyApp myFileURL
doesn't actually pass myFileURL
to MyApp
.
To actually pass any arguments to MyApp
, you need to specifically use --args
.
How to launch a terminal app on PATH in Swift on macOS?
A comment from @MartinR brought me to the right idea. Don't run the terminal command directly, but from a new shell. This way it will do the PATH resolution for you:
let shellProcess = new Process();
shellProcess.launchPath = "/bin/bash";
shellProcess.arguments = [
"-l",
"-c",
// Important: this must all be one parameter to make it work.
"mysqlsh --py -e 'print(\"Call from shell\")",
];
shellProcess.launch();
This example uses the MySQL shell as example, which is in /usr/local/bin
(unlike git, which is in /usr/bin
). Git worked already in the beginning, while mysqlsh did not. From the comment you can also see that it is important to make the mysqlsh call a complete and single parameter entry. If you split that then /bin/bash -c
will only execute mysqlsh
and not pass on the given shell parameters.
How do I run a terminal command in a Swift script? (e.g. xcodebuild)
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
Running terminal commands from Cocoa App in Swift failing: command not found but works using Command Line Tool in swift
Add "--login"
as the first task argument:
task.arguments = ["--login", "-c", "dayone2 new 'Hello'"]
and that should fix your error.
Explanation:
When you run Terminal the shell starts up as a login shell, from man sh
:
When bash is invoked as an interactive login shell, or as a
non-inter-active shell with the--login
option, it first reads and executes commands from the file/etc/profile
, if that file exists. After reading that file, it looks for~/.bash_profile
,~/.bash_login
, and~/.profile
, in that order, and reads and executes commands from the first one that exists and is readable.
Among other things the commands in these files typically set the $PATH
environment variable, which defines the search path the shell uses to locate a command.
When you run your command line tool in the Terminal it inherits this environment variable and in turn passes it on to the shell it invokes to run your dayone2
command.
When you run a GUI app there is no underlying shell and the $PATH
variable is set to the system default. Your error "command not found" indicates that your dayone2
command is not on the default path.
HTH
Swift Run command line using processing(), but return Error Domain=NSPOSIXErrorDomain Code=13 Permission denied
I think you've misunderstood some features of how Process
works. The documentation
A process operates within an environment defined by the current values
for several items: the current directory, standard input, standard
output, standard error, and the values of any environment variables.
By default, an Process object inherits its environment from the
process that launches it.
So the process will run within the current directory of the parent process. It looks like you are trying to change the current directory to
/Users/donghanhu/Documents/TestFolder
By using the launchPath
of the Process
, but launchPath
should be set to the executable you want to run in the subprocess. In this case I think you want the launchProcess
to be "/bin/ls"
because you are trying to run an ls
command.
So if you want a Process that will use ls
to list the content of the folder /Users/donghanhu/Documents/TestFolder
it would be:
import Foundation
let task = Process()
task.launchPath = "/bin/ls"
task.arguments = ["/Users/donghanhu/Documents/TestFolder"]
do {
try task.run()
} catch {
print("something went wrong, error: \(error)")
}
task.waitUntilExit()
Run homebrew package from Swift Command Line Tool
So I've found multiple solutions :
Solution 1 : Adding $PATH to env variables
Edit Scheme > Arguments > Environment Variables : Add PATH
and set its value to ${PATH}
Solution 2 : Add Homebrew path to env variables
Edit Scheme > Arguments > Environment Variables : Add PATH
and set its value to PATH=${PATH}:/opt/homebrew/bin:/opt/homebrew/sbin
Solution 2 : Launch in terminal
Edit Scheme > Options : Set Console
to Use Terminal
. But I found this option very buggy with random errors.
Related Topics
Overriding Static Vars in Subclasses Swift 1.2
Swift Associated Types and Protocol Inheritance
iOS Swift - Uitableviewcell Custom Subclass Not Displaying Content
Swift 2, Protocol Extensions & Respondstoselector
How to Pass/Get Core Data Context in Swiftui Mvvm Viewmodel
How to Draw Dashed Line in Arkit (Scenekit) Like in the Measure App
Resizing Uimage When Using Sf Symbols - Uiimage(Systemname:)
How to Display an Int Without Commas
How to Avoid Duplicate Key Error in Swift When Iterating Over a Dictionary
Cast Any to Float Always Fails in Swift4.1
Create Endless Cgpath Without Framedrops
Firebase Storage Overwriting Files
Comparing Non-Optional Any to Nil Is Always False
How to Initialize a Mlmultiarray in Coreml
How to Catch Error When Setting Launchpath in Nstask
Display Table View When Searchbar (From Searchcontroller) Begin Edited Swift