Last active
October 24, 2024 05:57
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@main | |
struct Main { | |
static func main() { | |
let response = ShellCommand.run("echo hi!") | |
print("Exit code: \(response.exitCode)") | |
ShellCommand.streamSudo("brew services restart mysql") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Took me forever to learn how to run commands from swift in a way that
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
andstream
commands, with those commands supporting sudo as well as non sudo commands.