// Copyright 2014 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.lib.util.io; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.EnumSet; import java.util.logging.Logger; import java.util.regex.Pattern; /** * Allows to print "colored" strings by parsing predefined string keywords, * which, depending on the useColor value are either replaced with ANSI terminal * coloring sequences (as defined by the {@link AnsiTerminal} class) or stripped. * * Supported keywords are defined by the enum {@link AnsiTerminalPrinter.Mode}. * Following keywords are supported: * INFO - switches color to green. * ERROR - switches color to bold red. * WARNING - switches color to magenta. * NORMAL - resets terminal to the default state. * * Each keyword is starts with prefix "{#" followed by the enum constant name * and suffix "#}". Keywords should not be inserted manually - provided enum * constants should be used instead. */ @ThreadCompatible public class AnsiTerminalPrinter { private static final String MODE_PREFIX = "{#"; private static final String MODE_SUFFIX = "#}"; // Mode pattern must match MODE_PREFIX and do lookahead for the rest of the // mode string. private static final String MODE_PATTERN = "\\{\\#(?=[A-Z]+\\#\\})"; /** * List of supported coloring modes for the {@link AnsiTerminalPrinter}. */ public static enum Mode { INFO, // green ERROR, // bold red WARNING, // magenta DEFAULT; // default color @Override public String toString() { return MODE_PREFIX + name() + MODE_SUFFIX; } } private static final Logger LOG = Logger.getLogger(AnsiTerminalPrinter.class.getName()); private static final EnumSet<Mode> MODES = EnumSet.allOf(Mode.class); private static final Pattern PATTERN = Pattern.compile(MODE_PATTERN); private final OutputStream stream; private final PrintWriter writer; private final AnsiTerminal terminal; private boolean useColor; private Mode lastMode = Mode.DEFAULT; /** * Creates new instance using provided OutputStream and sets coloring logic * for that instance. */ public AnsiTerminalPrinter(OutputStream out, boolean useColor) { this.useColor = useColor; terminal = new AnsiTerminal(out); writer = new PrintWriter(out, true); stream = out; } /** * Writes the specified string to the output stream while injecting coloring * sequences when appropriate mode keyword is found and flushes. * * List of supported mode keywords is defined by the enum {@link Mode}. * * See class documentation for details. */ public void print(String str) { for (String part : PATTERN.split(str)) { int index = part.indexOf(MODE_SUFFIX); // Mode name will contain at least one character, so suffix index // must be at least 1. If it isn't then there is no match. if (index > 1) { for (Mode mode : MODES) { if (index == mode.name().length() && part.startsWith(mode.name())) { setupTerminal(mode); part = part.substring(index + MODE_SUFFIX.length()); break; } } } writer.print(part); writer.flush(); } } public void printLn(String str) { print(str + "\n"); } /** * Returns the underlying OutputStream. */ public OutputStream getOutputStream() { return stream; } /** * Injects coloring escape sequences if output should be colored and mode * has been changed. */ private void setupTerminal(Mode mode) { if (!useColor) { return; } try { if (lastMode != mode) { terminal.resetTerminal(); lastMode = mode; if (mode == Mode.DEFAULT) { return; // Terminal is already reset - nothing else to do. } else if (mode == Mode.INFO) { terminal.textGreen(); } else if (mode == Mode.ERROR) { terminal.textRed(); terminal.textBold(); } else if (mode == Mode.WARNING) { terminal.textMagenta(); } } } catch (IOException e) { // AnsiTerminal state is now considered to be inconsistent - coloring // should be disabled to prevent future use of AnsiTerminal instance. LOG.warning("Disabling coloring due to " + e.getMessage()); useColor = false; } } }