237 lines
6.0 KiB
Swift
237 lines
6.0 KiB
Swift
import Constants
|
|
import Dependencies
|
|
import DependenciesMacros
|
|
import Foundation
|
|
import ShellClient
|
|
|
|
extension DependencyValues {
|
|
|
|
/// Runs shell commands.
|
|
public 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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents logger setup options used when running commands.
|
|
///
|
|
/// This set's up metadata tags and keys appropriately for the command being run,
|
|
/// which can aid in debugging.
|
|
///
|
|
public struct LoggingOptions: Equatable, Sendable {
|
|
|
|
/// The command name / key that the logger is running for.
|
|
public let commandName: String
|
|
|
|
/// The log level.
|
|
public let logLevel: Logger.Level
|
|
|
|
/// Create the logging options.
|
|
///
|
|
/// - Parameters:
|
|
/// - commandName: The command name / key that the logger is running for.
|
|
/// - logLevel: The log level.
|
|
public init(commandName: String, logLevel: Logger.Level) {
|
|
self.commandName = commandName
|
|
self.logLevel = logLevel
|
|
}
|
|
|
|
/// Perform an operation with a setup logger.
|
|
///
|
|
/// - Parameters:
|
|
/// - operation: The operation to perform with the setup logger.
|
|
@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)
|
|
extension CommandClient {
|
|
|
|
/// Create a command client that can capture the arguments / options.
|
|
///
|
|
/// This is used for testing.
|
|
public 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
|
|
public 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 {
|
|
#if os(macOS)
|
|
self = .zsh(useDashC: true)
|
|
#else
|
|
// Generally we're in a docker container when this occurs, which does not have `zsh` installed.
|
|
self = .sh(useDashC: true)
|
|
#endif
|
|
}
|
|
}
|
|
}
|