File: erase.go 1 /* 2 The MIT License (MIT) 3 4 Copyright (c) 2026 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 To compile a smaller-sized command-line app, you can use the `go` command as 27 follows: 28 29 go build -ldflags "-s -w" -trimpath erase.go 30 */ 31 32 package main 33 34 import ( 35 "bufio" 36 "bytes" 37 "io" 38 "os" 39 "regexp" 40 ) 41 42 const info = ` 43 erase [options...] [regexes...] 44 45 46 Ignore/remove all occurrences of all regex matches along lines read from the 47 standard input. The regular-expression mode used is "re2", which is a superset 48 of the commonly-used "extended-mode". 49 50 All ANSI-style sequences are removed before trying to match-remove things, to 51 avoid messing those up. Each regex erases all its occurrences on the current 52 line in the order given among the arguments, so regex-order matters. 53 54 The options are, available both in single and double-dash versions 55 56 -h, -help show this help message 57 -i, -ins match regexes case-insensitively 58 ` 59 60 func main() { 61 args := os.Args[1:] 62 buffered := false 63 insensitive := false 64 65 for len(args) > 0 { 66 switch args[0] { 67 case `-b`, `--b`, `-buffered`, `--buffered`: 68 buffered = true 69 args = args[1:] 70 continue 71 72 case `-h`, `--h`, `-help`, `--help`: 73 os.Stdout.WriteString(info[1:]) 74 return 75 76 case `-i`, `--i`, `-ins`, `--ins`: 77 insensitive = true 78 args = args[1:] 79 continue 80 } 81 82 break 83 } 84 85 if len(args) > 0 && args[0] == `--` { 86 args = args[1:] 87 } 88 89 exprs := make([]*regexp.Regexp, 0, len(args)) 90 91 for _, s := range args { 92 var err error 93 var exp *regexp.Regexp 94 95 if insensitive { 96 exp, err = regexp.Compile(`(?i)` + s) 97 } else { 98 exp, err = regexp.Compile(s) 99 } 100 101 if err != nil { 102 os.Stderr.WriteString(err.Error()) 103 os.Stderr.WriteString("\n") 104 continue 105 } 106 107 exprs = append(exprs, exp) 108 } 109 110 // quit right away when given invalid regexes 111 if len(exprs) < len(args) { 112 os.Exit(1) 113 return 114 } 115 116 liveLines := !buffered 117 if !buffered { 118 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil { 119 liveLines = false 120 } 121 } 122 123 err := run(os.Stdout, os.Stdin, exprs, liveLines) 124 if err != nil && err != io.EOF { 125 os.Stderr.WriteString(err.Error()) 126 os.Stderr.WriteString("\n") 127 os.Exit(1) 128 return 129 } 130 } 131 132 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error { 133 var buf []byte 134 sc := bufio.NewScanner(r) 135 sc.Buffer(nil, 8*1024*1024*1024) 136 bw := bufio.NewWriter(w) 137 defer bw.Flush() 138 139 src := make([]byte, 8*1024) 140 dst := make([]byte, 8*1024) 141 142 for i := 0; sc.Scan(); i++ { 143 line := sc.Bytes() 144 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) { 145 line = line[3:] 146 } 147 148 s := line 149 if bytes.IndexByte(s, '\x1b') >= 0 { 150 buf = plain(buf[:0], s) 151 s = buf 152 } 153 154 if len(exprs) > 0 { 155 src = append(src[:0], s...) 156 for _, exp := range exprs { 157 dst = erase(dst[:0], src, exp) 158 src = append(src[:0], dst...) 159 } 160 bw.Write(dst) 161 } else { 162 bw.Write(s) 163 } 164 165 if bw.WriteByte('\n') != nil { 166 return io.EOF 167 } 168 169 if !live { 170 continue 171 } 172 173 if bw.Flush() != nil { 174 return io.EOF 175 } 176 } 177 178 return sc.Err() 179 } 180 181 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte { 182 for len(src) > 0 { 183 span := with.FindIndex(src) 184 // also ignore empty regex matches to avoid infinite outer loops, 185 // as skipping empty slices isn't advancing at all, leaving the 186 // string stuck to being empty-matched forever by the same regex 187 if len(span) != 2 || span[0] == span[1] || span[0] < 0 { 188 return append(dst, src...) 189 } 190 191 start, end := span[0], span[1] 192 dst = append(dst, src[:start]...) 193 // avoid infinite loops caused by empty regex matches 194 if start == end && end < len(src) { 195 dst = append(dst, src[end]) 196 end++ 197 } 198 src = src[end:] 199 } 200 201 return dst 202 } 203 204 func plain(dst []byte, src []byte) []byte { 205 for len(src) > 0 { 206 i, j := indexEscapeSequence(src) 207 if i < 0 { 208 dst = append(dst, src...) 209 break 210 } 211 if j < 0 { 212 j = len(src) 213 } 214 215 if i > 0 { 216 dst = append(dst, src[:i]...) 217 } 218 219 src = src[j:] 220 } 221 222 return dst 223 } 224 225 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is 226 // the multi-byte sequences starting with ESC[; the result is a pair of slice 227 // indices which can be independently negative when either the start/end of 228 // a sequence isn't found; given their fairly-common use, even the hyperlink 229 // ESC]8 sequences are supported 230 func indexEscapeSequence(s []byte) (int, int) { 231 var prev byte 232 233 for i, b := range s { 234 if prev == '\x1b' && b == '[' { 235 j := indexLetter(s[i+1:]) 236 if j < 0 { 237 return i, -1 238 } 239 return i - 1, i + 1 + j + 1 240 } 241 242 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' { 243 j := indexPair(s[i+1:], '\x1b', '\\') 244 if j < 0 { 245 return i, -1 246 } 247 return i - 1, i + 1 + j + 2 248 } 249 250 prev = b 251 } 252 253 return -1, -1 254 } 255 256 func indexLetter(s []byte) int { 257 for i, b := range s { 258 upper := b &^ 32 259 if 'A' <= upper && upper <= 'Z' { 260 return i 261 } 262 } 263 264 return -1 265 } 266 267 func indexPair(s []byte, x byte, y byte) int { 268 var prev byte 269 270 for i, b := range s { 271 if prev == x && b == y && i > 0 { 272 return i 273 } 274 prev = b 275 } 276 277 return -1 278 }