Search code examples
swiftmacoshomebrewzsh

How to use zsh with profiles in swift and get output?


I'm trying to work with zsh in Swift, and homebrew. I run into this issue every time I run my code. It can't find the command brew. I'm trying to run the brew list command through Sswift and get the output or listed packages, and then continue to display that. Is there a way to include the zsh profile with brew in the Swift command, but still produce output?

func run(_ cmd: String) -> String? {
    let pipe = Pipe()
    let process = Process()
    process.launchPath = "/usr/local/Home"
    process.arguments = ["-c", String(format:"%@", cmd)]
    process.standardOutput = pipe
    let fileHandle = pipe.fileHandleForReading
    process.launch()
    return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
}

func test(){
    do {
        run("brew list")
    } catch {
        print("errpr")
    }
}

Solution

  • There's a bunch of issues here. I'll tackle them one by one:

    process.launchPath = "/usr/local/Home"
    

    The launch path is the actual executable you want this Process to launch. Unless /usr/local/Home is some binary or script we don't know about, that's probably not what you want.

    Furthermore, there's no involvement of zsh, anywhere in the code you showed.

    String(format:"%@", cmd)
    

    This does precisely nothing. You're creating a new string from a format string that only includes the value of cmd, which is ... already a String. In Objective C this would have had the effect of copying cmd, but given that String is a value type in Swift, this copying has no effect.

    run("brew list")
    

    This is a misunderstanding of how program arguments work. At the OS level, program arguments are an array of strings. Not just one large string.

    When you do something like brew list in a shell (like zsh), it's the shell's job to parse apart this string by spaces, and come up with an array of strings of individual arguments to pass to the OS (via execv and friends). There's a few ways to invoke brew. The more "roundabout"

    import Foundation
    
    func run(_ cmd: String) -> String? {
        let process = Process()
        process.launchPath = "/bin/zsh"
        process.arguments = [
            "-l", // Login shell
            "-c", // Evaluate input from argument
            cmd // The commands to evaluate
        ]
        
        let pipe = Pipe()
        process.standardOutput = pipe
        let fileHandle = pipe.fileHandleForReading
        
        process.launch()
        return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
    }
    
    let output = run("brew list")
    print(output ?? "<nil>")
        
    

    Though even with -l, I can't get this to read my path configuration that I set in ~/.zshrc. I'm not sure why this is.

    Instead, you can just use a more direct way and just invoke brew directly, with no shell middleman.

    import Foundation
    
    func runBrewCommand(_ args: [String]) -> String? {
        let process = Process()
        process.launchPath = "/opt/homebrew/bin/brew"
        process.arguments = args
        
        let pipe = Pipe()
        process.standardOutput = pipe
        let fileHandle = pipe.fileHandleForReading
        
        process.launch()
        return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
    }
    
    let output = runBrewCommand(["list"])
    print(output ?? "<nil>")