2

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!

Willeke
  • 14,578
  • 4
  • 19
  • 47

2 Answers2

1

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

CRD
  • 52,522
  • 5
  • 70
  • 86
  • Thanks for the response. I'm relatively new to swift and `FileManager` went mostly over my head. Some help on how exactly to use it would be great. When I mentioned `cd` I just meant making the command work for the user, as it didn't actually change the directory, so I thought I could catch any `cd`s, and change and keep track of the cwd with my own code. Using `cd`s to change the directory seems needlessly complex, and I would like to try to use `FileManager`. I also had permission problems while testing it. I revived `Operation not permitted` when I `cd`'d to the home directory and did `ls`. – Cameron Delong Aug 24 '20 at 01:23
  • You have 2 distinct issues (a) changing the current directory from your question – which you have done with `cd` which is a good way to do it; and (b) the permissions issue you mention in your comment. The latter will be due to the restrictions of the *sandbox*, you cannot just move anywhere in the filesystem from an application and access files/execute commands. You need to research the sandbox and determine if what you're trying to do is allowable, whether you can turn the sandbox off, etc. Here is [a page](https://developer.apple.com/documentation/security/app_sandbox) you can start at. – CRD Aug 24 '20 at 01:54
  • I would prefer to use `FileManager`, because it seems like it would make my code simpler, though I have no idea how I would use it to do what I'm trying to. Could you point me in the right direction? If I end up needing to use `cd` that the start of every command, the "All Files Entitlement" property is deprecated, which is what I would have guessed I needed to use. Is there something I would need to use instead? And would using the other method even avoid this problem? – Cameron Delong Aug 24 '20 at 02:51
  • Using `FileManager` vs. `cd` has no impact on your app's access to the filesystem – you *need* to understand the sandbox, whether you should use it and the limitations & risks of turning to off, etc. Security exists for a reason. Using `FileManager`, which is just a class like any other class, won't make your code simpler *per se*, but it will mean that the current directory will change for your whole app and not just the shell command you are running – you need to figure out whether that is good or bad for your situation. – CRD Aug 24 '20 at 03:27
  • Ah okay, I see. So how exactly do I turn off sandbox? More importantly, it says that you have to have sandbox on to distribute apps through the App Store... Is there any way around this besides distributing the app on my own? – Cameron Delong Aug 24 '20 at 13:43
  • Whether an app uses the sandbox or not is set in its build preferences. Trying to avoid the security of the sandbox is not advised – its called "hacking". One of the core features of the sandbox is gaining the rights to do something by asking the user; e.g. through them opening a file or responding to a dialog box – you will have seen such things as you use a Mac. You need to understand the sandbox, the methods for gaining and keeping permissions, and look at your app design in this light. You want to access a directory? Ask the user for permission. Look at is a voyage of discovery, enjoy! HTH – CRD Aug 24 '20 at 19:51
  • Okay I definitely phrased that wrong. I don't want to avoid the security of the sandbox, I'm just trying to figure out what I have to do to get the permissions I need to have a working terminal application. I would guess that that would require full disk access, because you can see and edit any file from the terminal? But I don't remember granting iTerm permissions when I installed it. Am I missing something important? Also thanks for all the help so far! – Cameron Delong Aug 24 '20 at 22:43
  • `iTerm` is not sandboxed. You need to think how the sandbox works and how you might achieve what you want to do within it. Do you really need to spawn a process like `zsh`? What rights, if any, does a spawned process inherit from its parent? When a user selects something in a standard file dialog your app gets access rights to it, apps can remember that right and use it next time they launch without asking the user again, etc. You need to research and understand the sandbox model and determine how to accomplish your task within it, or choose to operate outside the sandbox as `iTerm` does. – CRD Aug 24 '20 at 23:31
  • Okay, I think I'll just make my app sandboxed. I don't fully understand how the sandbox works, and I have no idea how I would have my app sandboxed but have it be able to read/write any file it needs to like a terminal can. Maybe I'll change it once I understand it better but for now that's what I think I'm gonna do. Thank you for all the help! – Cameron Delong Aug 25 '20 at 13:14
0

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
}
Jawad
  • 361
  • 3
  • 8