You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2015/02/18 09:44:28 UTC

[3/3] karaf git commit: [KARAF-3537] More and less commands do not handle ansi escape sequences correctly

[KARAF-3537] More and less commands do not handle ansi escape sequences correctly


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/715a72a0
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/715a72a0
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/715a72a0

Branch: refs/heads/master
Commit: 715a72a0b245d818d1411fc3b992580fcd011798
Parents: 17178f2
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Tue Feb 17 17:51:53 2015 +0100
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Feb 18 09:39:46 2015 +0100

----------------------------------------------------------------------
 .../karaf/shell/commands/impl/LessAction.java   |  48 +--
 .../karaf/shell/commands/impl/MoreAction.java   |  37 +-
 .../karaf/shell/support/ansi/AnsiSplitter.java  | 378 +++++++++++++++++++
 .../shell/support/ansi/AnsiSplitterTest.java    |  35 ++
 4 files changed, 428 insertions(+), 70 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/715a72a0/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
----------------------------------------------------------------------
diff --git a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
index 4185c06..b2e9448 100644
--- a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
+++ b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
@@ -48,6 +48,7 @@ import org.apache.karaf.shell.api.console.Session;
 import org.apache.karaf.shell.api.console.Signal;
 import org.apache.karaf.shell.api.console.SignalListener;
 import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.ansi.AnsiSplitter;
 import org.jledit.jline.NonBlockingInputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -454,7 +455,7 @@ public class LessAction implements Action, SignalListener {
                 int off = offsetInLine;
                 for (int l = 0; l < height - 1; l++) {
                     String line = getLine(lastLineToDisplay);
-                    if (line.length() > off + width) {
+                    if (ansiLength(line) > off + width) {
                         off += width;
                     } else {
                         off = 0;
@@ -468,7 +469,7 @@ public class LessAction implements Action, SignalListener {
             }
 
             String line = getLine(firstLineToDisplay);
-            if (line.length() > width + offsetInLine) {
+            if (ansiLength(line) > width + offsetInLine) {
                 offsetInLine += width;
             } else {
                 offsetInLine = 0;
@@ -485,7 +486,8 @@ public class LessAction implements Action, SignalListener {
             } else if (firstLineInMemory < firstLineToDisplay) {
                 firstLineToDisplay--;
                 String line = getLine(firstLineToDisplay);
-                offsetInLine = line.length() - line.length() % width;
+                int length = ansiLength(line);
+                offsetInLine = length - length % width;
             } else {
                 bof();
                 return;
@@ -557,7 +559,7 @@ public class LessAction implements Action, SignalListener {
                     curLine = "";
                 }
                 if (compiled != null) {
-                    curLine = compiled.matcher(curLine).replaceAll("\033[7m$1\033[0m");
+                    curLine = compiled.matcher(curLine).replaceAll("\033[7m$1\033[27m");
                 }
             }
             String toDisplay;
@@ -605,40 +607,12 @@ public class LessAction implements Action, SignalListener {
         return compiled;
     }
 
-    private String ansiSubstring(String curLine, int begin, int end) {
-        int printPos = 0;
-        int csi = 0;
-        char sgr = '0';
+    private int ansiLength(String curLine) throws IOException {
+        return AnsiSplitter.length(curLine);
+    }
 
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < curLine.length() && printPos < end; i++) {
-            char c = curLine.charAt(i);
-            if (csi == 0 && c == '\033') {
-                csi++;
-            } else if (csi == 1 && c == '[') {
-                csi++;
-            } else if (csi == 2) {
-                sgr = c;
-                csi++;
-            } else if (csi == 3 && c == 'm') {
-                csi = 0;
-                if (printPos >= begin) {
-                    sb.append("\033[").append(sgr).append("m");
-                }
-            } else {
-                if (printPos == begin && sgr != '0') {
-                    sb.append("\033[7m");
-                }
-                if (printPos >= begin && printPos < end) {
-                    sb.append(c);
-                }
-                ++printPos;
-            }
-        }
-        if (sgr != '0') {
-            sb.append("\033[0m");
-        }
-        return sb.toString();
+    private String ansiSubstring(String curLine, int begin, int end) throws IOException {
+        return AnsiSplitter.substring(curLine, begin, end);
     }
 
     String getLine(int line) throws IOException {

http://git-wip-us.apache.org/repos/asf/karaf/blob/715a72a0/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/MoreAction.java
----------------------------------------------------------------------
diff --git a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/MoreAction.java b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/MoreAction.java
index 5db87f6..0d344a9 100644
--- a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/MoreAction.java
+++ b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/MoreAction.java
@@ -17,20 +17,18 @@
 package org.apache.karaf.shell.commands.impl;
 
 import java.io.BufferedReader;
-import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.lang.reflect.Method;
-import java.util.LinkedList;
-import java.util.List;
 
 import org.apache.karaf.shell.api.action.Action;
 import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.Option;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.api.console.Terminal;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.ansi.AnsiSplitter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -66,7 +64,7 @@ public class MoreAction implements Action {
                 if (lines == 0) {
                     lines = terminal.getHeight();
                 }
-                LineSplitter reader = new LineSplitter(new BufferedReader(new InputStreamReader(System.in)), terminal.getWidth());
+                AnsiSplitter.AnsiBufferedReader reader = AnsiSplitter.splitter(System.in, terminal.getWidth());
                 int count = 0;
                 int c;
                 do {
@@ -120,33 +118,6 @@ public class MoreAction implements Action {
         return null;
     }
 
-    public static class LineSplitter {
-
-        private final BufferedReader reader;
-        private final int width;
-        private final List<String> lines = new LinkedList<String>();
-
-        public LineSplitter(BufferedReader reader, int width) {
-            this.reader = reader;
-            this.width = width;
-        }
-
-        public String readLine() throws IOException {
-            if (lines.isEmpty()) {
-                String str = reader.readLine();
-                if (str == null) {
-                    return null;
-                }
-                while (str.length() > width) {
-                    lines.add(str.substring(0, width));
-                    str = str.substring(width);
-                }
-                lines.add(str);
-            }
-            return lines.remove(0);
-        }
-    }
-
     protected boolean isTty(OutputStream out) {
         try {
             Method mth = out.getClass().getDeclaredMethod("getCurrent");

http://git-wip-us.apache.org/repos/asf/karaf/blob/715a72a0/shell/core/src/main/java/org/apache/karaf/shell/support/ansi/AnsiSplitter.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/support/ansi/AnsiSplitter.java b/shell/core/src/main/java/org/apache/karaf/shell/support/ansi/AnsiSplitter.java
new file mode 100644
index 0000000..f855c1c
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/support/ansi/AnsiSplitter.java
@@ -0,0 +1,378 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.karaf.shell.support.ansi;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.fusesource.jansi.Ansi;
+import org.fusesource.jansi.AnsiOutputStream;
+
+public class AnsiSplitter {
+
+    public static List<String> splitLines(String text, int maxLength) throws IOException {
+        AnsiOutputStreamSplitter splitter = new AnsiOutputStreamSplitter(maxLength);
+        splitter.write(text.getBytes());
+        splitter.close();
+        return splitter.lines;
+    }
+
+    public static String substring(String text, int begin, int end) throws IOException {
+        AnsiOutputStreamSplitter splitter = new AnsiOutputStreamSplitter(begin, end, Integer.MAX_VALUE);
+        splitter.write(text.getBytes());
+        splitter.close();
+        return splitter.lines.get(0);
+    }
+
+    public static int length(String curLine) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        AnsiOutputStream aos = new AnsiOutputStream(baos);
+        aos.write(curLine.getBytes());
+        aos.close();
+        return baos.toString().length();
+    }
+
+    public static String cut(String text, int maxLength)  throws IOException {
+        return splitLines(text, maxLength).get(0);
+    }
+
+    public static AnsiBufferedReader window(InputStream is, int begin, int end) throws IOException {
+        return new AnsiBufferedReader(is, begin, end, Integer.MAX_VALUE);
+    }
+
+    public static AnsiBufferedReader splitter(InputStream is, int maxLength) throws IOException {
+        return new AnsiBufferedReader(is, 0, Integer.MAX_VALUE, maxLength);
+    }
+
+
+    public static class AnsiBufferedReader implements Closeable {
+
+        private final InputStream in;
+        private final AnsiOutputStreamSplitter splitter;
+
+        public AnsiBufferedReader(InputStream in, int begin, int end, int maxLength) {
+            this.in = in;
+            this.splitter = new AnsiOutputStreamSplitter(begin, end, maxLength);
+        }
+
+        public String readLine() throws IOException {
+            while (splitter.lines.isEmpty()) {
+                int c = in.read();
+                if (c < 0) {
+                    splitter.flushLine(false);
+                    break;
+                } else {
+                    splitter.write(c);
+                }
+            }
+            if (splitter.lines.isEmpty()) {
+                return null;
+            } else {
+                return splitter.lines.remove(0);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+        }
+    }
+
+    static class AnsiOutputStreamSplitter extends AnsiOutputStream {
+
+        protected static final int ATTRIBUTE_NEGATIVE_OFF = 27;
+
+        Ansi.Attribute intensity;
+        Ansi.Attribute underline;
+        Ansi.Attribute blink;
+        Ansi.Attribute negative;
+        Ansi.Color fg;
+        Ansi.Color bg;
+
+        private int begin;
+        private int length;
+        private int maxLength;
+        private int escapeLength;
+        private int windowState;
+        private List<String> lines = new ArrayList<>();
+
+        public AnsiOutputStreamSplitter(int maxLength) {
+            this(0, Integer.MAX_VALUE, maxLength);
+        }
+
+        public AnsiOutputStreamSplitter(int begin, int end, int maxLength) {
+            super(new ByteArrayOutputStream());
+            this.begin = begin;
+            this.length = end - begin;
+            this.maxLength = maxLength - begin;
+            this.windowState = begin > 0 ? 0 : 1;
+            reset();
+        }
+
+        protected void reset() {
+            intensity = Ansi.Attribute.INTENSITY_BOLD_OFF;
+            underline = Ansi.Attribute.UNDERLINE_OFF;
+            blink = Ansi.Attribute.BLINK_OFF;
+            negative = Ansi.Attribute.NEGATIVE_OFF;
+            fg = Ansi.Color.DEFAULT;
+            bg = Ansi.Color.DEFAULT;
+        }
+
+        @Override
+        public void write(int data) throws IOException {
+            if (data == '\n') {
+                flushLine(true);
+            } else {
+                if (windowState != 2) {
+                    super.write(data);
+                }
+                ByteArrayOutputStream baos = (ByteArrayOutputStream) out;
+                if (windowState == 0 && baos.size() - escapeLength > begin) {
+                    windowState = 1;
+                    int nbMissing = baos.size() - escapeLength - begin;
+                    byte[] old = baos.toByteArray();
+                    beginAttributes();
+                    baos.write(old, old.length - nbMissing, nbMissing);
+                } else if (windowState == 1 && baos.size() - escapeLength >= length) {
+                    windowState = 2;
+                    endAttributes();
+                    reset();
+                }
+                if (baos.size() - escapeLength >= maxLength) {
+                    flushLine(true);
+                }
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (windowState == 0) {
+                beginAttributes();
+            }
+            flushLine(lines.isEmpty());
+            super.close();
+        }
+
+        private void flushLine(boolean force) throws IOException {
+            ByteArrayOutputStream baos = (ByteArrayOutputStream) out;
+            if (windowState == 0) {
+                beginAttributes();
+            }
+            if (force || baos.size() > escapeLength) {
+                endAttributes();
+                lines.add(new String(baos.toByteArray()));
+                beginAttributes();
+            }
+            windowState = 0;
+        }
+
+        private void endAttributes() throws IOException {
+            if (intensity != Ansi.Attribute.INTENSITY_BOLD_OFF) {
+                setAttribute(Ansi.Attribute.INTENSITY_BOLD_OFF);
+            }
+            if (underline != Ansi.Attribute.UNDERLINE_OFF) {
+                setAttribute(Ansi.Attribute.UNDERLINE_OFF);
+            }
+            if (blink != Ansi.Attribute.BLINK_OFF) {
+                setAttribute(Ansi.Attribute.BLINK_OFF);
+            }
+            if (negative != Ansi.Attribute.NEGATIVE_OFF) {
+                setAttribute(Ansi.Attribute.NEGATIVE_OFF);
+            }
+            if (fg != Ansi.Color.DEFAULT) {
+                setAttributeFg(Ansi.Color.DEFAULT);
+            }
+            if (bg != Ansi.Color.DEFAULT) {
+                setAttributeBg(Ansi.Color.DEFAULT);
+            }
+        }
+
+        private void beginAttributes() throws IOException {
+            ((ByteArrayOutputStream) out).reset();
+            escapeLength = 0;
+            if (intensity != Ansi.Attribute.INTENSITY_BOLD_OFF) {
+                setAttribute(intensity);
+            }
+            if (underline != Ansi.Attribute.UNDERLINE_OFF) {
+                setAttribute(underline);
+            }
+            if (blink != Ansi.Attribute.BLINK_OFF) {
+                setAttribute(blink);
+            }
+            if (negative != Ansi.Attribute.NEGATIVE_OFF) {
+                setAttribute(negative);
+            }
+            if (fg != Ansi.Color.DEFAULT) {
+                setAttributeFg(fg);
+            }
+            if (bg != Ansi.Color.DEFAULT) {
+                setAttributeBg(bg);
+            }
+        }
+
+        @Override
+        protected void processAttributeRest() throws IOException {
+            setAttribute(Ansi.Attribute.RESET);
+            reset();
+        }
+
+        @Override
+        protected void processSetAttribute(int attribute) throws IOException {
+            switch(attribute) {
+            case ATTRIBUTE_INTENSITY_BOLD:
+                setIntensity(Ansi.Attribute.INTENSITY_BOLD);
+                break;
+            case ATTRIBUTE_INTENSITY_FAINT:
+                setIntensity(Ansi.Attribute.INTENSITY_FAINT);
+                break;
+            case ATTRIBUTE_INTENSITY_NORMAL:
+                setIntensity(Ansi.Attribute.INTENSITY_BOLD_OFF);
+                break;
+            case ATTRIBUTE_UNDERLINE:
+                setUnderline(Ansi.Attribute.UNDERLINE);
+                break;
+            case ATTRIBUTE_UNDERLINE_DOUBLE:
+                setUnderline(Ansi.Attribute.UNDERLINE_DOUBLE);
+                break;
+            case ATTRIBUTE_UNDERLINE_OFF:
+                setUnderline(Ansi.Attribute.UNDERLINE_OFF);
+                break;
+            case ATTRIBUTE_BLINK_OFF:
+                setBlink(Ansi.Attribute.BLINK_OFF);
+                break;
+            case ATTRIBUTE_BLINK_SLOW:
+                setBlink(Ansi.Attribute.BLINK_SLOW);
+                break;
+            case ATTRIBUTE_BLINK_FAST:
+                setBlink(Ansi.Attribute.BLINK_FAST);
+                break;
+            case ATTRIBUTE_NEGATIVE_ON:
+                setNegative(Ansi.Attribute.NEGATIVE_ON);
+                break;
+            case ATTRIBUTE_NEGATIVE_OFF:
+                setNegative(Ansi.Attribute.NEGATIVE_OFF);
+                break;
+            default:
+                break;
+            }
+        }
+
+        @Override
+        protected void processSetForegroundColor(int color) throws IOException {
+            Ansi.Color c;
+            switch (color) {
+            case 0: c = Ansi.Color.BLACK; break;
+            case 1: c = Ansi.Color.RED; break;
+            case 2: c = Ansi.Color.GREEN; break;
+            case 3: c = Ansi.Color.YELLOW; break;
+            case 4: c = Ansi.Color.BLUE; break;
+            case 5: c = Ansi.Color.MAGENTA; break;
+            case 6: c = Ansi.Color.CYAN; break;
+            case 7: c = Ansi.Color.WHITE; break;
+            case 9: c = Ansi.Color.DEFAULT; break;
+            default: return;
+            }
+            if (this.fg != c) {
+                this.fg = c;
+                setAttributeFg(c);
+            }
+        }
+
+        @Override
+        protected void processSetBackgroundColor(int color) throws IOException {
+            Ansi.Color c;
+            switch (color) {
+            case 0: c = Ansi.Color.BLACK; break;
+            case 1: c = Ansi.Color.RED; break;
+            case 2: c = Ansi.Color.GREEN; break;
+            case 3: c = Ansi.Color.YELLOW; break;
+            case 4: c = Ansi.Color.BLUE; break;
+            case 5: c = Ansi.Color.MAGENTA; break;
+            case 6: c = Ansi.Color.CYAN; break;
+            case 7: c = Ansi.Color.WHITE; break;
+            case 9: c = Ansi.Color.DEFAULT; break;
+            default: return;
+            }
+            if (this.bg != c) {
+                this.bg = c;
+                setAttributeBg(c);
+            }
+        }
+
+        @Override
+        protected void processDefaultTextColor() throws IOException {
+            processSetForegroundColor(9);
+        }
+
+        @Override
+        protected void processDefaultBackgroundColor() throws IOException {
+            processSetBackgroundColor(9);
+        }
+
+        protected void setIntensity(Ansi.Attribute intensity) throws IOException {
+            if (this.intensity != intensity) {
+                this.intensity = intensity;
+                setAttribute(intensity);
+            }
+        }
+
+        protected void setUnderline(Ansi.Attribute underline) throws IOException {
+            if (this.underline != underline) {
+                this.underline = underline;
+                setAttribute(underline);
+            }
+        }
+
+        protected void setBlink(Ansi.Attribute blink) throws IOException {
+            if (this.blink != blink) {
+                this.blink = blink;
+                setAttribute(blink);
+            }
+        }
+
+        protected void setNegative(Ansi.Attribute negative) throws IOException {
+            if (this.negative != negative) {
+                this.negative = negative;
+                setAttribute(negative);
+            }
+        }
+
+        private void setAttributeFg(Ansi.Color color) throws IOException {
+            String sequence = Ansi.ansi().fg(color).toString();
+            escapeLength += sequence.length();
+            out.write(sequence.getBytes());
+        }
+
+        private void setAttributeBg(Ansi.Color color) throws IOException {
+            String sequence = Ansi.ansi().bg(color).toString();
+            escapeLength += sequence.length();
+            out.write(sequence.getBytes());
+        }
+
+        private void setAttribute(Ansi.Attribute attribute) throws IOException {
+            String sequence = Ansi.ansi().a(attribute).toString();
+            escapeLength += sequence.length();
+            out.write(sequence.getBytes());
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/715a72a0/shell/core/src/test/java/org/apache/karaf/shell/support/ansi/AnsiSplitterTest.java
----------------------------------------------------------------------
diff --git a/shell/core/src/test/java/org/apache/karaf/shell/support/ansi/AnsiSplitterTest.java b/shell/core/src/test/java/org/apache/karaf/shell/support/ansi/AnsiSplitterTest.java
new file mode 100644
index 0000000..be4148c
--- /dev/null
+++ b/shell/core/src/test/java/org/apache/karaf/shell/support/ansi/AnsiSplitterTest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.karaf.shell.support.ansi;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class AnsiSplitterTest {
+
+    @Test
+    public void testWindow() throws IOException {
+        String text = "\u001B[1mThis is bold.\u001B[22m";
+        assertEquals("\u001B[1mis\u001B[22m", AnsiSplitter.substring(text, 5, 7));
+    }
+
+}