Files
swift-hpa/Sources/CommandClient/CommandClient.swift

211 lines
5.1 KiB
Swift

import Constants
import Dependencies
import DependenciesMacros
import Foundation
import ShellClient
public extension DependencyValues {
/// Runs shell commands.
var commandClient: CommandClient {
get { self[CommandClient.self] }
set { self[CommandClient.self] = newValue }
}
}
@DependencyClient
public struct CommandClient: Sendable {
/// Runs a shell command.
public var runCommand: @Sendable (RunCommandOptions) async throws -> Void
/// Runs a shell command, sets up logging, and returns an output value.
///
/// This is useful when you need to give some output value to the caller,
/// generally for the ability of that output to be piped into other commands.
///
@discardableResult
public func run<T>(
logging logginOptions: LoggingOptions,
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
makeArguments: @Sendable @escaping () async throws -> ([String], T)
) async throws -> T {
try await logginOptions.withLogger {
let (arguments, returnValue) = try await makeArguments()
try await self.run(
quiet: quiet,
shell: shell,
arguments
)
return returnValue
}
}
/// Runs a shell command and sets up logging.
public func run(
logging logginOptions: LoggingOptions,
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
makeArguments: @Sendable @escaping () async throws -> [String]
) async throws {
try await run(
logging: logginOptions,
quiet: quiet,
shell: shell,
in: workingDirectory,
makeArguments: { try await (makeArguments(), ()) }
)
}
/// Runs a shell command.
public func run(
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
_ arguments: [String]
) async throws {
try await runCommand(.init(
arguments: arguments,
quiet: quiet,
shell: shell,
workingDirectory: workingDirectory
))
}
/// Runs a shell command.
public func run(
quiet: Bool = false,
shell: String? = nil,
in workingDirectory: String? = nil,
_ arguments: String...
) async throws {
try await run(
quiet: quiet,
shell: shell,
in: workingDirectory,
arguments
)
}
public struct RunCommandOptions: Sendable, Equatable {
public let arguments: [String]
public let quiet: Bool
public let shell: String?
public let workingDirectory: String?
public init(
arguments: [String],
quiet: Bool,
shell: String? = nil,
workingDirectory: String? = nil
) {
self.arguments = arguments
self.quiet = quiet
self.shell = shell
self.workingDirectory = workingDirectory
}
}
}
public struct LoggingOptions: Equatable, Sendable {
public let commandName: String
public let logLevel: Logger.Level
public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName
self.logLevel = logLevel
}
@discardableResult
public func withLogger<T>(
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger = .init(label: "\(Constants.executableName)")
$0.logger.logLevel = logLevel
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
} operation: {
try await operation()
}
}
}
extension CommandClient: DependencyKey {
public static let testValue: CommandClient = Self()
public static func live(
environment: [String: String]
) -> CommandClient {
.init { options in
@Dependency(\.asyncShellClient) var shellClient
if !options.quiet {
try await shellClient.foreground(.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
} else {
try await shellClient.background(.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
}
}
}
public static var liveValue: CommandClient {
.live(environment: ProcessInfo.processInfo.environment)
}
}
@_spi(Internal)
public extension CommandClient {
/// Create a command client that can capture the arguments / options.
///
/// This is used for testing.
static func capturing(_ client: CapturingClient) -> Self {
.init { options in
await client.set(options)
}
}
/// Captures the arguments / options passed into the command client's run commands.
///
@dynamicMemberLookup
actor CapturingClient: Sendable {
public private(set) var options: RunCommandOptions?
public init() {}
public func set(
_ options: RunCommandOptions
) {
self.options = options
}
public subscript<T>(dynamicMember keyPath: KeyPath<RunCommandOptions, T>) -> T? {
options?[keyPath: keyPath]
}
}
}
extension ShellCommand.Shell: @retroactive @unchecked Sendable {}
extension ShellCommand.Shell {
init(_ path: String?) {
if let path {
self = .custom(path: path, useDashC: true)
} else {
self = .zsh(useDashC: true)
}
}
}