| // 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; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| /** | |
| * A pass-thru {@link OutputStream} that strips ANSI control codes. | |
| */ | |
| public class AnsiStrippingOutputStream extends OutputStream { | |
| // The idea is straightforward: the regexp for ANSI control codes is | |
| // \x1b\[[;0-9]*[a-zA-Z] . Implementing it as a stream is a little ugly, | |
| // though. | |
| private enum State { | |
| NORMAL, | |
| AFTER_ESCAPE, | |
| PARAMETER, | |
| } | |
| private byte[] outputBuffer; | |
| private int outputBufferPos; | |
| private static final int ESCAPE_BUFFER_LENGTH = 128; | |
| private byte[] escapeCodeBuffer; | |
| private int escapeCodeBufferPos; | |
| private OutputStream output; | |
| private State state; | |
| public AnsiStrippingOutputStream(OutputStream output) { | |
| this.output = output; | |
| escapeCodeBuffer = new byte[ESCAPE_BUFFER_LENGTH]; | |
| escapeCodeBufferPos = 0; | |
| state = State.NORMAL; | |
| } | |
| @Override | |
| public synchronized void write(int b) throws IOException { | |
| // As per the contract of OutputStream.write(int) | |
| byte[] array = { (byte) (b & 0xff) }; | |
| write(array, 0, 1); | |
| } | |
| @Override | |
| public synchronized void write(byte[] b, int off, int len) throws IOException { | |
| int i = 0; | |
| if (state == State.NORMAL) { | |
| // Avoid outputBuffer allocation entirely if that's possible | |
| while ((i < len) && (b[off + i] != 0x1b)) { | |
| i++; | |
| } | |
| if (i == len) { | |
| output.write(b, off, len); | |
| return; | |
| } | |
| } | |
| // In the worst case, the contents of the escape buffer and the contents | |
| // of the input buffer are both copied to the output, so the length of the | |
| // output buffer should be the sum of the length of both these buffers. | |
| outputBuffer = new byte[len + ESCAPE_BUFFER_LENGTH]; | |
| System.arraycopy(b, off, outputBuffer, 0, i); | |
| outputBufferPos = i; | |
| for (; i < len; i++) { | |
| processByte(b[off + i]); | |
| } | |
| try { | |
| output.write(outputBuffer, 0, outputBufferPos); | |
| } finally { | |
| outputBuffer = null; // Make it possible to garbage collect the array | |
| } | |
| } | |
| private void processByte(byte b) { | |
| switch (state) { | |
| case NORMAL: | |
| if (escapeCodeBufferPos != 0) { | |
| throw new IllegalStateException(); | |
| } | |
| if (b == 0x1b) { | |
| state = State.AFTER_ESCAPE; | |
| addByteToEscapeBuffer(b); | |
| } else { | |
| dumpByte(b); | |
| } | |
| break; | |
| case AFTER_ESCAPE: | |
| if (b == '[') { | |
| state = State.PARAMETER; | |
| addByteToEscapeBuffer(b); | |
| } else if (b == 0x1b) { | |
| dumpEscapeBuffer(); | |
| state = State.AFTER_ESCAPE; | |
| addByteToEscapeBuffer(b); | |
| } else { | |
| dumpEscapeBuffer(); | |
| dumpByte(b); | |
| state = State.NORMAL; | |
| } | |
| break; | |
| case PARAMETER: | |
| if ((b >= '0' && b <= '9') || b == ';') { | |
| // Parameter continues | |
| addByteToEscapeBuffer(b); | |
| } else if ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) { | |
| // Found a control sequence, discard it and revert to normal state | |
| discardEscapeBuffer(); | |
| state = State.NORMAL; | |
| } else if (b == 0x1b) { | |
| // Another escape sequence begins immediately after, and this is | |
| // an illegal escape sequence | |
| dumpEscapeBuffer(); | |
| state = State.AFTER_ESCAPE; | |
| addByteToEscapeBuffer(b); | |
| } else { | |
| // Illegal control sequence, output it | |
| dumpEscapeBuffer(); | |
| state = State.NORMAL; | |
| } | |
| break; | |
| } | |
| } | |
| private void addByteToEscapeBuffer(byte b) { | |
| escapeCodeBuffer[escapeCodeBufferPos++] = b; | |
| if (escapeCodeBufferPos == ESCAPE_BUFFER_LENGTH) { | |
| // Buffer full. Assume that no sane code emits an ANSI control code this | |
| // long and revert to normal state. | |
| dumpEscapeBuffer(); | |
| state = State.NORMAL; | |
| } | |
| } | |
| private void discardEscapeBuffer() { | |
| escapeCodeBufferPos = 0; | |
| } | |
| private void dumpByte(byte b) { | |
| outputBuffer[outputBufferPos++] = b; | |
| } | |
| private void dumpEscapeBuffer() { | |
| System.arraycopy(escapeCodeBuffer, 0, | |
| outputBuffer, outputBufferPos, escapeCodeBufferPos); | |
| outputBufferPos += escapeCodeBufferPos; | |
| escapeCodeBufferPos = 0; | |
| } | |
| @Override | |
| public void flush() throws IOException { | |
| output.flush(); | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| output.close(); | |
| } | |
| } |