diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee9e661d..c0c7a0abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ ##### Breaking -- None. +- The `--no-color` option has been replaced with `--color=never`. ##### Enhancements -- None. +- The `--color` option now accepts one of `auto`, `always` and `never`. In `auto` mode, color is disabled for dumb terminals and non-TTYs. ##### Bug Fixes diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index 2c7099b3b..5210e93b1 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -275,6 +275,7 @@ swift_library( swift_library( name = "Configuration", srcs = [ + "Configuration/ColorOption.swift", "Configuration/Configuration.swift", "Configuration/OutputFormat.swift", ], diff --git a/Sources/Configuration/ColorOption.swift b/Sources/Configuration/ColorOption.swift new file mode 100644 index 000000000..2fc9c8be7 --- /dev/null +++ b/Sources/Configuration/ColorOption.swift @@ -0,0 +1,17 @@ +public enum ColorOption: String, CaseIterable, Equatable { + case auto + case always + case never + + public static let `default` = ColorOption.auto + + init?(anyValue: Any) { + if let option = anyValue as? ColorOption { + self = option + return + } + guard let stringValue = anyValue as? String else { return nil } + + self.init(rawValue: stringValue) + } +} diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index b4e283d13..eb3362493 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -98,8 +98,8 @@ public final class Configuration { @Setting(key: "quiet", defaultValue: false) public var quiet: Bool - @Setting(key: "color", defaultValue: true) - public var color: Bool + @Setting(key: "color", defaultValue: .default, setter: { ColorOption(anyValue: $0) }) + public var color: ColorOption @Setting(key: "disable_update_check", defaultValue: false) public var disableUpdateCheck: Bool @@ -174,7 +174,7 @@ public final class Configuration { let encodedYAML = try String(contentsOf: path.url, encoding: .utf8) let yaml = try Yams.load(yaml: encodedYAML) as? [String: Any] ?? [:] - let logger = Logger(quiet: false, verbose: false, coloredOutputEnabled: false) + let logger = Logger(quiet: false, verbose: false, colorMode: .never) for (key, value) in yaml { if let setting = settings.first(where: { key == $0.key }) { @@ -338,3 +338,9 @@ extension FilePath: Yams.ScalarRepresentable { string.represented() } } + +extension ColorOption: Yams.ScalarRepresentable { + public func represented() -> Node.Scalar { + rawValue.represented() + } +} diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index 49fac5adf..14d1a743c 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -30,7 +30,7 @@ struct ScanCommand: ParsableCommand { @Option(parsing: .upToNextOption, help: "Schemes to build. All targets built by these schemes will be scanned") var schemes: [String] = defaultConfiguration.$schemes.defaultValue - @Option(help: "Output format (allowed: \(OutputFormat.allValueStrings.joined(separator: ", ")))") + @Option(help: "Output format") var format: OutputFormat = defaultConfiguration.$outputFormat.defaultValue @Flag(help: "Exclude test targets from indexing") @@ -126,8 +126,11 @@ struct ScanCommand: ParsableCommand { @Flag(help: "Only output results") var quiet: Bool = defaultConfiguration.$quiet.defaultValue - @Flag(inversion: .prefixedNo, help: "Colored output") - var color: Bool = defaultConfiguration.$color.defaultValue + @Option(help: "Colored output mode") + var color: ColorOption = defaultConfiguration.$color.defaultValue + + @Flag(name: .customLong("no-color"), help: .hidden) + var noColor: Bool = false @Option(help: "JSON package manifest path (obtained using `swift package describe --type json` or manually)") var jsonPackageManifestPath: FilePath? @@ -191,7 +194,7 @@ struct ScanCommand: ParsableCommand { configuration.apply(\.$externalTestCaseClasses, externalTestCaseClasses) configuration.apply(\.$verbose, verbose) configuration.apply(\.$quiet, quiet) - configuration.apply(\.$color, color) + configuration.apply(\.$color, noColor ? .never : color) configuration.apply(\.$disableUpdateCheck, disableUpdateCheck) configuration.apply(\.$strict, strict) configuration.apply(\.$indexStorePath, indexStorePath) @@ -268,7 +271,7 @@ struct ScanCommand: ParsableCommand { let outputFormat = configuration.outputFormat let formatter = outputFormat.formatter.init(configuration: configuration, logger: logger) - let colored = outputFormat.supportsColoredOutput && configuration.color + let colored = outputFormat.supportsColoredOutput && logger.isColoredOutputEnabled if let output = try formatter.format(filteredResults, colored: colored) { if outputFormat.supportsAuxiliaryOutput { @@ -309,6 +312,7 @@ struct ScanCommand: ParsableCommand { } extension OutputFormat: ExpressibleByArgument {} +extension ColorOption: ExpressibleByArgument {} extension FilePath: ArgumentParser.ExpressibleByArgument { public init?(argument: String) { diff --git a/Sources/Frontend/Logger+Extension.swift b/Sources/Frontend/Logger+Extension.swift index 64302964c..ce36f6f9b 100644 --- a/Sources/Frontend/Logger+Extension.swift +++ b/Sources/Frontend/Logger+Extension.swift @@ -4,10 +4,15 @@ import Logger public extension Logger { @inlinable init(configuration: Configuration) { + let colorMode: LoggerColorMode = switch configuration.color { + case .auto: .auto + case .always: .always + case .never: .never + } self.init( quiet: configuration.quiet, verbose: configuration.verbose, - coloredOutputEnabled: configuration.color + colorMode: colorMode ) } } diff --git a/Sources/Logger/Logger.swift b/Sources/Logger/Logger.swift index 65197e4cf..4ff721a6e 100644 --- a/Sources/Logger/Logger.swift +++ b/Sources/Logger/Logger.swift @@ -21,40 +21,46 @@ public enum ANSIColor: String { case gray = "\u{001B}[0;1;30m" } +public enum LoggerColorMode: Sendable { + case auto + case always + case never +} + public struct Logger: Sendable { let outputQueue: DispatchQueue let quiet: Bool let verbose: Bool - let coloredOutputEnabled: Bool + let colorMode: LoggerColorMode #if canImport(os) let signposter = OSSignposter() #endif - private var isColorOutputCapable: Bool = { - guard let term = ProcessInfo.processInfo.environment["TERM"], - term.lowercased() != "dumb", - isatty(fileno(stdout)) != 0 - else { - return false + public var isColoredOutputEnabled: Bool { + switch colorMode { + case .auto: + isColorOutputCapable + case .always: + true + case .never: + false } - - return true - }() + } public init( quiet: Bool, verbose: Bool, - coloredOutputEnabled: Bool + colorMode: LoggerColorMode ) { self.quiet = quiet self.verbose = verbose - self.coloredOutputEnabled = coloredOutputEnabled + self.colorMode = colorMode outputQueue = DispatchQueue(label: "Logger.outputQueue") } public func colorize(_ text: String, _ color: ANSIColor) -> String { - guard isColorOutputCapable, coloredOutputEnabled else { return text } + guard isColoredOutputEnabled else { return text } return "\(color.rawValue)\(text)\u{001B}[0;0m" } @@ -112,6 +118,17 @@ public struct Logger: Sendable { func log(_ line: String, output: UnsafeMutablePointer) { _ = outputQueue.sync { fputs(line + "\n", output) } } + + private var isColorOutputCapable: Bool = { + guard let term = ProcessInfo.processInfo.environment["TERM"], + term.lowercased() != "dumb", + isatty(fileno(stdout)) != 0 + else { + return false + } + + return true + }() } public struct ContextualLogger: Sendable { diff --git a/Tests/PeripheryTests/Syntax/FunctionVisitTest.swift b/Tests/PeripheryTests/Syntax/FunctionVisitTest.swift index 830a2ed13..836a3021a 100644 --- a/Tests/PeripheryTests/Syntax/FunctionVisitTest.swift +++ b/Tests/PeripheryTests/Syntax/FunctionVisitTest.swift @@ -11,7 +11,7 @@ final class FunctionVisitTest: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, coloredOutputEnabled: false)) + let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, colorMode: .never)) let swiftVersion = SwiftVersion(shell: shell) let multiplexingVisitor = try MultiplexingSyntaxVisitor(file: fixturePath, swiftVersion: swiftVersion) let visitor = multiplexingVisitor.add(DeclarationSyntaxVisitor.self) diff --git a/Tests/PeripheryTests/Syntax/ImportVisitTest.swift b/Tests/PeripheryTests/Syntax/ImportVisitTest.swift index f88cf30d8..a5e4cdcb3 100644 --- a/Tests/PeripheryTests/Syntax/ImportVisitTest.swift +++ b/Tests/PeripheryTests/Syntax/ImportVisitTest.swift @@ -11,7 +11,7 @@ final class ImportVisitTest: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, coloredOutputEnabled: false)) + let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, colorMode: .never)) let swiftVersion = SwiftVersion(shell: shell) let multiplexingVisitor = try MultiplexingSyntaxVisitor(file: fixturePath, swiftVersion: swiftVersion) let visitor = multiplexingVisitor.add(ImportSyntaxVisitor.self) diff --git a/Tests/PeripheryTests/Syntax/PropertyVisitTest.swift b/Tests/PeripheryTests/Syntax/PropertyVisitTest.swift index cfa8a71aa..cb6cd6b61 100644 --- a/Tests/PeripheryTests/Syntax/PropertyVisitTest.swift +++ b/Tests/PeripheryTests/Syntax/PropertyVisitTest.swift @@ -11,7 +11,7 @@ final class PropertyVisitTest: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, coloredOutputEnabled: false)) + let shell = ShellImpl(logger: Logger(quiet: true, verbose: false, colorMode: .never)) let swiftVersion = SwiftVersion(shell: shell) let multiplexingVisitor = try MultiplexingSyntaxVisitor(file: fixturePath, swiftVersion: swiftVersion) let visitor = multiplexingVisitor.add(DeclarationSyntaxVisitor.self) diff --git a/Tests/Shared/SourceGraphTestCase.swift b/Tests/Shared/SourceGraphTestCase.swift index c946639e1..f3abf396e 100644 --- a/Tests/Shared/SourceGraphTestCase.swift +++ b/Tests/Shared/SourceGraphTestCase.swift @@ -21,7 +21,7 @@ open class SourceGraphTestCase: XCTestCase { override open class func setUp() { super.setUp() - logger = Logger(quiet: true, verbose: false, coloredOutputEnabled: false) + logger = Logger(quiet: true, verbose: false, colorMode: .never) shell = ShellImpl(logger: logger) swiftVersion = SwiftVersion(shell: shell) let configuration = Configuration() diff --git a/Tests/XcodeTests/XcodeTargetTest.swift b/Tests/XcodeTests/XcodeTargetTest.swift index ed27d2bf6..c83acdb6a 100644 --- a/Tests/XcodeTests/XcodeTargetTest.swift +++ b/Tests/XcodeTests/XcodeTargetTest.swift @@ -11,7 +11,7 @@ final class XcodeTargetTest: XCTestCase { override func setUp() { super.setUp() - let logger = Logger(quiet: true, verbose: false, coloredOutputEnabled: false) + let logger = Logger(quiet: true, verbose: false, colorMode: .never) let shell = ShellImpl(logger: logger) let xcodebuild = Xcodebuild(shell: shell, logger: logger) var loadedProjectPaths: Set = [] diff --git a/Tests/XcodeTests/XcodebuildBuildProjectTest.swift b/Tests/XcodeTests/XcodebuildBuildProjectTest.swift index 0dea5a1d0..67d5edb0d 100644 --- a/Tests/XcodeTests/XcodebuildBuildProjectTest.swift +++ b/Tests/XcodeTests/XcodebuildBuildProjectTest.swift @@ -12,7 +12,7 @@ final class XcodebuildBuildProjectTest: XCTestCase { override func setUp() { super.setUp() - let logger = Logger(quiet: true, verbose: false, coloredOutputEnabled: false) + let logger = Logger(quiet: true, verbose: false, colorMode: .never) let shell = ShellImpl(logger: logger) var loadedProjectPaths: Set = [] xcodebuild = Xcodebuild(shell: shell, logger: logger) diff --git a/Tests/XcodeTests/XcodebuildSchemesTest.swift b/Tests/XcodeTests/XcodebuildSchemesTest.swift index a7083d0c4..88f9c195c 100644 --- a/Tests/XcodeTests/XcodebuildSchemesTest.swift +++ b/Tests/XcodeTests/XcodebuildSchemesTest.swift @@ -8,7 +8,7 @@ final class XcodebuildSchemesTest: XCTestCase { func testParseSchemes() { for output in XcodebuildListOutputs { let shell = ShellMock(output: output) - let logger = Logger(quiet: true, verbose: false, coloredOutputEnabled: false) + let logger = Logger(quiet: true, verbose: false, colorMode: .never) var loadedProjectPaths: Set = [] let xcodebuild = Xcodebuild(shell: shell, logger: logger) let project = try! XcodeProject(path: UIKitProjectPath, loadedProjectPaths: &loadedProjectPaths, xcodebuild: xcodebuild, shell: shell, logger: logger)