File: ./bytes.go
   1 package main
   2 
   3 import (
   4     "io"
   5     "unicode/utf8"
   6 )
   7 
   8 // advance tries to advance up to the number of runes given
   9 func advance(data []byte, i int, n int) int {
  10     offset := 0
  11     data = data[i:]
  12     for n > 0 && len(data) > 0 {
  13         _, size := utf8.DecodeRune(data)
  14         data = data[size:]
  15         offset += size
  16         n--
  17     }
  18     return i + offset
  19 }
  20 
  21 // retreat tries to retreat up to the number of runes given
  22 func retreat(data []byte, i int, n int) int {
  23     offset := 0
  24     data = data[:i]
  25     for n > 0 && len(data) > 0 {
  26         _, size := utf8.DecodeLastRune(data)
  27         data = data[:len(data)-size]
  28         offset += size
  29         n--
  30     }
  31     return i - offset
  32 }
  33 
  34 // slurpFlat reads all input, turning end-of-line markers into spaces
  35 func slurpFlat(r io.Reader) ([]byte, error) {
  36     const chunksize = 128 * 1024
  37     res := make([]byte, 0, chunksize)
  38     var chunk [chunksize]byte
  39 
  40     for {
  41         n, err := r.Read(chunk[:])
  42 
  43         for _, b := range chunk[:n] {
  44             switch b {
  45             case '\r':
  46                 // always ignore carriage returns
  47             case '\n':
  48                 // ignore leading line-feeds, turning later ones into spaces
  49                 if len(res) > 0 {
  50                     res = append(res, ' ')
  51                 }
  52             default:
  53                 // keep all other bytes the same
  54                 res = append(res, b)
  55             }
  56         }
  57 
  58         if err == io.EOF && n < 1 {
  59             return res, nil
  60         }
  61         if err != nil {
  62             return res, err
  63         }
  64     }
  65 }

     File: ./config.go
   1 package main
   2 
   3 import (
   4     "flag"
   5     "fmt"
   6     "os"
   7     "regexp"
   8     "strings"
   9 )
  10 
  11 // config is the result of parsing the cmd-line args given to the app
  12 type config struct {
  13     // Expression is the regular expression to match
  14     Expression string
  15 
  16     // Filenames is the list of (optional) filenames given
  17     Filenames []string
  18 
  19     // ShowPositions determines whether to show byte-offsets or line numbers
  20     ShowPositions bool
  21 
  22     // Flatten determines whether to flatten the input(s), by turning every
  23     // line-feed into a single space
  24     Flatten bool
  25 }
  26 
  27 const (
  28     caseInsUsage = "enable case-insensitive matching"
  29     showPosUsage = "show byte-offsets or line numbers"
  30 )
  31 
  32 // parseFlags is the constructor for type config
  33 func parseFlags(usage string) config {
  34     flag.Usage = func() {
  35         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  36         flag.PrintDefaults()
  37     }
  38 
  39     var cfg config
  40     caseIns := false
  41     flag.BoolVar(&caseIns, "i", caseIns, caseInsUsage)
  42     flag.BoolVar(&cfg.ShowPositions, "n", cfg.ShowPositions, showPosUsage)
  43     flag.Parse()
  44 
  45     args := flag.Args()
  46     if len(args) == 0 {
  47         flag.Usage()
  48         os.Exit(0)
  49     }
  50 
  51     cfg.Expression = args[0]
  52     if caseIns && !strings.HasPrefix(cfg.Expression, "(?i)") {
  53         cfg.Expression = "(?i)" + cfg.Expression
  54     }
  55     cfg.Filenames = args[1:]
  56     cfg.Flatten = true &&
  57         !strings.HasPrefix(cfg.Expression, "^") &&
  58         !strings.HasSuffix(cfg.Expression, "$") &&
  59         !strings.HasPrefix(cfg.Expression, "(?i)^")
  60     return cfg
  61 }
  62 
  63 // matchConfig groups all parameters the matching funcs need
  64 type matchConfig struct {
  65     // Regex is the pattern-finder, obviously
  66     Regex *regexp.Regexp
  67 
  68     // Name is the (optional) filename to show when matching multiple files
  69     Name string
  70 
  71     // ShowPositions determines whether to show byte-offsets or line numbers
  72     ShowPositions bool
  73 
  74     // Flatten determines whether to flatten the input(s), by turning every
  75     // line-feed into a single space
  76     Flatten bool
  77 }

     File: ./go.mod
   1 module frep
   2 
   3 go 1.18

     File: ./info.txt
   1 frep [options...] regex [files...]
   2 
   3 Flat Regular Expression Print(er) is an app similar to `grep`, except it
   4 normally ignores line-feeds and/or carriage returns and follows the RE2
   5 syntax, described at https://github.com/google/re2/wiki/Syntax
   6 
   7 The exception to that is when the regular expression given starts/ends with
   8 explicit line-delimiters, namely `^` and `$`: in that case the app behaves
   9 like a regular `grep` clone, except for the RE2 syntax.

     File: ./main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "errors"
   6     "fmt"
   7     "io"
   8     "os"
   9     "regexp"
  10 
  11     _ "embed"
  12 )
  13 
  14 //go:embed info.txt
  15 var info string
  16 
  17 // errDoneWriting is a dummy error to handle quitting the app early
  18 var errDoneWriting = errors.New("no more output")
  19 
  20 // maxbufsize is the max capacity line-scanners are allowed to reach
  21 const maxbufsize = 8 * 1024 * 1024 * 1024
  22 
  23 func main() {
  24     errors := run()
  25     if errors > 0 {
  26         os.Exit(1)
  27     }
  28 }
  29 
  30 // run returns the number of errors
  31 func run() int {
  32     cfg := parseFlags(info)
  33     re, err := regexp.Compile(cfg.Expression)
  34     if err != nil {
  35         showError(err)
  36         return 1
  37     }
  38 
  39     errors := 0
  40     for _, path := range cfg.Filenames {
  41         params := matchConfig{
  42             Regex:         re,
  43             Name:          path,
  44             ShowPositions: cfg.ShowPositions,
  45             Flatten:       cfg.Flatten,
  46         }
  47 
  48         err := handleFile(os.Stdout, path, params)
  49         if err == errDoneWriting {
  50             return errors
  51         }
  52         if err != nil {
  53             showError(err)
  54             errors++
  55         }
  56     }
  57 
  58     if len(cfg.Filenames) == 0 {
  59         params := matchConfig{
  60             Regex:         re,
  61             ShowPositions: cfg.ShowPositions,
  62             Flatten:       cfg.Flatten,
  63         }
  64 
  65         err := handleReader(os.Stdout, os.Stdin, params)
  66         if err == errDoneWriting {
  67             return errors
  68         }
  69         if err != nil {
  70             showError(err)
  71             errors++
  72         }
  73     }
  74 
  75     return errors
  76 }
  77 
  78 // showError ensures all errors are shown consistently in this app
  79 func showError(err error) {
  80     fmt.Fprintf(os.Stderr, "\x1b[31m%s\x1b[0m\n", err.Error())
  81 }
  82 
  83 // handleFile simplifies func main by closing files via defer
  84 func handleFile(w io.Writer, path string, cfg matchConfig) error {
  85     f, err := os.Open(path)
  86     if err != nil {
  87         return err
  88     }
  89     defer f.Close()
  90     return handleReader(w, f, cfg)
  91 }
  92 
  93 // handleReader handles input from generic data sources
  94 func handleReader(w io.Writer, r io.Reader, cfg matchConfig) error {
  95     if cfg.Flatten {
  96         return handleFlattened(w, r, cfg)
  97     }
  98 
  99     re := cfg.Regex
 100     sc := bufio.NewScanner(r)
 101     sc.Buffer(nil, maxbufsize)
 102 
 103     for linenum := 1; sc.Scan(); linenum++ {
 104         line := sc.Bytes()
 105         if re.Match(line) {
 106             if len(line) > 0 && line[len(line)-1] == '\r' {
 107                 line = line[:len(line)-1]
 108             }
 109 
 110             // w.Write(line)
 111             m := re.FindIndex(line)
 112             if m != nil {
 113                 a := 0
 114                 b := len(line)
 115                 start, stop := m[0], m[1]
 116 
 117                 if len(cfg.Name) > 0 {
 118                     fmt.Fprintf(w, "\x1b[35m%s\x1b[0m ", cfg.Name)
 119                 }
 120                 if cfg.ShowPositions {
 121                     fmt.Fprintf(w, "\x1b[34m%06d\x1b[0m ", linenum)
 122                 }
 123 
 124                 fmt.Fprintf(w, "%s\x1b[42m\x1b[97m", line[a:start])
 125                 w.Write(line[start:stop])
 126                 fmt.Fprintf(w, "\x1b[0m%s", line[stop:b])
 127             } else {
 128                 w.Write(line)
 129             }
 130 
 131             _, err := w.Write([]byte{'\n'})
 132             // quit early by reporting a dummy error, since the error is
 133             // probably due to a closed output pipe
 134             if err != nil {
 135                 return errDoneWriting
 136             }
 137         }
 138     }
 139     return sc.Err()
 140 }
 141 
 142 // handleFlattened simplifies the control-flow of func handleReader, by handling
 143 // flat-reading of input data, where line-feeds become spaces
 144 func handleFlattened(w io.Writer, r io.Reader, cfg matchConfig) error {
 145     // padding is the max number of runes allowed to show both before and after
 146     // the matched bytes, which means it adds double its value of runes to each
 147     // line showing matches
 148     const padding = 20
 149     data, err := slurpFlat(r)
 150     if err != nil {
 151         return err
 152     }
 153 
 154     re := cfg.Regex
 155 
 156     for offset := 0; len(data) > 0; {
 157         m := re.FindIndex(data)
 158         if m != nil {
 159             start, stop := m[0], m[1]
 160 
 161             if len(cfg.Name) > 0 {
 162                 fmt.Fprintf(w, "\x1b[35m%s\x1b[0m ", cfg.Name)
 163             }
 164             if cfg.ShowPositions {
 165                 a := offset + start
 166                 b := offset + stop
 167                 fmt.Fprintf(w, "\x1b[34m[%09d:%09d]\x1b[0m ", a, b)
 168             }
 169 
 170             a := retreat(data, start, padding)
 171             b := advance(data, stop, padding)
 172             fmt.Fprintf(w, "%s\x1b[42m\x1b[97m", data[a:start])
 173             w.Write(data[start:stop])
 174             fmt.Fprintf(w, "\x1b[0m%s", data[stop:b])
 175 
 176             _, err := w.Write([]byte{'\n'})
 177             // quit early by reporting a dummy error, since the error is
 178             // probably due to a closed output pipe
 179             if err != nil {
 180                 return errDoneWriting
 181             }
 182 
 183             data = data[stop:]
 184             offset += stop
 185             continue
 186         }
 187 
 188         // no further matches
 189         return nil
 190     }
 191 
 192     return nil
 193 }