File: ecoli.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 ecoli.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 ecoli [options...] [regex/style pairs...] 44 45 46 Expressions COloring LInes tries to match each line read from the standard 47 input to the regexes given, coloring/styling with the named-style paired 48 to the first matching regex, if any. Lines not matching any regex stay the 49 same. 50 51 The options are, available both in single and double-dash versions 52 53 -h, -help show this help message 54 -i, -ins, -insensitive match the regexes given case-insensitively 55 56 Some of the colors/styles available are: 57 58 blue blueback 59 bold 60 gray grayback 61 green greenback 62 inverse 63 magenta magentaback 64 orange orangeback 65 purple purpleback 66 red redback 67 underline 68 69 Some style aliases are: 70 71 b blue bb blueback 72 g green gb greenback 73 m magenta mb magentaback 74 o orange ob orangeback 75 p purple pb purpleback 76 r red rb redback 77 i inverse (highlight) 78 u underline 79 ` 80 81 var styleAliases = map[string]string{ 82 `b`: `blue`, 83 `g`: `green`, 84 `m`: `magenta`, 85 `o`: `orange`, 86 `p`: `purple`, 87 `r`: `red`, 88 `u`: `underline`, 89 90 `bb`: `blueback`, 91 `bg`: `greenback`, 92 `bm`: `magentaback`, 93 `bo`: `orangeback`, 94 `bp`: `purpleback`, 95 `br`: `redback`, 96 97 `gb`: `greenback`, 98 `mb`: `magentaback`, 99 `ob`: `orangeback`, 100 `pb`: `purpleback`, 101 `rb`: `redback`, 102 103 `hi`: `inverse`, 104 `inv`: `inverse`, 105 `mag`: `magenta`, 106 107 `du`: `doubleunderline`, 108 109 `flip`: `inverse`, 110 `swap`: `inverse`, 111 112 `reset`: `plain`, 113 `highlight`: `inverse`, 114 `hilite`: `inverse`, 115 `invert`: `inverse`, 116 `inverted`: `inverse`, 117 `swapped`: `inverse`, 118 119 `dunderline`: `doubleunderline`, 120 `dunderlined`: `doubleunderline`, 121 122 `strikethrough`: `strike`, 123 `strikethru`: `strike`, 124 `struck`: `strike`, 125 126 `underlined`: `underline`, 127 128 `bblue`: `blueback`, 129 `bgray`: `grayback`, 130 `bgreen`: `greenback`, 131 `bmagenta`: `magentaback`, 132 `borange`: `orangeback`, 133 `bpurple`: `purpleback`, 134 `bred`: `redback`, 135 136 `bgblue`: `blueback`, 137 `bggray`: `grayback`, 138 `bggreen`: `greenback`, 139 `bgmag`: `magentaback`, 140 `bgmagenta`: `magentaback`, 141 `bgorange`: `orangeback`, 142 `bgpurple`: `purpleback`, 143 `bgred`: `redback`, 144 145 `bluebg`: `blueback`, 146 `graybg`: `grayback`, 147 `greenbg`: `greenback`, 148 `magbg`: `magentaback`, 149 `magentabg`: `magentaback`, 150 `orangebg`: `orangeback`, 151 `purplebg`: `purpleback`, 152 `redbg`: `redback`, 153 154 `backblue`: `blueback`, 155 `backgray`: `grayback`, 156 `backgreen`: `greenback`, 157 `backmag`: `magentaback`, 158 `backmagenta`: `magentaback`, 159 `backorange`: `orangeback`, 160 `backpurple`: `purpleback`, 161 `backred`: `redback`, 162 } 163 164 var styles = map[string]string{ 165 `blue`: "\x1b[38;2;0;95;215m", 166 `bold`: "\x1b[1m", 167 `doubleunderline`: "\x1b[21m", 168 `gray`: "\x1b[38;2;168;168;168m", 169 `green`: "\x1b[38;2;0;135;95m", 170 `inverse`: "\x1b[7m", 171 `magenta`: "\x1b[38;2;215;0;255m", 172 `orange`: "\x1b[38;2;215;95;0m", 173 `plain`: "\x1b[0m", 174 `purple`: "\x1b[38;2;135;95;255m", 175 `red`: "\x1b[38;2;204;0;0m", 176 `strike`: "\x1b[9m", 177 `underline`: "\x1b[4m", 178 179 `blueback`: "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m", 180 `grayback`: "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m", 181 `greenback`: "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m", 182 `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m", 183 `orangeback`: "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m", 184 `purpleback`: "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m", 185 `redback`: "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m", 186 } 187 188 // pair has a regular-expression and its associated ANSI-code style together 189 type pair struct { 190 expr *regexp.Regexp 191 style string 192 } 193 194 func main() { 195 buffered := false 196 insensitive := false 197 args := os.Args[1:] 198 199 for len(args) > 0 { 200 switch args[0] { 201 case `-b`, `--b`, `-buffered`, `--buffered`: 202 buffered = true 203 args = args[1:] 204 continue 205 206 case `-h`, `--h`, `-help`, `--help`: 207 os.Stdout.WriteString(info[1:]) 208 return 209 210 case `-i`, `--i`, `-ins`, `--ins`: 211 insensitive = true 212 args = args[1:] 213 continue 214 } 215 216 break 217 } 218 219 if len(args) > 0 && args[0] == `--` { 220 args = args[1:] 221 } 222 223 if len(args)%2 != 0 { 224 const msg = "you forgot the style-name for/after the last regex\n" 225 os.Stderr.WriteString(msg) 226 os.Exit(1) 227 } 228 229 nerr := 0 230 pairs := make([]pair, 0, len(args)/2) 231 232 for len(args) >= 2 { 233 src := args[0] 234 sname := args[1] 235 236 var err error 237 var exp *regexp.Regexp 238 if insensitive { 239 exp, err = regexp.Compile(`(?i)` + src) 240 } else { 241 exp, err = regexp.Compile(src) 242 } 243 if err != nil { 244 os.Stderr.WriteString(err.Error()) 245 os.Stderr.WriteString("\n") 246 nerr++ 247 } 248 249 if alias, ok := styleAliases[sname]; ok { 250 sname = alias 251 } 252 253 style, ok := styles[sname] 254 if !ok { 255 os.Stderr.WriteString("no style named `") 256 os.Stderr.WriteString(args[1]) 257 os.Stderr.WriteString("`\n") 258 nerr++ 259 } 260 261 pairs = append(pairs, pair{expr: exp, style: style}) 262 args = args[2:] 263 } 264 265 if nerr > 0 { 266 os.Exit(1) 267 } 268 269 liveLines := !buffered 270 if !buffered { 271 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil { 272 liveLines = false 273 } 274 } 275 276 sc := bufio.NewScanner(os.Stdin) 277 sc.Buffer(nil, 8*1024*1024*1024) 278 bw := bufio.NewWriter(os.Stdout) 279 var plain []byte 280 281 for i := 0; sc.Scan(); i++ { 282 s := sc.Bytes() 283 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) { 284 s = s[3:] 285 } 286 plain = appendPlain(plain[:0], s) 287 288 if err := handleLine(bw, s, noANSI(plain), pairs); err != nil { 289 return 290 } 291 292 if !liveLines { 293 continue 294 } 295 296 if err := bw.Flush(); err != nil { 297 return 298 } 299 } 300 } 301 302 // appendPlain extends the slice given using the non-ANSI parts of a string 303 func appendPlain(dst []byte, src []byte) []byte { 304 for len(src) > 0 { 305 i, j := indexEscapeSequence(src) 306 if i < 0 { 307 dst = append(dst, src...) 308 break 309 } 310 if j < 0 { 311 j = len(src) 312 } 313 314 if i > 0 { 315 dst = append(dst, src[:i]...) 316 } 317 318 src = src[j:] 319 } 320 321 return dst 322 } 323 324 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is 325 // the multi-byte sequences starting with ESC[; the result is a pair of slice 326 // indices which can be independently negative when either the start/end of 327 // a sequence isn't found; given their fairly-common use, even the hyperlink 328 // ESC]8 sequences are supported 329 func indexEscapeSequence(s []byte) (int, int) { 330 var prev byte 331 332 for i, b := range s { 333 if prev == '\x1b' && b == '[' { 334 j := indexLetter(s[i+1:]) 335 if j < 0 { 336 return i, -1 337 } 338 return i - 1, i + 1 + j + 1 339 } 340 341 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' { 342 j := indexPair(s[i+1:], '\x1b', '\\') 343 if j < 0 { 344 return i, -1 345 } 346 return i - 1, i + 1 + j + 2 347 } 348 349 prev = b 350 } 351 352 return -1, -1 353 } 354 355 func indexLetter(s []byte) int { 356 for i, b := range s { 357 upper := b &^ 32 358 if 'A' <= upper && upper <= 'Z' { 359 return i 360 } 361 } 362 363 return -1 364 } 365 366 func indexPair(s []byte, x byte, y byte) int { 367 var prev byte 368 369 for i, b := range s { 370 if prev == x && b == y && i > 0 { 371 return i 372 } 373 prev = b 374 } 375 376 return -1 377 } 378 379 // noANSI ensures arguments to func handleLine are given in the right order 380 type noANSI []byte 381 382 // handleLine styles the current line given to it using the first matching 383 // regex, keeping it as given if none of the regexes match; it's given 2 384 // strings: the first is the original line, the latter is its plain-text 385 // version (with no ANSI codes) and is used for the regex-matching, since 386 // ANSI codes use a mix of numbers and letters, which can themselves match 387 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error { 388 for _, p := range pairs { 389 if p.expr.Match(plain) { 390 w.WriteString(p.style) 391 w.Write(s) 392 w.WriteString("\x1b[0m") 393 return w.WriteByte('\n') 394 } 395 } 396 397 w.Write(s) 398 return w.WriteByte('\n') 399 }