1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-01 17:20:09 +02:00

♻️ Use discardableResult

- Removed ShellProtocol.quiet(), now that pipe() is discardable
- Detecting PHP versions is also discardable
- The system command is also discardable

This is a nice quality of life change overall, and gets rid of a couple
of silly `_ =` assignments.
This commit is contained in:
2026-02-17 15:17:34 +01:00
parent d0ce16fad2
commit 9856840533
27 changed files with 55 additions and 70 deletions

View File

@@ -122,7 +122,7 @@ class Actions {
try! container.filesystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
// Tell php-cgi to run the PHP and output as an .html file
await container.shell.quiet("\(paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
await container.shell.pipe("\(paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}

View File

View File

@@ -18,7 +18,7 @@ func brew(
_ command: String,
sudo: Bool = false,
) async {
await container.shell.quiet("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
await container.shell.pipe("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
}
/**
@@ -37,9 +37,9 @@ func sed(
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if container.filesystem.fileExists("\(container.paths.binPath)/gsed") {
await container.shell.quiet("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
await container.shell.pipe("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {
await container.shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
await container.shell.pipe("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
}
}
@@ -71,20 +71,3 @@ func delay(seconds: Double) async {
func url(_ string: String) -> URL {
return URL(string: string)!
}
/**
Execute a script with administrative privileges.
*/
func sudo(_ script: String) throws {
let source = "do shell script \"\(script)\" with administrator privileges"
Log.info("Running script via AppleScript as administrator: `\(source)`")
let appleScript = NSAppleScript(source: source)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if eventResult == nil {
throw AdminPrivilegeError(kind: .applescriptNilError)
}
}

View File

@@ -87,7 +87,7 @@ class RealFileSystem: FileSystemProtocol {
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = container.shell.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
container.shell.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks

View File

@@ -48,7 +48,7 @@ class Application {
(This will open the app if it isn't open yet.)
*/
@objc public func open(arg: String) {
Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(arg)\"") }
Task { await container.shell.pipe("/usr/bin/open -a \"\(name)\" \"\(arg)\"") }
}
/**

View File

@@ -11,6 +11,7 @@ import Foundation
/**
Run a simple blocking Shell command on the user's own system.
*/
@discardableResult
public func system(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/sh"

View File

@@ -218,7 +218,7 @@ class PhpEnvironments {
}
public func reloadPhpVersions() async {
_ = await self.detectPhpVersions()
await self.detectPhpVersions()
}
/**
@@ -228,6 +228,7 @@ class PhpEnvironments {
Returns a `Set<String>` of installations that are considered valid.
*/
@discardableResult
public func detectPhpVersions() async -> Set<String> {
let files = await container.shell.pipe("ls \(container.paths.optPath) | grep php@").out

View File

@@ -127,13 +127,13 @@ class PhpHelper {
if !container.filesystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
await container.shell.quiet("ln -s \(source) \(destination)")
await container.shell.pipe("ln -s \(source) \(destination)")
return
}
if !App.shared.container.filesystem.isSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
await container.shell.quiet("ln -fs \(source) \(destination)")
await container.shell.pipe("ln -fs \(source) \(destination)")
return
}

View File

@@ -11,6 +11,7 @@ import Foundation
extension InternalSwitcher {
typealias FixApplied = Bool
@discardableResult
public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied {
// Early exit if Valet is not installed
if !Valet.installed {

View File

@@ -53,7 +53,7 @@ class InternalSwitcher: PhpSwitcher {
for formula in versions {
if Valet.installed {
Log.info("Ensuring that the Valet configuration is valid...")
_ = await self.ensureValetConfigurationIsValidForPhpVersion(formula)
await self.ensureValetConfigurationIsValidForPhpVersion(formula)
}
Log.info("Will start PHP \(version)... (primary: \(version == formula))")
@@ -112,7 +112,7 @@ class InternalSwitcher: PhpSwitcher {
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacing(".", with: "")
await container.shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
await container.shell.pipe("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}

View File

@@ -185,6 +185,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
return .out(stdOut, stdErr)
}
@discardableResult
func pipe(_ command: String) async -> ShellOutput {
let process = getShellProcess(for: command)
@@ -220,6 +221,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
}
}
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let process = getShellProcess(for: command)
@@ -285,10 +287,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
}
}
func quiet(_ command: String) async {
_ = await self.pipe(command)
}
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,

View File

@@ -22,6 +22,7 @@ protocol ShellProtocol {
let output = Shell.sync("php -v")
```
*/
@discardableResult
func sync(_ command: String) -> ShellOutput
/**
@@ -33,6 +34,7 @@ protocol ShellProtocol {
let output = await Shell.pipe("php -v")
```
*/
@discardableResult
func pipe(_ command: String) async -> ShellOutput
/**
@@ -43,14 +45,9 @@ protocol ShellProtocol {
- Parameter timeout: Timeout in seconds. If the command exceeds this, it is terminated.
- Returns: The shell output. If the command times out, returns empty output.
*/
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
/**
Run a command asynchronously, without returning the output of the command.
Returns the most relevant output (prefers error output if it exists).
*/
func quiet(_ command: String) async
/**
Runs a command asynchronously, and fires closure with `stdout` or `stderr` data as it comes in.
@@ -61,6 +58,7 @@ protocol ShellProtocol {
Unlike `sync`, `pipe` and `quiet`, you can capture both `stdout` and `stderr` with this mechanism.
The end result is still the most relevant output (where error output is preferred if it exists).
*/
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,

View File

@@ -19,6 +19,7 @@ public class TestableShell: ShellProtocol {
var expectations: [String: BatchFakeShellOutput] = [:]
@discardableResult
func sync(_ command: String) -> ShellOutput {
// This assertion will only fire during test builds
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
@@ -30,19 +31,18 @@ public class TestableShell: ShellProtocol {
return expectation.syncOutput()
}
func quiet(_ command: String) async {
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
}
@discardableResult
func pipe(_ command: String) async -> ShellOutput {
return await pipe(command, timeout: 60)
}
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let (_, output) = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
return output
}
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,

View File

@@ -71,7 +71,7 @@ class RemovePhpExtensionCommand: BrewCommand {
await performExtensionCleanup(for: ext)
}
_ = await container.phpEnvs.detectPhpVersions()
await container.phpEnvs.detectPhpVersions()
await Actions(container).restartPhpFpm(version: phpExtension.phpVersion)

View File

@@ -82,7 +82,7 @@ class ModifyPhpVersionCommand: BrewCommand {
}
// Re-check the installed versions
_ = await container.phpEnvs.detectPhpVersions()
await container.phpEnvs.detectPhpVersions()
// After performing operations, attempt to run repairs if needed
try await self.repairBrokenPackages(onProgress)
@@ -186,7 +186,7 @@ class ModifyPhpVersionCommand: BrewCommand {
await BrewDiagnostics.shared.checkForOutdatedPhpInstallationSymlinks()
// Check which version of PHP are now installed
_ = await container.phpEnvs.detectPhpVersions()
await container.phpEnvs.detectPhpVersions()
// Keep track of the currently installed version
await MainMenu.shared.refreshActiveInstallation()

View File

@@ -74,7 +74,7 @@ class RemovePhpVersionCommand: BrewCommand {
if process.terminationStatus == 0 {
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
_ = await container.phpEnvs.detectPhpVersions()
await container.phpEnvs.detectPhpVersions()
await MainMenu.shared.refreshActiveInstallation()

View File

@@ -34,11 +34,11 @@ class ValetInteractor {
// MARK: - Managing Domains
public func link(path: String, domain: String) async throws {
await container.shell.quiet("cd '\(path)' && \(container.paths.valet) link '\(domain)' && valet links")
await container.shell.pipe("cd '\(path)' && \(container.paths.valet) link '\(domain)' && valet links")
}
public func unlink(site: ValetSite) async throws {
await container.shell.quiet("valet unlink '\(site.name)'")
await container.shell.pipe("valet unlink '\(site.name)'")
}
public func proxy(domain: String, proxy: String, secure: Bool) async throws {
@@ -46,12 +46,12 @@ class ValetInteractor {
? "\(container.paths.valet) proxy \(domain) \(proxy) --secure"
: "\(container.paths.valet) proxy \(domain) \(proxy)"
await container.shell.quiet(command)
await container.shell.pipe(command)
await Actions(container).restartNginx()
}
public func remove(proxy: ValetProxy) async throws {
await container.shell.quiet("valet unproxy '\(proxy.domain)'")
await container.shell.pipe("valet unproxy '\(proxy.domain)'")
}
// MARK: - Modifying Domains
@@ -73,7 +73,7 @@ class ValetInteractor {
}
// Run the command
await container.shell.quiet(command)
await container.shell.pipe(command)
// Check if the secured status has actually changed
site.determineSecured()
@@ -98,7 +98,7 @@ class ValetInteractor {
// Run the commands
for command in commands {
await container.shell.quiet(command)
await container.shell.pipe(command)
}
// Check if the secured status has actually changed
@@ -117,7 +117,7 @@ class ValetInteractor {
let command = "sudo \(container.paths.valet) isolate php@\(version) --site '\(site.name)'"
// Run the command
await container.shell.quiet(command)
await container.shell.pipe(command)
// Check if the secured status has actually changed
site.determineIsolated()
@@ -133,7 +133,7 @@ class ValetInteractor {
let command = "sudo \(container.paths.valet) unisolate --site '\(site.name)'"
// Run the command
await container.shell.quiet(command)
await container.shell.pipe(command)
// Check if the secured status has actually changed
site.determineIsolated()

View File

@@ -37,7 +37,7 @@ struct CustomPrefs: Decodable {
extension Preferences {
func loadCustomPreferences() async {
// Ensure the configuration directory is created if missing
await container.shell.quiet("mkdir -p ~/.config/phpmon")
await container.shell.pipe("mkdir -p ~/.config/phpmon")
// Move the legacy file
await moveOutdatedConfigurationFile()
@@ -57,7 +57,7 @@ extension Preferences {
if container.filesystem.fileExists("~/.phpmon.conf.json")
&& !container.filesystem.fileExists("~/.config/phpmon/config.json") {
Log.info("An outdated configuration file was found. Moving it...")
await container.shell.quiet("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
await container.shell.pipe("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
Log.info("The configuration file was copied successfully!")
}
}

View File

@@ -35,7 +35,7 @@ class AppearancePreferencesVC: GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
_ = vc.addView(when: true, vc.getDynamicIconPV())
vc.addView(when: true, vc.getDynamicIconPV())
.addView(when: true, vc.getIconOptionsPV())
.addView(when: true, vc.getIconDensityPV())
.addView(when: true, vc.getMenuIconsPV())

View File

@@ -28,6 +28,7 @@ class GenericPreferenceVC: NSViewController {
Log.perf("deinit: \(String(describing: self)).\(#function)")
}
@discardableResult
func addView(when condition: Bool, _ view: NSView) -> GenericPreferenceVC {
if condition {
self.views.append(view)

View File

@@ -276,7 +276,7 @@ struct Preset: Codable, Equatable {
private func persistRevert() async {
let data = try! JSONEncoder().encode(self.revertSnapshot)
await container.shell.quiet("mkdir -p ~/.config/phpmon")
await container.shell.pipe("mkdir -p ~/.config/phpmon")
try! String(data: data, encoding: .utf8)!
.write(

View File

@@ -27,7 +27,7 @@ class InstallHomebrew {
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
"""
_ = try await container.shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
try await container.shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
print(string)
}, withTimeout: 60 * 10)
}

View File

@@ -21,6 +21,7 @@ class ZshRunCommand {
/**
Adds a given line to .zshrc, which may be needed to adjust the PATH.
*/
@discardableResult
private func add(_ text: String) async -> Bool {
// Escape single quotes to prevent shell injection
let escaped = text.replacingOccurrences(of: "'", with: "'\\''")
@@ -44,13 +45,13 @@ class ZshRunCommand {
Adds Homebrew binaries to the PATH.
*/
public func addHomebrewPath() async {
_ = await add("export PATH=$HOME/bin:/opt/homebrew/bin:$PATH")
await add("export PATH=$HOME/bin:/opt/homebrew/bin:$PATH")
}
/**
Adds PHP Monitor binaries to the PATH.
*/
public func addPhpMonitorPath() async {
_ = await add("export PATH=$HOME/bin:~/.config/phpmon/bin:$PATH")
await add("export PATH=$HOME/bin:~/.config/phpmon/bin:$PATH")
}
}

View File

@@ -31,11 +31,11 @@ extension DomainListVC {
}
@objc func openInFinder() {
Task { return await App.shared.container.shell.quiet("open '\(selectedSite!.absolutePath)'") }
Task { return await App.shared.container.shell.pipe("open '\(selectedSite!.absolutePath)'") }
}
@objc func openInTerminal() {
Task { await App.shared.container.shell.quiet("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") }
Task { await App.shared.container.shell.pipe("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") }
}
@objc func openWithApp(sender: ApplicationMenuItem) {
@@ -58,7 +58,7 @@ extension DomainListVC {
let rowToReload = tableView.selectedRow
waitAndExecute {
await App.shared.container.shell.quiet(command)
await App.shared.container.shell.pipe(command)
} completion: { [self] in
beforeCellReload()
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])

View File

@@ -87,7 +87,7 @@ extension WarningManager {
] },
url: "https://github.com/shivammathur/homebrew-php",
fix: {
await self.container.shell.quiet("brew tap shivammathur/php")
await self.container.shell.pipe("brew tap shivammathur/php")
await BrewDiagnostics.shared.loadInstalledTaps()
await self.checkEnvironment()
}
@@ -103,7 +103,7 @@ extension WarningManager {
] },
url: "https://github.com/shivammathur/homebrew-extensions",
fix: {
await self.container.shell.quiet("brew tap shivammathur/extensions")
await self.container.shell.pipe("brew tap shivammathur/extensions")
await BrewDiagnostics.shared.loadInstalledTaps()
await self.checkEnvironment()
}

View File

@@ -63,7 +63,7 @@ struct RealFileSystemTest {
#expect(filesystem.directoryExists("\(temporaryDirectory)/brew/etc/lib"))
#expect(filesystem.directoryExists("\(temporaryDirectory)/brew/etc/lib/c"))
_ = system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c")
system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c")
#expect(filesystem.directoryExists("\(temporaryDirectory)/c"))
#expect(filesystem.isSymlink("\(temporaryDirectory)/c"))
#expect(

View File

@@ -89,9 +89,9 @@ struct RealShellTest {
let start = ContinuousClock.now
await withTaskGroup(of: Void.self) { group in
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
}
let duration = start.duration(to: .now)