mirror of https://github.com/sbt/sbt.git
better handling of multi-loggers with mixed escape sequence support
* multi-logger supports ansi escapes if at least one logger support them * escape sequences removed from strings for loggers without escape support
This commit is contained in:
parent
4c1a979d8a
commit
4e574d0df3
|
|
@ -27,6 +27,53 @@ object ConsoleLogger
|
|||
def println() = { out.newLine(); out.flush() }
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
def isEscapeTerminator(c: Char): Boolean =
|
||||
c >= '@' && c <= '~'
|
||||
|
||||
/** Returns true if the string contains the ESC character. */
|
||||
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)
|
||||
{
|
||||
val escIndex = s.indexOf(ESC, start)
|
||||
if(escIndex < 0)
|
||||
sb.append(s, start, s.length)
|
||||
else {
|
||||
sb.append(s, start, escIndex)
|
||||
val next = 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)
|
||||
|
||||
val formatEnabled =
|
||||
{
|
||||
import java.lang.Boolean.{getBoolean, parseBoolean}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ object MainLogging
|
|||
def defaultScreen(suppressedMessage: SuppressedTraceContext => Option[String]): AbstractLogger = ConsoleLogger(suppressedMessage = suppressedMessage)
|
||||
|
||||
def defaultBacked(useColor: Boolean = ConsoleLogger.formatEnabled): PrintWriter => ConsoleLogger =
|
||||
to => ConsoleLogger(ConsoleLogger.printWriterOut(to), useColor = useColor) // TODO: should probably filter ANSI codes when useColor=false
|
||||
to => ConsoleLogger(ConsoleLogger.printWriterOut(to), useColor = useColor)
|
||||
}
|
||||
|
||||
final case class MultiLoggerConfig(console: AbstractLogger, backed: AbstractLogger, extra: List[AbstractLogger],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ package sbt
|
|||
// on the behavior of the delegates.
|
||||
class MultiLogger(delegates: List[AbstractLogger]) extends BasicLogger
|
||||
{
|
||||
override lazy val ansiCodesSupported = delegates.forall(_.ansiCodesSupported)
|
||||
override lazy val ansiCodesSupported = delegates exists supported
|
||||
private[this] lazy val allSupportCodes = delegates forall supported
|
||||
private[this] def supported = (_: AbstractLogger).ansiCodesSupported
|
||||
|
||||
override def setLevel(newLevel: Level.Value)
|
||||
{
|
||||
super.setLevel(newLevel)
|
||||
|
|
@ -29,5 +32,24 @@ class MultiLogger(delegates: List[AbstractLogger]) extends BasicLogger
|
|||
def success(message: => String) { dispatch(new Success(message)) }
|
||||
def logAll(events: Seq[LogEvent]) { delegates.foreach(_.logAll(events)) }
|
||||
def control(event: ControlEvent.Value, message: => String) { delegates.foreach(_.control(event, message)) }
|
||||
private def dispatch(event: LogEvent) { delegates.foreach(_.log(event)) }
|
||||
private[this] def dispatch(event: LogEvent)
|
||||
{
|
||||
val plainEvent = if(allSupportCodes) event else removeEscapes(event)
|
||||
for( d <- delegates)
|
||||
if(d.ansiCodesSupported)
|
||||
d.log(event)
|
||||
else
|
||||
d.log(plainEvent)
|
||||
}
|
||||
|
||||
private[this] def removeEscapes(event: LogEvent): LogEvent =
|
||||
{
|
||||
import ConsoleLogger.{removeEscapeSequences => rm}
|
||||
event match {
|
||||
case s: Success => new Success(rm(s.msg))
|
||||
case l: Log => new Log(l.level, rm(l.msg))
|
||||
case ce: ControlEvent => new ControlEvent(ce.event, rm(ce.msg))
|
||||
case _: Trace | _: SetLevel | _: SetTrace | _: SetSuccess => event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package sbt
|
||||
|
||||
import org.scalacheck._
|
||||
import Prop._
|
||||
import Gen.{listOf, oneOf}
|
||||
|
||||
import ConsoleLogger.{ESC, hasEscapeSequence, isEscapeTerminator, removeEscapeSequences}
|
||||
|
||||
object Escapes extends Properties("Escapes")
|
||||
{
|
||||
property("genTerminator only generates terminators") =
|
||||
forAllNoShrink(genTerminator) { (c: Char) => isEscapeTerminator(c) }
|
||||
|
||||
property("genWithoutTerminator only generates terminators") =
|
||||
forAllNoShrink(genWithoutTerminator) { (s: String) =>
|
||||
s.forall { c => !isEscapeTerminator(c) }
|
||||
}
|
||||
|
||||
property("hasEscapeSequence is false when no escape character is present") = forAllNoShrink(genWithoutEscape) { (s: String) =>
|
||||
!hasEscapeSequence(s)
|
||||
}
|
||||
|
||||
property("hasEscapeSequence is true when escape character is present") = forAllNoShrink(genWithRandomEscapes) { (s: String) =>
|
||||
hasEscapeSequence(s)
|
||||
}
|
||||
|
||||
property("removeEscapeSequences is the identity when no escape character is present") = forAllNoShrink(genWithoutEscape) { (s: String) =>
|
||||
val removed: String = removeEscapeSequences(s)
|
||||
("Escape sequence removed: '" + removed + "'") |:
|
||||
(removed == s)
|
||||
}
|
||||
|
||||
property("No escape characters remain after removeEscapeSequences") = forAll { (s: String) =>
|
||||
val removed: String = removeEscapeSequences(s)
|
||||
("Escape sequence removed: '" + removed + "'") |:
|
||||
!hasEscapeSequence(removed)
|
||||
}
|
||||
|
||||
property("removeEscapeSequences returns string without escape sequences") =
|
||||
forAllNoShrink( genWithoutEscape, genEscapePairs ) { (start: String, escapes: List[EscapeAndNot]) =>
|
||||
val withEscapes: String = start + escapes.map { ean => ean.escape.makeString + ean.notEscape }
|
||||
val removed: String = removeEscapeSequences(withEscapes)
|
||||
val original = start + escapes.map(_.notEscape)
|
||||
("Input string with escapes: '" + withEscapes + "'") |:
|
||||
("Escapes removed '" + removed + "'") |:
|
||||
(original == removed)
|
||||
}
|
||||
|
||||
final case class EscapeAndNot(escape: EscapeSequence, notEscape: String)
|
||||
final case class EscapeSequence(content: String, terminator: Char)
|
||||
{
|
||||
assert( content.forall(c => !isEscapeTerminator(c) ), "Escape sequence content contains an escape terminator: '" + content + "'" )
|
||||
assert( isEscapeTerminator(terminator) )
|
||||
def makeString: String = ESC + content + terminator
|
||||
}
|
||||
private[this] def noEscape(s: String): String = s.replace(ESC, ' ')
|
||||
|
||||
lazy val genEscapeSequence: Gen[EscapeSequence] = oneOf(genKnownSequence, genArbitraryEscapeSequence)
|
||||
lazy val genEscapePair: Gen[EscapeAndNot] = for(esc <- genEscapeSequence; not <- genWithoutEscape) yield EscapeAndNot(esc, not)
|
||||
lazy val genEscapePairs: Gen[List[EscapeAndNot]] = listOf(genEscapePair)
|
||||
|
||||
lazy val genArbitraryEscapeSequence: Gen[EscapeSequence] =
|
||||
for(content <- genWithoutTerminator; term <- genTerminator) yield
|
||||
new EscapeSequence(content, term)
|
||||
|
||||
lazy val genKnownSequence: Gen[EscapeSequence] =
|
||||
oneOf((misc ++ setGraphicsMode ++ setMode ++ resetMode).map(toEscapeSequence))
|
||||
|
||||
def toEscapeSequence(s: String): EscapeSequence = EscapeSequence(s.init, s.last)
|
||||
|
||||
lazy val misc = Seq("14;23H", "5;3f", "2A", "94B", "19C", "85D", "s", "u", "2J", "K")
|
||||
|
||||
lazy val setGraphicsMode: Seq[String] =
|
||||
for(txt <- 0 to 8; fg <- 30 to 37; bg <- 40 to 47) yield
|
||||
txt.toString + ";" + fg.toString + ";" + bg.toString + "m"
|
||||
|
||||
lazy val resetMode = setModeLike('I')
|
||||
lazy val setMode = setModeLike('h')
|
||||
def setModeLike(term: Char): Seq[String] = (0 to 19).map(i => "=" + i.toString + term)
|
||||
|
||||
lazy val genWithoutTerminator = genRawString.map( _.filter { c => !isEscapeTerminator(c) } )
|
||||
|
||||
lazy val genTerminator: Gen[Char] = Gen.choose('@', '~')
|
||||
lazy val genWithoutEscape: Gen[String] = genRawString.map(noEscape)
|
||||
|
||||
def genWithRandomEscapes: Gen[String] =
|
||||
for(ls <- listOf(genRawString); end <- genRawString) yield
|
||||
ls.mkString("", ESC.toString, ESC.toString + end)
|
||||
|
||||
private def genRawString = Arbitrary.arbString.arbitrary
|
||||
}
|
||||
Loading…
Reference in New Issue