app/packer/PackerOutputParser.scala (64 lines of code) (raw):

package packer import models.{MessagePart, AmiId} import scala.collection.mutable.ArrayBuffer import scala.util.matching.Regex.MatchIterator object PackerOutputParser { sealed abstract class PackerEvent case class UiOutput(logLevel: String, messageParts: List[MessagePart]) extends PackerEvent case class AmiCreated(amiId: AmiId) extends PackerEvent private val UiOutputRegex = """^\d+,,ui,(.*?),(.*)$""".r private val AmiCreatedRegex = """^\d+,.*,artifact,\d+,id,[a-z0-9-]*:(.*)$""".r def parseLine(line: String): Option[PackerEvent] = line match { case UiOutputRegex(messageType, output) => Some(UiOutput(toLogLevel(messageType), parseUiOutput(output))) case AmiCreatedRegex(amiId) => Some(AmiCreated(AmiId(amiId))) case _ => None } // Message type will be one of "say", "message" or "error". // Not really sure of the difference between say and message. private def toLogLevel(messageType: String) = messageType match { case "error" => "error" case _ => "info" } // Matches a piece of text wrapped in ANSI codes, e.g. "\u001B[0;32mHello I am green\u001B[0m" private val AnsiColouredPart = "(?s)\u001B\\[0;(\\d{1,2})m(.*?)\u001B\\[0m".r private def parseUiOutput(output: String): List[MessagePart] = { val message = output .replace("%!(PACKER_COMMA)", ",") .replace("\\n", "\n") .replace("\\r", "\r") parseAnsiColouredString(message).toList } /* Note: while this is not a full-blown ANSI code parser, it should handle anything Packer throws at it. Packer kindly terminates all coloured sections with a reset, which makes things slightly easier for us. */ private def parseAnsiColouredString(message: String): Seq[MessagePart] = { val it = AnsiColouredPart.findAllIn(message) val parts = ArrayBuffer.empty[MessagePart] var previousEndIndex = 0 while (it.hasNext) { it.next() if (it.start > previousEndIndex) { // Add any non-coloured part between the end of the previous coloured part // and the start of this one parts += MessagePart( message.substring(previousEndIndex, it.start), MessagePart.defaultColour ) } val colourCode = it.group(1) val colour = AnsiColours.getOrElse(colourCode, MessagePart.defaultColour) val text = it.group(2) parts += MessagePart(text, colour) previousEndIndex = it.end } if (previousEndIndex < message.length) { // Add any non-coloured part after the final coloured part parts += MessagePart( message.substring(previousEndIndex), MessagePart.defaultColour ) } parts.toSeq } // Only handle the foreground colours private val AnsiColours = Map( "31" -> "#B43C2A", "32" -> "#00C200", "33" -> "#C7C400", "34" -> "#2744C7", "35" -> "#C040BE", "36" -> "#00C5C7" ) /* iTerm2's default theme */ }