File: ihima.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-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 Single-file source-code for ihima.
  27 
  28 To compile a smaller-sized command-line app, you can use the `go` command as
  29 follows:
  30 
  31 go build -ldflags "-s -w" -trimpath ihima.go
  32 */
  33 
  34 package main
  35 
  36 import (
  37     "bufio"
  38     "bytes"
  39     "os"
  40     "regexp"
  41 )
  42 
  43 func main() {
  44     nerr := 0
  45     n := len(os.Args) - 1
  46     exprs := make([]*regexp.Regexp, 0, n)
  47     for _, s := range os.Args[1:] {
  48         e, err := regexp.Compile(`(?i)` + s)
  49         if err != nil {
  50             os.Stderr.WriteString("\x1b[31m")
  51             os.Stderr.WriteString(err.Error())
  52             os.Stderr.WriteString("\x1b[0m\n")
  53             nerr++
  54         }
  55         exprs = append(exprs, e)
  56     }
  57 
  58     if nerr > 0 {
  59         os.Exit(1)
  60     }
  61 
  62     sc := bufio.NewScanner(os.Stdin)
  63     sc.Buffer(nil, 8*1024*1024*1024)
  64     bw := bufio.NewWriter(os.Stdout)
  65 
  66     for sc.Scan() {
  67         handleLine(bw, sc.Bytes(), exprs)
  68         bw.WriteByte('\n')
  69         if err := bw.Flush(); err != nil {
  70             return
  71         }
  72     }
  73 }
  74 
  75 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
  76 // either the alert/bell byte, or the multi-byte sequences starting either
  77 // with ESC[ or ESC]; either returned index can be negative
  78 func indexEscapeSequence(s []byte) (int, int) {
  79     var prev byte
  80 
  81     for i, b := range s {
  82         if b == '\a' {
  83             return i, i + 1
  84         }
  85 
  86         if prev == '\x1b' && b == '[' {
  87             j := indexLetter(s[i+1:])
  88             if j < 0 {
  89                 return i, -1
  90             }
  91             return i - 1, i + 1 + j + 1
  92         }
  93 
  94         if prev == '\x1b' && b == ']' {
  95             j := bytes.IndexByte(s[i+1:], ':')
  96             if j < 0 {
  97                 return i, -1
  98             }
  99             return i - 1, i + 1 + j + 1
 100         }
 101 
 102         if prev == '\x1b' && b == '\\' {
 103             return i - 1, i + 1
 104         }
 105 
 106         prev = b
 107     }
 108 
 109     return -1, -1
 110 }
 111 
 112 func indexLetter(s []byte) int {
 113     for i, b := range s {
 114         if 'A' <= b && b <= 'Z' {
 115             return i
 116         }
 117         if 'a' <= b && b <= 'z' {
 118             return i
 119         }
 120     }
 121 
 122     return -1
 123 }
 124 
 125 func handleLine(w *bufio.Writer, s []byte, with []*regexp.Regexp) {
 126     for len(s) > 0 {
 127         i, j := indexEscapeSequence(s)
 128         if i < 0 {
 129             handleLineChunk(w, s, with)
 130             return
 131         }
 132 
 133         handleLineChunk(w, s[:i], with)
 134         w.Write(s[i:j])
 135 
 136         if j < 0 {
 137             break
 138         }
 139         s = s[j:]
 140     }
 141 }
 142 
 143 func handleLineChunk(w *bufio.Writer, s []byte, with []*regexp.Regexp) {
 144     start := -1
 145     end := -1
 146 
 147     for len(s) > 0 {
 148         start = -1
 149         for _, e := range with {
 150             span := e.FindIndex(s)
 151             if span != nil && (span[0] < start || start < 0) {
 152                 start = span[0]
 153                 end = span[1]
 154             }
 155         }
 156 
 157         if start < 0 {
 158             w.Write(s)
 159             return
 160         }
 161 
 162         w.Write(s[:start])
 163         w.WriteString("\x1b[7m")
 164         w.Write(s[start:end])
 165         w.WriteString("\x1b[0m")
 166         s = s[end:]
 167     }
 168 }