Home > Enterprise >  Swift CLI: run process, display its output and allow user input
Swift CLI: run process, display its output and allow user input

Time:11-12

I've been trying for some days now to find and equivalent to Ruby's exec in Swift.

I haven't yet been able to find a solution. Multiple sources online suggest how to forward predefined input to STDIN of the child process and out to read its output, but now how to start a child process and let it take control of the STDIN and STDOUT as if the Swift process was letting it take over.

The use case would be to run SSH commands and develop an in-house company tool, formerly in Ruby, that would allow similar features as heroku run /bin/sh for instance. I am already able to replicate things like heroku logs, since they donnot require user input.

The basic idea of starting a new process and attaching it to STDIN and STDOUT doesn't seem to be working as expected.

let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = ["bash", "-c", "read"]
task.standardInput = FileHandle.standardInput
task.standardOutput = FileHandle.standardOutput
task.standardError = FileHandle.standardError
task.launch()
task.waitUntilExit()

Any ideas or hints of how to achieve this ?

CodePudding user response:

This appears to be a bug in Apple's Foundation. The intended behaviour should be that not assigning any of those would get you your desired result. As the documentation says:

If this method isn’t used, the standard input is inherited from the process that created the receiver.

(Similar things are said for stdout and stderr too)

However, stdin doesn't seem to be actually connected, so your input is being received by your Swift process, rather than bash -c read.

According to this similar discussion here on Swift Forums about allowing users to enter passwords into a subprocess,

  • Process is a wrapper around posix_spawn, but calling posix_spawn directly works
  • Rust's Command and Python's subprocess.run also use posix_spawn, but they work correctly.
  • Building the SwiftFoundation framework from source and using the Process class from that, also works.

Based on the above points, I think this is a bug.

For now, you can use this workaround (this is also from the Swift Forums post)

let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", "read"]
let pipe = Pipe()
task.standardInput = pipe
let fileHandle = FileHandle(fileDescriptor: STDIN_FILENO)
fileHandle.readabilityHandler = { handle in
    let data = handle.availableData
    if data.count > 0 {
       pipe.fileHandleForWriting.write(data)
    }
}
try task.run()
task.waitUntilExit()
  • Related