From f55a509fdd05c7a1cce7f0e3461f92f3ac080e37 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Fri, 7 Jul 2017 11:25:45 +0200 Subject: [PATCH] Cleanup `ConsoleAppender` --- .../sbt/internal/util/ConsoleAppender.scala | 523 ++++++++++-------- .../scala/sbt/internal/util/ConsoleOut.scala | 2 +- .../scala/sbt/internal/util/EscHelpers.scala | 91 +++ .../scala/sbt/internal/util/MainLogging.scala | 10 +- .../scala/sbt/internal/util/MultiLogger.scala | 2 +- .../util-logging/src/test/scala/Escapes.scala | 2 +- 6 files changed, 397 insertions(+), 233 deletions(-) create mode 100644 internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index e85f70fa8..4af11dc84 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -1,5 +1,6 @@ package sbt.internal.util +import scala.compat.Platform.EOL import sbt.util._ import java.io.{ PrintStream, PrintWriter } import java.util.Locale @@ -14,146 +15,220 @@ import ConsoleAppender._ object ConsoleLogger { // These are provided so other modules do not break immediately. - @deprecated("Use ConsoleAppender.", "0.13.x") - final val ESC = ConsoleAppender.ESC - @deprecated("Use ConsoleAppender.", "0.13.x") - private[sbt] def isEscapeTerminator(c: Char): Boolean = ConsoleAppender.isEscapeTerminator(c) - @deprecated("Use ConsoleAppender.", "0.13.x") - def hasEscapeSequence(s: String): Boolean = ConsoleAppender.hasEscapeSequence(s) - @deprecated("Use ConsoleAppender.", "0.13.x") - def removeEscapeSequences(s: String): String = ConsoleAppender.removeEscapeSequences(s) - @deprecated("Use ConsoleAppender.", "0.13.x") - val formatEnabled = ConsoleAppender.formatEnabled + @deprecated("Use EscHelpers.", "0.13.x") + final val ESC = EscHelpers.ESC + @deprecated("Use EscHelpers.", "0.13.x") + private[sbt] def isEscapeTerminator(c: Char): Boolean = EscHelpers.isEscapeTerminator(c) + @deprecated("Use EscHelpers.", "0.13.x") + def hasEscapeSequence(s: String): Boolean = EscHelpers.hasEscapeSequence(s) + @deprecated("Use EscHelpers.", "0.13.x") + def removeEscapeSequences(s: String): String = EscHelpers.removeEscapeSequences(s) + @deprecated("Use ConsoleAppenders.formatEnabledInEnv", "0.13.x") + val formatEnabled = ConsoleAppender.formatEnabledInEnv @deprecated("Use ConsoleAppender.", "0.13.x") val noSuppressedMessage = ConsoleAppender.noSuppressedMessage + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @return A new `ConsoleLogger` that logs to `out`. + */ def apply(out: PrintStream): ConsoleLogger = apply(ConsoleOut.printStreamOut(out)) + + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @return A new `ConsoleLogger` that logs to `out`. + */ def apply(out: PrintWriter): ConsoleLogger = apply(ConsoleOut.printWriterOut(out)) - def apply(out: ConsoleOut = ConsoleOut.systemOut, ansiCodesSupported: Boolean = ConsoleAppender.formatEnabled, - useColor: Boolean = ConsoleAppender.formatEnabled, suppressedMessage: SuppressedTraceContext => Option[String] = ConsoleAppender.noSuppressedMessage): ConsoleLogger = - new ConsoleLogger(out, ansiCodesSupported, useColor, suppressedMessage) + + /** + * A new `ConsoleLogger` that logs to `out`. + * + * @param out Where to log the messages. + * @param ansiCodesSupported `true` if `out` supported ansi codes, `false` otherwise. + * @param useFormat `true` to show formatting, `false` to remove it from messages. + * @param suppressedMessage How to show suppressed stack traces. + * @return A new `ConsoleLogger` that logs to `out`. + */ + def apply(out: ConsoleOut = ConsoleOut.systemOut, + ansiCodesSupported: Boolean = ConsoleAppender.formatEnabledInEnv, + useFormat: Boolean = ConsoleAppender.formatEnabledInEnv, + suppressedMessage: SuppressedTraceContext => Option[String] = ConsoleAppender.noSuppressedMessage): ConsoleLogger = + new ConsoleLogger(out, ansiCodesSupported, useFormat, suppressedMessage) } /** * A logger that logs to the console. On supported systems, the level labels are * colored. */ -class ConsoleLogger private[ConsoleLogger] (val out: ConsoleOut, override val ansiCodesSupported: Boolean, val useColor: Boolean, val suppressedMessage: SuppressedTraceContext => Option[String]) extends BasicLogger { - private[sbt] val appender = ConsoleAppender(generateName, out, ansiCodesSupported, useColor, suppressedMessage) +class ConsoleLogger private[ConsoleLogger] (out: ConsoleOut, + override val ansiCodesSupported: Boolean, + useFormat: Boolean, + suppressedMessage: SuppressedTraceContext => Option[String]) extends BasicLogger { + + private[sbt] val appender: ConsoleAppender = + ConsoleAppender(generateName(), out, ansiCodesSupported, useFormat, suppressedMessage) override def control(event: ControlEvent.Value, message: => String): Unit = appender.control(event, message) + override def log(level: Level.Value, message: => String): Unit = - { - if (atLevel(level)) { - appender.appendLog(level, message) - } + if (atLevel(level)) { + appender.appendLog(level, message) } override def success(message: => String): Unit = - { - if (successEnabled) { - appender.success(message) - } + if (successEnabled) { + appender.success(message) } + override def trace(t: => Throwable): Unit = appender.trace(t, getTrace) - override def logAll(events: Seq[LogEvent]) = out.lockObject.synchronized { events.foreach(log) } + override def logAll(events: Seq[LogEvent]) = + out.lockObject.synchronized { events.foreach(log) } } object ConsoleAppender { - /** Escape character, used to introduce an escape sequence. */ - final val ESC = '\u001B' - /** - * An escape terminator is a character in the range `@` (decimal value 64) to `~` (decimal value 126). - * It is the final character in an escape sequence. - * - * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes - */ - private[sbt] def isEscapeTerminator(c: Char): Boolean = - c >= '@' && c <= '~' + /** Hide stack trace altogether. */ + val noSuppressedMessage = (_: SuppressedTraceContext) => None - /** - * Test if the character AFTER an ESC is the ANSI CSI. - * - * see: http://en.wikipedia.org/wiki/ANSI_escape_code - * - * The CSI (control sequence instruction) codes start with ESC + '['. This is for testing the second character. - * - * There is an additional CSI (one character) that we could test for, but is not frequnetly used, and we don't - * check for it. - * - * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes - */ - private def isCSI(c: Char): Boolean = c == '[' - - /** - * Tests whether or not a character needs to immediately terminate the ANSI sequence. - * - * c.f. http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements - */ - private def isAnsiTwoCharacterTerminator(c: Char): Boolean = - (c >= '@') && (c <= '_') - - /** - * Returns true if the string contains the ESC character. - * - * TODO - this should handle raw CSI (not used much) - */ - def hasEscapeSequence(s: String): Boolean = - s.indexOf(ESC) >= 0 - - /** - * Returns the string `s` with escape sequences removed. - * An escape sequence starts with the ESC character (decimal value 27) and ends with an escape terminator. - * @see isEscapeTerminator - */ - def removeEscapeSequences(s: String): String = - if (s.isEmpty || !hasEscapeSequence(s)) - s - else { - val sb = new java.lang.StringBuilder - nextESC(s, 0, sb) - sb.toString - } - private[this] def nextESC(s: String, start: Int, sb: java.lang.StringBuilder): Unit = { - val escIndex = s.indexOf(ESC, start) - if (escIndex < 0) { - sb.append(s, start, s.length) - () - } else { - sb.append(s, start, escIndex) - val next: Int = - // If it's a CSI we skip past it and then look for a terminator. - if (isCSI(s.charAt(escIndex + 1))) skipESC(s, escIndex + 2) - else if (isAnsiTwoCharacterTerminator(s.charAt(escIndex + 1))) escIndex + 2 - else { - // There could be non-ANSI character sequences we should make sure we handle here. - skipESC(s, escIndex + 1) - } - nextESC(s, next, sb) - } + /** Indicates whether formatting has been disabled in environment variables. */ + val formatEnabledInEnv: Boolean = { + import java.lang.Boolean.{ getBoolean, parseBoolean } + val value = System.getProperty("sbt.log.format") + if (value eq null) (ansiSupported && !getBoolean("sbt.log.noformat")) else parseBoolean(value) } - /** Skips the escape sequence starting at `i-1`. `i` should be positioned at the character after the ESC that starts the sequence. */ - private[this] def skipESC(s: String, i: Int): Int = { - if (i >= s.length) { - i - } else if (isEscapeTerminator(s.charAt(i))) { - i + 1 - } else { - skipESC(s, i + 1) - } + private[this] val generateId: AtomicInteger = new AtomicInteger + + /** + * A new `ConsoleAppender` that writes to standard output. + * + * @return A new `ConsoleAppender` that writes to standard output. + */ + def apply(): ConsoleAppender = apply(ConsoleOut.systemOut) + + /** + * A new `ConsoleAppender` that appends log message to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender`. + */ + def apply(out: PrintStream): ConsoleAppender = apply(ConsoleOut.printStreamOut(out)) + + /** + * A new `ConsoleAppender` that appends log messages to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender`. + */ + def apply(out: PrintWriter): ConsoleAppender = apply(ConsoleOut.printWriterOut(out)) + + /** + * A new `ConsoleAppender` that writes to `out`. + * + * @param out Where to write messages. + * @return A new `ConsoleAppender that writes to `out`. + */ + def apply(out: ConsoleOut): ConsoleAppender = apply(generateName(), out) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to standard output. + * + * @param name An identifier for the `ConsoleAppender`. + * @return A new `ConsoleAppender` that writes to standard output. + */ + def apply(name: String): ConsoleAppender = apply(name, ConsoleOut.systemOut) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, out: ConsoleOut): ConsoleAppender = apply(name, out, formatEnabledInEnv) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param suppressedMessage How to handle stack traces. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, out: ConsoleOut, suppressedMessage: SuppressedTraceContext => Option[String]): ConsoleAppender = + apply(name, out, formatEnabledInEnv, formatEnabledInEnv, suppressedMessage) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param useFormat `true` to enable format (color, bold, etc.), `false` to remove formatting. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, out: ConsoleOut, useFormat: Boolean): ConsoleAppender = + apply(name, out, formatEnabledInEnv, useFormat, noSuppressedMessage) + + /** + * A new `ConsoleAppender` identified by `name`, and that writes to `out`. + * + * @param name An identifier for the `ConsoleAppender`. + * @param out Where to write messages. + * @param ansiCodesSupported `true` if the output stream supports ansi codes, `false` otherwise. + * @param useFormat `true` to enable format (color, bold, etc.), `false` to remove + * formatting. + * @return A new `ConsoleAppender` that writes to `out`. + */ + def apply(name: String, + out: ConsoleOut, + ansiCodesSupported: Boolean, + useFormat: Boolean, + suppressedMessage: SuppressedTraceContext => Option[String]): ConsoleAppender = { + val appender = new ConsoleAppender(name, out, ansiCodesSupported, useFormat, suppressedMessage) + appender.start + appender } - val formatEnabled: Boolean = - { - import java.lang.Boolean.{ getBoolean, parseBoolean } - val value = System.getProperty("sbt.log.format") - if (value eq null) (ansiSupported && !getBoolean("sbt.log.noformat")) else parseBoolean(value) + /** + * Converts the Log4J `level` to the corresponding sbt level. + * + * @param level A level, as represented by Log4J. + * @return The corresponding level in sbt's world. + */ + def toLevel(level: XLevel): Level.Value = + level match { + case XLevel.OFF => Level.Debug + case XLevel.FATAL => Level.Error + case XLevel.ERROR => Level.Error + case XLevel.WARN => Level.Warn + case XLevel.INFO => Level.Info + case XLevel.DEBUG => Level.Debug + case _ => Level.Debug } + + /** + * Converts the sbt `level` to the corresponding Log4J level. + * + * @param level A level, as represented by sbt. + * @return The corresponding level in Log4J's world. + */ + def toXLevel(level: Level.Value): XLevel = + level match { + case Level.Error => XLevel.ERROR + case Level.Warn => XLevel.WARN + case Level.Info => XLevel.INFO + case Level.Debug => XLevel.DEBUG + } + + private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet + private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected" private[this] def ansiSupported = @@ -172,57 +247,9 @@ object ConsoleAppender { throw new IncompatibleClassChangeError("JLine incompatibility detected. Check that the sbt launcher is version 0.13.x or later.") } - val noSuppressedMessage = (_: SuppressedTraceContext) => None - private[this] def os = System.getProperty("os.name") private[this] def isWindows = os.toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0 - def apply(out: PrintStream): ConsoleAppender = apply(ConsoleOut.printStreamOut(out)) - def apply(out: PrintWriter): ConsoleAppender = apply(ConsoleOut.printWriterOut(out)) - def apply(): ConsoleAppender = apply(ConsoleOut.systemOut) - def apply(name: String): ConsoleAppender = apply(name, ConsoleOut.systemOut) - def apply(out: ConsoleOut): ConsoleAppender = apply(generateName, out) - def apply(name: String, out: ConsoleOut): ConsoleAppender = apply(name, out, formatEnabled) - - def apply(name: String, out: ConsoleOut, suppressedMessage: SuppressedTraceContext => Option[String]): ConsoleAppender = - apply(name, out, formatEnabled, formatEnabled, suppressedMessage) - - def apply(name: String, out: ConsoleOut, useColor: Boolean): ConsoleAppender = - apply(name, out, formatEnabled, useColor, noSuppressedMessage) - - def apply(name: String, out: ConsoleOut, ansiCodesSupported: Boolean, - useColor: Boolean, suppressedMessage: SuppressedTraceContext => Option[String]): ConsoleAppender = - { - val appender = new ConsoleAppender(name, out, ansiCodesSupported, useColor, suppressedMessage) - appender.start - appender - } - - def generateName: String = "out-" + generateId.incrementAndGet - - private val generateId: AtomicInteger = new AtomicInteger - - private[this] val EscapeSequence = (27.toChar + "[^@-~]*[@-~]").r - def stripEscapeSequences(s: String): String = - EscapeSequence.pattern.matcher(s).replaceAll("") - - def toLevel(level: XLevel): Level.Value = - level match { - case XLevel.OFF => Level.Debug - case XLevel.FATAL => Level.Error - case XLevel.ERROR => Level.Error - case XLevel.WARN => Level.Warn - case XLevel.INFO => Level.Info - case XLevel.DEBUG => Level.Debug - case _ => Level.Debug - } - def toXLevel(level: Level.Value): XLevel = - level match { - case Level.Error => XLevel.ERROR - case Level.Warn => XLevel.WARN - case Level.Info => XLevel.INFO - case Level.Debug => XLevel.DEBUG - } } // See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2 @@ -237,35 +264,135 @@ object ConsoleAppender { * This logger is not thread-safe. */ class ConsoleAppender private[ConsoleAppender] ( - val name: String, - val out: ConsoleOut, - val ansiCodesSupported: Boolean, - val useColor: Boolean, - val suppressedMessage: SuppressedTraceContext => Option[String] + name: String, + out: ConsoleOut, + ansiCodesSupported: Boolean, + useFormat: Boolean, + suppressedMessage: SuppressedTraceContext => Option[String] ) extends AbstractAppender(name, null, PatternLayout.createDefaultLayout(), true) { import scala.Console.{ BLUE, GREEN, RED, RESET, YELLOW } - def append(event: XLogEvent): Unit = - { - val level = ConsoleAppender.toLevel(event.getLevel) - val message = event.getMessage - // val str = messageToString(message) - appendMessage(level, message) + private final val SUCCESS_LABEL_COLOR = GREEN + private final val SUCCESS_MESSAGE_COLOR = RESET + private final val NO_COLOR = RESET + + override def append(event: XLogEvent): Unit = { + val level = ConsoleAppender.toLevel(event.getLevel) + val message = event.getMessage + appendMessage(level, message) + } + + // TODO: + // success is called by ConsoleLogger. + // This should turn into an event. + private[sbt] def success(message: => String): Unit = { + appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message) + } + + /** + * Logs the stack trace of `t`, possibly shortening it. + * + * The `traceLevel` parameter configures how the stack trace will be shortened. + * See `StackTrace.trimmed`. + * + * @param t The `Throwable` whose stack trace to log. + * @param traceLevel How to shorten the stack trace. + */ + def trace(t: => Throwable, traceLevel: Int): Unit = + out.lockObject.synchronized { + if (traceLevel >= 0) + write(StackTrace.trimmed(t, traceLevel)) + if (traceLevel <= 2) + for (msg <- suppressedMessage(new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat))) + appendLog(NO_COLOR, "trace", NO_COLOR, msg) } - def appendMessage(level: Level.Value, msg: Message): Unit = + /** + * Logs a `ControlEvent` to the log. + * + * @param event The kind of `ControlEvent`. + * @param message The message to log. + */ + def control(event: ControlEvent.Value, message: => String): Unit = + appendLog(labelColor(Level.Info), Level.Info.toString, BLUE, message) + + /** + * Appends the message `message` to the to the log at level `level`. + * + * @param level The importance level of the message. + * @param message The message to log. + */ + def appendLog(level: Level.Value, message: => String): Unit = { + appendLog(labelColor(level), level.toString, NO_COLOR, message) + } + + /** + * Formats `msg` with `format, wrapped between `RESET`s + * + * @param format The format to use + * @param msg The message to format + * @return The formatted message. + */ + private def formatted(format: String, msg: String): String = + s"${RESET}${format}${msg}${RESET}" + + /** + * Select the right color for the label given `level`. + * + * @param level The label to consider to select the color. + * @return The color to use to color the label. + */ + private def labelColor(level: Level.Value): String = + level match { + case Level.Error => RED + case Level.Warn => YELLOW + case _ => NO_COLOR + } + + /** + * Appends a full message to the log. Each line is prefixed with `[$label]`, written in + * `labelColor` if formatting is enabled. The lines of the messages are colored with + * `messageColor` if formatting is enabled. + * + * @param labelColor The color to use to format the label. + * @param label The label to prefix each line with. The label is shown between square + * brackets. + * @param messageColor The color to use to format the message. + * @param message The message to write. + */ + private def appendLog(labelColor: String, label: String, messageColor: String, message: String): Unit = + out.lockObject.synchronized { + message.lines.foreach { line => + val labeledLine = s"[${formatted(labelColor, label)}] ${formatted(messageColor, line)}" + writeLine(labeledLine) + } + } + + private def write(msg: String): Unit = { + val cleanedMsg = + if (!useFormat) EscHelpers.removeEscapeSequences(msg) + else msg + out.println(cleanedMsg) + } + + private def writeLine(line: String): Unit = + write(line + EOL) + + private def appendMessage(level: Level.Value, msg: Message): Unit = msg match { case o: ObjectMessage => objectToLines(o.getParameter) foreach { appendLog(level, _) } case o: ReusableObjectMessage => objectToLines(o.getParameter) foreach { appendLog(level, _) } case _ => appendLog(level, msg.getFormattedMessage) } - def objectToLines(o: AnyRef): Vector[String] = + + private def objectToLines(o: AnyRef): Vector[String] = o match { case x: StringEvent => Vector(x.message) case x: ObjectEvent[_] => objectEventToLines(x) case _ => Vector(o.toString) } - def objectEventToLines(oe: ObjectEvent[_]): Vector[String] = + + private def objectEventToLines(oe: ObjectEvent[_]): Vector[String] = { val contentType = oe.contentType LogExchange.stringCodec[AnyRef](contentType) match { @@ -273,61 +400,7 @@ class ConsoleAppender private[ConsoleAppender] ( case _ => Vector(oe.message.toString) } } - def messageColor(level: Level.Value) = RESET - def labelColor(level: Level.Value) = - level match { - case Level.Error => RED - case Level.Warn => YELLOW - case _ => RESET - } - // success is called by ConsoleLogger. - // This should turn into an event. - private[sbt] def success(message: => String): Unit = { - appendLog(successLabelColor, Level.SuccessLabel, successMessageColor, message) - } - private[sbt] def successLabelColor = GREEN - private[sbt] def successMessageColor = RESET - - def trace(t: => Throwable, traceLevel: Int): Unit = - out.lockObject.synchronized { - if (traceLevel >= 0) - out.print(StackTrace.trimmed(t, traceLevel)) - if (traceLevel <= 2) - for (msg <- suppressedMessage(new SuppressedTraceContext(traceLevel, ansiCodesSupported && useColor))) - printLabeledLine(labelColor(Level.Error), "trace", messageColor(Level.Error), msg) - } - - def control(event: ControlEvent.Value, message: => String): Unit = - appendLog(labelColor(Level.Info), Level.Info.toString, BLUE, message) - - def appendLog(level: Level.Value, message: => String): Unit = { - appendLog(labelColor(level), level.toString, messageColor(level), message) - } - private def reset(): Unit = setColor(RESET) - - private def setColor(color: String): Unit = { - if (ansiCodesSupported && useColor) - out.lockObject.synchronized { out.print(color) } - } - private def appendLog(labelColor: String, label: String, messageColor: String, message: String): Unit = - out.lockObject.synchronized { - for (line <- message.split("""\n""")) - printLabeledLine(labelColor, label, messageColor, line) - } - private def printLabeledLine(labelColor: String, label: String, messageColor: String, line: String): Unit = - { - reset() - out.print("[") - setColor(labelColor) - out.print(label) - reset() - out.print("] ") - setColor(messageColor) - out.print(line) - reset() - out.println() - } } -final class SuppressedTraceContext(val traceLevel: Int, val useColor: Boolean) +final class SuppressedTraceContext(val traceLevel: Int, val useFormat: Boolean) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala index 72fa01594..b9834d7e8 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala @@ -33,7 +33,7 @@ object ConsoleOut { def println(s: String): Unit = synchronized { current.append(s); println() } def println(): Unit = synchronized { val s = current.toString - if (ConsoleAppender.formatEnabled && last.exists(lmsg => f(s, lmsg))) + if (ConsoleAppender.formatEnabledInEnv && last.exists(lmsg => f(s, lmsg))) lockObject.print(OverwriteLine) lockObject.println(s) last = Some(s) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala new file mode 100644 index 000000000..ad394994c --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala @@ -0,0 +1,91 @@ +package sbt.internal.util + +object EscHelpers { + + /** Escape character, used to introduce an escape sequence. */ + final val ESC = '\u001B' + + /** + * An escape terminator is a character in the range `@` (decimal value 64) to `~` (decimal value 126). + * It is the final character in an escape sequence. + * + * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes + */ + private[sbt] def isEscapeTerminator(c: Char): Boolean = + c >= '@' && c <= '~' + + /** + * Test if the character AFTER an ESC is the ANSI CSI. + * + * see: http://en.wikipedia.org/wiki/ANSI_escape_code + * + * The CSI (control sequence instruction) codes start with ESC + '['. This is for testing the second character. + * + * There is an additional CSI (one character) that we could test for, but is not frequnetly used, and we don't + * check for it. + * + * cf. http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes + */ + private def isCSI(c: Char): Boolean = c == '[' + + /** + * Tests whether or not a character needs to immediately terminate the ANSI sequence. + * + * c.f. http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements + */ + private def isAnsiTwoCharacterTerminator(c: Char): Boolean = + (c >= '@') && (c <= '_') + + /** + * Returns true if the string contains the ESC character. + * + * TODO - this should handle raw CSI (not used much) + */ + def hasEscapeSequence(s: String): Boolean = + s.indexOf(ESC) >= 0 + + /** + * Returns the string `s` with escape sequences removed. + * An escape sequence starts with the ESC character (decimal value 27) and ends with an escape terminator. + * @see isEscapeTerminator + */ + def removeEscapeSequences(s: String): String = + if (s.isEmpty || !hasEscapeSequence(s)) + s + else { + val sb = new java.lang.StringBuilder + nextESC(s, 0, sb) + sb.toString + } + + private[this] def nextESC(s: String, start: Int, sb: java.lang.StringBuilder): Unit = { + val escIndex = s.indexOf(ESC, start) + if (escIndex < 0) { + sb.append(s, start, s.length) + () + } else { + sb.append(s, start, escIndex) + val next: Int = + // If it's a CSI we skip past it and then look for a terminator. + if (isCSI(s.charAt(escIndex + 1))) skipESC(s, escIndex + 2) + else if (isAnsiTwoCharacterTerminator(s.charAt(escIndex + 1))) escIndex + 2 + else { + // There could be non-ANSI character sequences we should make sure we handle here. + skipESC(s, escIndex + 1) + } + nextESC(s, next, sb) + } + } + + /** Skips the escape sequence starting at `i-1`. `i` should be positioned at the character after the ESC that starts the sequence. */ + private[this] def skipESC(s: String, i: Int): Int = { + if (i >= s.length) { + i + } else if (isEscapeTerminator(s.charAt(i))) { + i + 1 + } else { + skipESC(s, i + 1) + } + } + +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala b/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala index 37dac9b70..dd08ba0bb 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/MainLogging.scala @@ -48,15 +48,15 @@ object MainAppender { def defaultScreen(name: String, console: ConsoleOut, suppressedMessage: SuppressedTraceContext => Option[String]): Appender = ConsoleAppender(name, console, suppressedMessage = suppressedMessage) - def defaultBacked: PrintWriter => Appender = defaultBacked(generateGlobalBackingName, ConsoleAppender.formatEnabled) - def defaultBacked(loggerName: String): PrintWriter => Appender = defaultBacked(loggerName, ConsoleAppender.formatEnabled) - def defaultBacked(useColor: Boolean): PrintWriter => Appender = defaultBacked(generateGlobalBackingName, useColor) - def defaultBacked(loggerName: String, useColor: Boolean): PrintWriter => Appender = + def defaultBacked: PrintWriter => Appender = defaultBacked(generateGlobalBackingName, ConsoleAppender.formatEnabledInEnv) + def defaultBacked(loggerName: String): PrintWriter => Appender = defaultBacked(loggerName, ConsoleAppender.formatEnabledInEnv) + def defaultBacked(useFormat: Boolean): PrintWriter => Appender = defaultBacked(generateGlobalBackingName, useFormat) + def defaultBacked(loggerName: String, useFormat: Boolean): PrintWriter => Appender = to => { ConsoleAppender( ConsoleAppender.generateName, ConsoleOut.printWriterOut(to), - useColor = useColor + useFormat = useFormat ) } diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala b/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala index c5f7d1103..c72d094af 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/MultiLogger.scala @@ -41,7 +41,7 @@ class MultiLogger(delegates: List[AbstractLogger]) extends BasicLogger { private[this] def removeEscapes(event: LogEvent): LogEvent = { - import ConsoleAppender.{ removeEscapeSequences => rm } + import EscHelpers.{ removeEscapeSequences => rm } event match { case s: Success => new Success(rm(s.msg)) case l: Log => new Log(l.level, rm(l.msg)) diff --git a/internal/util-logging/src/test/scala/Escapes.scala b/internal/util-logging/src/test/scala/Escapes.scala index a226e4d3b..0ae24a6e4 100644 --- a/internal/util-logging/src/test/scala/Escapes.scala +++ b/internal/util-logging/src/test/scala/Escapes.scala @@ -4,7 +4,7 @@ import org.scalacheck._ import Prop._ import Gen.{ listOf, oneOf } -import ConsoleAppender.{ ESC, hasEscapeSequence, isEscapeTerminator, removeEscapeSequences } +import EscHelpers.{ ESC, hasEscapeSequence, isEscapeTerminator, removeEscapeSequences } object Escapes extends Properties("Escapes") { property("genTerminator only generates terminators") =