Skip to content

Instantly share code, notes, and snippets.

@myleshyson
Last active July 10, 2024 20:36
Show Gist options
  • Save myleshyson/1bf057c2174332d6bcff81448dd8d095 to your computer and use it in GitHub Desktop.
Save myleshyson/1bf057c2174332d6bcff81448dd8d095 to your computer and use it in GitHub Desktop.
Execute CLI Commands with Swift. Supports generating output, streaming output, and running sudo commands.
import Foundation
struct ShellCommand {
@discardableResult
static func stream(_ command: String) -> Int32 {
let outputPipe = Pipe()
let task = self.createProcess([command], outputPipe)
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in self.streamOutput(outputPipe, fileHandle) }
do {
try task.run()
} catch {
return -1
}
task.waitUntilExit()
return task.terminationStatus
}
@discardableResult
static func streamSudo(_ command: String) -> Int32 {
let testSudo = self.createSudoTestProcess()
do {
try testSudo.run()
testSudo.waitUntilExit()
} catch {
return -1
}
if testSudo.terminationStatus == 0 {
return self.stream(command)
}
let pipe = Pipe()
let sudo = self.createProcess(["sudo " + command], pipe)
pipe.fileHandleForReading.readabilityHandler = { fileHandle in self.streamOutput(pipe, fileHandle) }
do {
try sudo.run()
} catch {
return -1
}
if tcsetpgrp(STDIN_FILENO, sudo.processIdentifier) == -1 {
return -1
}
sudo.waitUntilExit()
return sudo.terminationStatus
}
@discardableResult
static func runSudo(_ command: String) -> ShellResponse {
let testSudo = self.createSudoTestProcess()
do {
try testSudo.run()
testSudo.waitUntilExit()
} catch {
return ShellResponse(output: "", exitCode: -1)
}
if testSudo.terminationStatus == 0 {
return self.run(command)
}
let pipe = Pipe()
var output = ""
let sudo = self.createProcess(["sudo " + command], pipe)
pipe.fileHandleForReading.readabilityHandler = { fileHandle in self.saveOutput(pipe, fileHandle, &output) }
do {
try sudo.run()
} catch {
return ShellResponse(output: "", exitCode: -1)
}
if tcsetpgrp(STDIN_FILENO, sudo.processIdentifier) == -1 {
return ShellResponse(output: "", exitCode: -1)
}
sudo.waitUntilExit()
return ShellResponse(output: output, exitCode: sudo.terminationStatus)
}
@discardableResult
static func run(_ command: String, printCommand: Bool = false, streamOutput: Bool = false, withSudo: Bool = false) -> ShellResponse {
let outputPipe = Pipe()
let task = self.createProcess([command], outputPipe)
var commandOutput = ""
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in self.saveOutput(outputPipe, fileHandle, &commandOutput) }
do {
try task.run()
} catch {
return ShellResponse(output: commandOutput, exitCode: -1)
}
task.waitUntilExit()
return ShellResponse(output: commandOutput, exitCode: task.terminationStatus)
}
private static func createProcess(_ arguments: [String], _ pipe: Pipe) -> Process {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c"] + arguments
task.standardOutput = pipe
task.standardError = pipe
return task
}
private static func createSudoTestProcess() -> Process {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sudo -nv"]
task.standardOutput = nil
task.standardError = nil
task.standardInput = nil
return task
}
private static func saveOutput(_ pipe: Pipe, _ fileHandle: FileHandle, _ result: UnsafeMutablePointer<String>? = nil) -> Void {
let data = fileHandle.availableData
guard data.count > 0 else {
pipe.fileHandleForReading.readabilityHandler = nil
return
}
if let line = String(data: data, encoding: .utf8) {
result?.pointee.append(line)
}
}
private static func streamOutput(_ pipe: Pipe, _ fileHandle: FileHandle) -> Void {
let data = fileHandle.availableData
guard data.count > 0 else {
pipe.fileHandleForReading.readabilityHandler = nil
return
}
if let line = String(data: data, encoding: .utf8) {
print(line)
}
}
}
struct ShellResponse {
var output: String
var exitCode: Int32
init(output: String = "", exitCode: Int32 = 0) {
self.output = output
self.exitCode = exitCode
}
}
@main
struct Main {
static func main() {
let response = ShellCommand.run("echo hi!")
print("Exit code: \(response.exitCode)")
ShellCommand.streamSudo("brew services restart mysql")
}
}
@myleshyson
Copy link
Author

Took me forever to learn how to run commands from swift in a way that

  1. returns an output
  2. lets you stream the output
  3. lets you do either of those as sudo

Thanks to this I was able to get sudo commands working. However I noticed after the first command, if I called subsequent sudo commands they would run in the background. Still not entirely sure why, but I know it has something to do with the tcsetpgrp call. To get around this I figured the easiest path was to separate sudo calls into their own methods, and in those methods test if a user has sudo privileges first before doing anything.

If anyone comes across this who knows of a more stream lined way to do what I'm doing I'm all ears! Ideally you can just have run and stream commands, with those commands supporting sudo as well as non sudo commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment