pax_global_header00006660000000000000000000000064134251063460014516gustar00rootroot0000000000000052 comment=88cfb0c2efe8ed7b0ccf0af83db39359829027bb vtclean-1.0.0/000077500000000000000000000000001342510634600131505ustar00rootroot00000000000000vtclean-1.0.0/.travis.yml000066400000000000000000000001121342510634600152530ustar00rootroot00000000000000language: go sudo: false script: go test -v go: - 1.5 - 1.6 - 1.7 vtclean-1.0.0/LICENSE000066400000000000000000000020401342510634600141510ustar00rootroot00000000000000Copyright (c) 2015 Ryan Hileman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vtclean-1.0.0/README.md000066400000000000000000000017341342510634600144340ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/lunixbochs/vtclean.svg?branch=master)](https://travis-ci.org/lunixbochs/vtclean) vtclean ---- Clean up raw terminal output by stripping escape sequences, optionally preserving color. Get it: `go get github.com/lunixbochs/vtclean/vtclean` API: import "github.com/lunixbochs/vtclean" vtclean.Clean(line string, color bool) string Command line example: $ echo -e '\x1b[1;32mcolor example color forced to stop at end of line backspace is ba\b\bgood no beeps!\x07\x07' | ./vtclean -color color example color forced to stop at end of line backspace is good no beeps! Go example: package main import ( "fmt" "github.com/lunixbochs/vtclean" ) func main() { line := vtclean.Clean( "\033[1;32mcolor, " + "curs\033[Aor, " + "backspace\b\b\b\b\b\b\b\b\b\b\b\033[K", false) fmt.Println(line) } Output: color, cursor vtclean-1.0.0/go.mod000066400000000000000000000000451342510634600142550ustar00rootroot00000000000000module github.com/lunixbochs/vtclean vtclean-1.0.0/io.go000066400000000000000000000030131342510634600141030ustar00rootroot00000000000000package vtclean import ( "bufio" "bytes" "io" ) type reader struct { io.Reader scanner *bufio.Scanner buf []byte color bool } func NewReader(r io.Reader, color bool) io.Reader { return &reader{Reader: r, color: color} } func (r *reader) scan() bool { if r.scanner == nil { r.scanner = bufio.NewScanner(r.Reader) } if len(r.buf) > 0 { return true } if r.scanner.Scan() { r.buf = []byte(Clean(r.scanner.Text(), r.color) + "\n") return true } return false } func (r *reader) fill(p []byte) int { n := len(r.buf) copy(p, r.buf) if len(p) < len(r.buf) { r.buf = r.buf[len(p):] n = len(p) } else { r.buf = nil } return n } func (r *reader) Read(p []byte) (int, error) { n := r.fill(p) if n < len(p) { if !r.scan() { if n == 0 { return 0, io.EOF } return n, nil } n += r.fill(p[n:]) } return n, nil } type writer struct { io.Writer buf []byte color bool } func NewWriter(w io.Writer, color bool) io.WriteCloser { return &writer{Writer: w, color: color} } func (w *writer) Write(p []byte) (int, error) { buf := append(w.buf, p...) lines := bytes.Split(buf, []byte("\n")) if len(lines) > 0 { last := len(lines) - 1 w.buf = lines[last] count := 0 for _, line := range lines[:last] { n, err := w.Writer.Write([]byte(Clean(string(line), w.color) + "\n")) count += n if err != nil { return count, err } } } return len(p), nil } func (w *writer) Close() error { cl := Clean(string(w.buf), w.color) _, err := w.Writer.Write([]byte(cl)) return err } vtclean-1.0.0/line.go000066400000000000000000000034341342510634600144320ustar00rootroot00000000000000package vtclean type char struct { char byte vt100 []byte } func chars(p []byte) []char { tmp := make([]char, len(p)) for i, v := range p { tmp[i].char = v } return tmp } type lineEdit struct { buf []char pos, size int vt100 []byte } func newLineEdit(length int) *lineEdit { return &lineEdit{buf: make([]char, length)} } func (l *lineEdit) Vt100(p []byte) { l.vt100 = p } func (l *lineEdit) Move(x int) { if x < 0 && l.pos <= -x { l.pos = 0 } else if x > 0 && l.pos+x > l.size { l.pos = l.size } else { l.pos += x } } func (l *lineEdit) MoveAbs(x int) { if x < l.size { l.pos = x } } func (l *lineEdit) Write(p []byte) { c := chars(p) if len(c) > 0 { c[0].vt100 = l.vt100 l.vt100 = nil } if len(l.buf)-l.pos < len(c) { l.buf = append(l.buf[:l.pos], c...) } else { copy(l.buf[l.pos:], c) } l.pos += len(c) if l.pos > l.size { l.size = l.pos } } func (l *lineEdit) Insert(p []byte) { c := chars(p) if len(c) > 0 { c[0].vt100 = l.vt100 l.vt100 = nil } l.size += len(c) c = append(c, l.buf[l.pos:]...) l.buf = append(l.buf[:l.pos], c...) } func (l *lineEdit) Delete(n int) { most := l.size - l.pos if n > most { n = most } copy(l.buf[l.pos:], l.buf[l.pos+n:]) l.size -= n } func (l *lineEdit) Clear() { for i := 0; i < len(l.buf); i++ { l.buf[i].char = ' ' } } func (l *lineEdit) ClearLeft() { for i := 0; i < l.pos+1; i++ { l.buf[i].char = ' ' } } func (l *lineEdit) ClearRight() { l.size = l.pos } func (l *lineEdit) Bytes() []byte { length := 0 buf := l.buf[:l.size] for _, v := range buf { length += 1 + len(v.vt100) } tmp := make([]byte, 0, length) for _, v := range buf { tmp = append(tmp, v.vt100...) tmp = append(tmp, v.char) } return tmp } func (l *lineEdit) String() string { return string(l.Bytes()) } vtclean-1.0.0/vtclean.go000066400000000000000000000037341342510634600151420ustar00rootroot00000000000000package vtclean import ( "bytes" "regexp" "strconv" ) // regex based on ECMA-48: // 1. optional: // one of [ or ] // any amount of 0x30-0x3f // any amount of 0x20-0x2f // 3. exactly one 0x40-0x7e var vt100re = regexp.MustCompile(`^\033([\[\]]([0-9:;<=>\?]*)([!"#$%&'()*+,\-./]*))?([@A-Z\[\]^_\x60a-z{|}~])`) var vt100exc = regexp.MustCompile(`^\033(\[[^a-zA-Z0-9@\?]+|[\(\)]).`) // this is to handle the RGB escape generated by `tput initc 1 500 500 500` var vt100long = regexp.MustCompile(`^\033](\d+);([^\033]+)\033\\`) func Clean(line string, color bool) string { var edit = newLineEdit(len(line)) lineb := []byte(line) hadColor := false for i := 0; i < len(lineb); { c := lineb[i] switch c { case '\r': edit.MoveAbs(0) case '\b': edit.Move(-1) case '\033': // set terminal title if bytes.HasPrefix(lineb[i:], []byte("\x1b]0;")) { pos := bytes.Index(lineb[i:], []byte("\a")) if pos != -1 { i += pos + 1 continue } } if m := vt100long.Find(lineb[i:]); m != nil { i += len(m) } else if m := vt100exc.Find(lineb[i:]); m != nil { i += len(m) } else if m := vt100re.FindSubmatch(lineb[i:]); m != nil { i += len(m[0]) num := string(m[2]) n, err := strconv.Atoi(num) if err != nil || n > 10000 { n = 1 } switch m[4][0] { case 'm': if color { hadColor = true edit.Vt100(m[0]) } case '@': edit.Insert(bytes.Repeat([]byte{' '}, n)) case 'G': edit.MoveAbs(n) case 'C': edit.Move(n) case 'D': edit.Move(-n) case 'P': edit.Delete(n) case 'K': switch num { case "", "0": edit.ClearRight() case "1": edit.ClearLeft() case "2": edit.Clear() } } } else { i += 1 } continue default: if c == '\n' || c == '\t' || c >= ' ' { edit.Write([]byte{c}) } } i += 1 } out := edit.Bytes() if hadColor { out = append(out, []byte("\033[0m")...) } return string(out) } vtclean-1.0.0/vtclean/000077500000000000000000000000001342510634600146045ustar00rootroot00000000000000vtclean-1.0.0/vtclean/vtclean.go000066400000000000000000000004031342510634600165640ustar00rootroot00000000000000package main import ( "flag" "github.com/lunixbochs/vtclean" "io" "os" ) func main() { color := flag.Bool("color", false, "enable color") flag.Parse() stdout := vtclean.NewWriter(os.Stdout, *color) defer stdout.Close() io.Copy(stdout, os.Stdin) } vtclean-1.0.0/vtclean_test.go000066400000000000000000000033161342510634600161750ustar00rootroot00000000000000package vtclean import ( "testing" ) var tests = map[string]string{ // "set title" special case "\x1b]0;asdjklfasdkljf\atest": "test", "hi man\x1b[3Gdude": "hi dude", // basic escape "\033[12laaa": "aaa", "\033[?1049laaa": "aaa", // for the second regex "a\033[!pa": "aa", // backspace and clear "aaa\b\bb": "aba", "aaa\b\b\033[K": "a", "aaa\b\b\033[1K": " a", "aaa\b\b\033[2Ka": " a ", // character movement "aaa\033[2Db": "aba", "aaa\033[4D\033[2Cb": "aab", "aaa\033[4D\033[1Cb": "aba", "aaa\033[1Cb": "aaab", // vt52 "aaa\033D\033Db": "aba", "a\033@b": "ab", // delete and insert "aaa\b\b\033[2@": "a aa", "aaa\b\b\033[P": "aa", "aaa\b\b\033[4P": "a", // strip color "aaa \033[25;25mtest": "aaa test", "bbb \033]4;1;rgb:38/54/71\033\\test": "bbb test", "ccc \033]4;1;rgb:38/54/71test": "ccc gb:38/54/71test", // tabs "aa\tbb": "aa\tbb", // carriage return "aaa\rb": "baa", } var colorTests = map[string]string{ "aaa \033[25;25mtest": "aaa \033[25;25mtest\x1b[0m", "\033[0;m aa": "\033[0;m aa\033[0m", "\033[32;1m$ echo foobar\033[0;m": "\033[32;1m$ echo foobar\033[0m", } func TestMain(t *testing.T) { for a, b := range tests { tmp := Clean(a, false) if tmp != b { t.Logf("Clean() failed: %#v -> %#v != %#v\n", a, tmp, b) t.Fail() } } } func TestColor(t *testing.T) { for a, b := range colorTests { tmp := Clean(a, true) if tmp != b { t.Logf("Clean() failed: %#v -> %#v != %#v\n", a, tmp, b) t.Fail() } } } func TestWriteBounds(t *testing.T) { l := &lineEdit{buf: nil} s := "asdf" l.Write([]byte(s)) if l.String() != s { t.Fatalf("l.String(): %#v != %#v", l.String(), s) } }