File: ./info.txt
   1 dog [options...] [paths/URIs...]
   2 
   3 
   4 Dog fetches data from the named sources given to it, whether these are
   5 filenames or URIs. Single dashes stand for standard input, and can be
   6 used more than once. When no names are given, stdin is read by default.
   7 
   8 A line-mode is available via leading option `-l`, or its aliases `--l`,
   9 `-lines`, and `--lines`. This mode turns all CRLF byte-pairs into single
  10 LF bytes, and ensures all non-empty inputs end with a final LF byte,
  11 which avoids accidentally joining lines across different inputs.
  12 
  13 Line-mode also ignores leading UTF-8 BOMs on each input's first line.

     File: ./main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "errors"
   7     "io"
   8     "net/http"
   9     "os"
  10     "strings"
  11 
  12     _ "embed"
  13 )
  14 
  15 //go:embed info.txt
  16 var info string
  17 
  18 // errNoMoreOutput is a dummy error, which is meant to be ignored; it's just
  19 // an excuse to quit the app early, instead of wasting time emitting output
  20 // which presumably will be ignored anyway
  21 var errNoMoreOutput = errors.New(`no more output`)
  22 
  23 func main() {
  24     lines := false
  25     args := os.Args[1:]
  26 
  27     if len(args) > 0 {
  28         switch os.Args[1] {
  29         case `-h`, `-help`, `--h`, `--help`:
  30             // handle the help option(s), quitting the app right after
  31             // showing a help/info message, describing the app
  32             os.Stderr.WriteString(info)
  33             return
  34 
  35         case `-l`, `-lines`, `--l`, `--lines`, `-unixify`, `--unixify`:
  36             // enable line-mode, and ignore leading arg, to avoid it
  37             // being mistaken for a named input
  38             lines = true
  39             args = args[1:]
  40         }
  41     }
  42 
  43     err := run(os.Stdout, os.Stdin, args, lines)
  44     if err != nil && err != errNoMoreOutput {
  45         os.Stderr.WriteString(err.Error())
  46         os.Stderr.WriteString("\n")
  47         os.Exit(1)
  48     }
  49 }
  50 
  51 // run handles buffering/flushing and some simple config for func main
  52 func run(w io.Writer, r io.Reader, names []string, lines bool) error {
  53     bw := bufio.NewWriter(os.Stdout)
  54     defer bw.Flush()
  55 
  56     if lines {
  57         return fetchAll(r, names, func(r io.Reader) error {
  58             return emitLines(bw, r)
  59         })
  60     }
  61 
  62     return fetchAll(r, names, func(r io.Reader) error {
  63         return emitBytes(bw, r)
  64     })
  65 }
  66 
  67 // fetchAll loads/emits all inputs, either named or implied, keeping track
  68 // of possible multiple uses of stdin, via the single-dash dummy name
  69 func fetchAll(r io.Reader, names []string, emit func(r io.Reader) error) error {
  70     // figure out how many single-dash names were given
  71     dashes := 0
  72     var input []byte
  73     gotInput := false
  74     for _, s := range names {
  75         if s == `-` {
  76             dashes++
  77         }
  78     }
  79 
  80     for _, s := range names {
  81         // load main input only once, the first time it's needed, and only
  82         // when multiple explicit dashes were given
  83         if dashes > 1 && s == `-` && !gotInput {
  84             b, err := io.ReadAll(r)
  85             if err != nil {
  86                 return err
  87             }
  88 
  89             input = b
  90             gotInput = true
  91         }
  92 
  93         // emit main input when it's cached
  94         if s == `-` && dashes > 1 {
  95             err := emit(bytes.NewReader(input))
  96             if err != nil {
  97                 return err
  98             }
  99         }
 100 
 101         // emit main input uncached, since it's only needed once
 102         if s == `-` {
 103             err := emit(r)
 104             if err != nil {
 105                 return err
 106             }
 107         }
 108 
 109         // emit regular named inputs
 110         err := handle(s, emit)
 111         if err != nil {
 112             return err
 113         }
 114     }
 115 
 116     // no names were given, so emit the main input by default
 117     if len(names) == 0 {
 118         return emit(r)
 119     }
 120     return nil
 121 }
 122 
 123 // handle emits bytes from a named source
 124 func handle(name string, emit func(r io.Reader) error) error {
 125     if hasAnyPrefix(name, `https://`, `http://`) {
 126         resp, err := http.Get(name)
 127         if err != nil {
 128             return err
 129         }
 130         defer resp.Body.Close()
 131         return emit(resp.Body)
 132     }
 133 
 134     f, err := os.Open(name)
 135     if err != nil {
 136         return err
 137     }
 138     defer f.Close()
 139     return emit(f)
 140 }
 141 
 142 // hasAnyPrefix generalizes func strings.HasPrefix to check multiple
 143 // possible prefixes
 144 func hasAnyPrefix(s string, prefixes ...string) bool {
 145     for _, pre := range prefixes {
 146         if strings.HasPrefix(s, pre) {
 147             return true
 148         }
 149     }
 150     return false
 151 }
 152 
 153 // emitBytes just copies bytes verbatim, as fast as it can
 154 func emitBytes(w io.Writer, r io.Reader) error {
 155     _, err := io.Copy(w, r)
 156 
 157     // assume write-errors are due to a closed output pipe,
 158     // so emit a dummy error to quit the app early
 159     if err != nil {
 160         return errNoMoreOutput
 161     }
 162     return nil
 163 }
 164 
 165 // emitLines-mode turns all CRLF byte-pairs into single LF bytes, and
 166 // guarantees a final LF byte, unless input has no bytes at all
 167 func emitLines(w io.Writer, r io.Reader) error {
 168     const gb = 1024 * 1024 * 1024
 169     sc := bufio.NewScanner(r)
 170     sc.Buffer(nil, 8*gb)
 171 
 172     for i := 0; sc.Scan(); i++ {
 173         if i == 0 {
 174             // ignore leading UTF-8 BOMs (byte-order markers) on first
 175             // lines of inputs: when using UTF-8, those BOMs are useless,
 176             // since the UTF-8 format has only one possible byte-order
 177             const utf8BOM = "\xef\xbb\xbf"
 178             w.Write(bytes.TrimPrefix(sc.Bytes(), []byte(utf8BOM)))
 179         } else {
 180             // emit all later lines without checking for BOMs
 181             w.Write(sc.Bytes())
 182         }
 183 
 184         // assume write-errors are due to a closed output pipe,
 185         // so emit a dummy error to quit the app early
 186         _, err := w.Write([]byte{'\n'})
 187         if err != nil {
 188             return errNoMoreOutput
 189         }
 190     }
 191 
 192     // report `actual` errors
 193     return sc.Err()
 194 }