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