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