aboutsummaryrefslogtreecommitdiff
path: root/Sources/phbar/Models/Runner.swift
blob: a98d34aecc5c5762a2981d0e85ea9d93a647f0a1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Foundation

/// Runs a shell command and returns its stdout as cells (one per line).
///
/// Blocking — always call from a background context (e.g. `Task.detached`).
enum Runner {
	static func run(_ command: String, env: [String: String] = [:]) -> [String] {
		let process = Process()
		process.executableURL = URL(fileURLWithPath: "/bin/sh")
		process.arguments = ["-c", command]

		var environment = ProcessInfo.processInfo.environment
		environment.merge(env) { _, new in new }
		process.environment = environment

		let stdout = Pipe()
		let stderr = Pipe()
		process.standardOutput = stdout
		process.standardError = stderr

		guard let _ = try? process.run() else {
			return ["(error)"]
		}

		let data = stdout.fileHandleForReading.readDataToEndOfFile()
		let errData = stderr.fileHandleForReading.readDataToEndOfFile()
		process.waitUntilExit()

		if process.terminationStatus != 0 {
			let reason = String(data: errData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
			FileHandle.standardError.write("phbar: `\(command)` failed: \(reason ?? "exit \(process.terminationStatus)")\n".data(using: .utf8)!)
		}

		return parse(String(data: data, encoding: .utf8) ?? "")
	}

	/// Split output into cells: one per line. A single trailing newline (from a
	/// final `\n`) is dropped; blank lines elsewhere are kept so a script can
	/// emit intentional gaps.
	static func parse(_ text: String) -> [String] {
		var lines = text.components(separatedBy: "\n")
		if lines.last == "" { lines.removeLast() }
		return lines
	}
}