Skip to content

Commit

Permalink
Add Strip function to strip ANSI escape sequences from a byte slice.
Browse files Browse the repository at this point in the history
Fix up some documentation.
  • Loading branch information
bormanp committed Sep 20, 2016
1 parent c4f1e2c commit 86f4995
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 9 deletions.
31 changes: 28 additions & 3 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ type S struct {
Params []string // parameters
}

// String returns s as a string. If s has no type or s.Code is unrecognized
// then s.Code is returned (s represents plain text, or there is an error).
// If s.Code is recognized, the original escape sequence is constructed and
// returned (single byte CSI sequences are translated to multi-byte sequences).
func (s *S) String() string {
if s.Type == "" {
return string(s.Code)
}
seq := s.Code.S()
if seq == nil {
return string(s.Code)
}

switch {
case s.Type == "C1":
// C1 sequences parameters follow the sequence
return string(seq.Type) + string(seq.Code) + strings.Join(s.Params, ";")
case len(s.Code) > 1 && (lookup[s.Code[1]]&sos) == sos:
// SOS sequence parameters follow the sequence followed by ST
return string(seq.Type) + string(seq.Code) + strings.Join(s.Params, ";") + string(ST)
default:
return string(seq.Type) + strings.Join(s.Params, ";") + string(seq.Code)
}
}

const (
sos = (1 << iota) // start of string
st // string terminator
Expand Down Expand Up @@ -61,9 +86,9 @@ var (
// ms returns the bytes of in as a Name
func ms(in ...byte) Name { return Name(in) }

// Decode decodes the next sequence in in, returning the bytes following
// the sequence. The sequence S, and any possible error. Single byte C1
// sequences are expanded to two byte sequences
// Decode decodes the next sequence in in, returning the bytes following the
// sequence, the sequence s, and any possible error. The value of s will never
// be nil. Single byte C1 sequences are expanded to two byte sequences.
func Decode(in []byte) (out []byte, s *S, err error) {
if len(in) == 0 {
return nil, nil, nil
Expand Down
19 changes: 13 additions & 6 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package ansi

import (
"reflect"
"strings"
"testing"
)

Expand All @@ -15,14 +16,9 @@ func TestDecode(t *testing.T) {
rem string
lu *Sequence
out *S
s string // what out.String should return, if not in.
err error
}{
{
in: "abc",
out: &S{
Code: "abc",
},
},
{
in: "abc",
out: &S{
Expand Down Expand Up @@ -52,6 +48,7 @@ func TestDecode(t *testing.T) {
},
{
in: "\202", // One byte version of "\033B"
s: "\033B",
out: &S{
Code: "\033B",
Type: "C1",
Expand Down Expand Up @@ -87,6 +84,7 @@ func TestDecode(t *testing.T) {
},
{
in: "\033[A",
s: "\033[1A",
out: &S{
Code: "\033[A",
Type: "CSI",
Expand Down Expand Up @@ -135,6 +133,7 @@ func TestDecode(t *testing.T) {
},
{
in: "\033[42 c",
s: "\033[42;32 c",
out: &S{
Code: "\033[ c",
Type: "CSI",
Expand Down Expand Up @@ -281,6 +280,14 @@ func TestDecode(t *testing.T) {
if lu != tt.lu {
t.Errorf("%q: got lu %#v, want %#v", tt.in, lu, tt.lu)
}
if tt.s == "" {
tt.s = strings.TrimSuffix(tt.in, tt.rem)
}
if err == nil {
if s := out.String(); s != tt.s {
t.Errorf("%q: String got %q, want %q", tt.in, s, tt.s)
}
}
}
}

Expand Down
56 changes: 56 additions & 0 deletions strip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ansi

import (
"fmt"
"strings"
)

// An errorList is simply a list of errors.
type errorList []error

func (e errorList) Error() string {
if len(e) == 0 {
return ""
}
parts := make([]string, len(e))
for x, err := range e {
parts[x] = err.Error()
}
return strings.Join(parts, "\n")
}

func (e errorList) err() error {
switch len(e) {
case 0:
return nil
case 1:
return e[0]
default:
return e
}
}

// Strip returns in with all ANSI escape sequences stripped. An error is
// also returned if one or more of the stripped escape sequences are invalid.
func Strip(in []byte) ([]byte, error) {
var errs errorList
var out []string
var s *S
var err error
for len(in) > 0 {
in, s, err = Decode(in)
if err != nil {
errs = append(errs, fmt.Errorf("%q: %v", s, err))
}
// If s.Type is "" then s represents plain text and not
// an escape sequence. We are only interested in plain
// text.
if s.Type == "" {
out = append(out, string(s.Code))
}
}
if len(out) > 0 {
return []byte(strings.Join(out, "")), errs.err()
}
return nil, errs.err()
}
91 changes: 91 additions & 0 deletions strip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ansi

import "testing"

func TestStrip(t *testing.T) {
for _, tt := range []struct {
in, out string
errors bool
}{
{},

// Make sure control characters are not stripped.
{in: "abc", out: "abc"},
{in: "abc\r\n", out: "abc\r\n"},
{in: "\t", out: "\t"},
{in: "abc\t", out: "abc\t"},
{in: "\tabc", out: "\tabc"},
{in: "abc\tabc", out: "abc\tabc"},

// Lone escape
{in: "\033", errors: true},
{in: "abc\033", out: "abc", errors: true},

// invalid escape sequence
{in: "abc\033\r\n", out: "abc\n", errors: true},
{in: "ab\033\tc", out: "abc", errors: true},
{in: "abc\033\033\n", out: "abc\n", errors: true},

// Strip simple escape sequences
{in: "\033B"},
{in: "abc\033B", out: "abc"},
{in: "\033Babc", out: "abc"},
{in: "a\033Bbc", out: "abc"},
{in: "a\033Bb\033Bc", out: "abc"},
{in: "a\033B\033Bbc", out: "abc"},

// Strip multi-byte CSI escape sequences with no parameters
{in: "\033[A"},
{in: "abc\033[A", out: "abc"},
{in: "\033[Aabc", out: "abc"},
{in: "a\033[Abc", out: "abc"},
{in: "a\033[Ab\033[Ac", out: "abc"},
{in: "a\033[A\033[Abc", out: "abc"},

// Strip single byte CSI escape sequences
{in: "\233A"},
{in: "abc\233A", out: "abc"},
{in: "\233Aabc", out: "abc"},
{in: "a\233Abc", out: "abc"},
{in: "a\233Ab\233Ac", out: "abc"},
{in: "a\233A\233Abc", out: "abc"},

// Strip CSI escape sequences with parameters
{in: "\033[4A"},
{in: "abc\033[4A", out: "abc"},
{in: "\033[4Aabc", out: "abc"},
{in: "a\033[4Abc", out: "abc"},
{in: "a\033[4Ab\033[4Ac", out: "abc"},
{in: "a\033[4A\033[4Abc", out: "abc"},
{in: "a\033[4A\033[4Abc", out: "abc"},

// Strip CSI escape sequenes with intermediate bytes
// Strip CSI escape sequence with multiple parameters
{in: "\033[1;2 Tabc", out: "abc"},

// Strip SOS escape sequences (start of string)
{in: "\033_This is an APC string\033\\abc", out: "abc"},

// SOS without ST
{in: "\033_This is an incomplete APC string", out: "", errors: true},

// too many parameters
{in: "\033[1;2Aabc", out: "abc", errors: true},

// two few parameters (" T" requires 2)
{in: "\033[ Tabc", out: "abc", errors: true},
{in: "\033[1 Tabc", out: "abc", errors: true},
} {
bout, err := Strip([]byte(tt.in))
switch {
case tt.errors && err == nil:
t.Errorf("%q: did not get expected error", tt.in)
case !tt.errors && err != nil:
t.Errorf("%q: got unexpected error %v", tt.in, err)
}
out := string(bout)
if out != tt.out {
t.Errorf("%q: got %q, want %q", tt.in, out, tt.out)
}
}
}

0 comments on commit 86f4995

Please sign in to comment.