189 lines
4.4 KiB
Swift
189 lines
4.4 KiB
Swift
import Constants
|
|
import Dependencies
|
|
import DependenciesMacros
|
|
import Foundation
|
|
import LoggingExtensions
|
|
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 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 logginOptions.withLogger {
|
|
let arguments = try await makeArguments()
|
|
try await self.run(
|
|
quiet: quiet,
|
|
shell: shell,
|
|
arguments
|
|
)
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
}
|