File: tcatl.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 tcatl.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "bytes"
  37     "errors"
  38     "io"
  39     "os"
  40     "unicode/utf8"
  41 )
  42 
  43 const info = `
  44 tcatl [options...] [file...]
  45 
  46 
  47 Title and Concatenate lines emits lines from all the named sources given,
  48 preceding each file's contents with its name, using an ANSI reverse style.
  49 
  50 The name "-" stands for the standard input. When no names are given, the
  51 standard input is used by default.
  52 
  53 All (optional) leading options start with either single or double-dash:
  54 
  55     -h, -help    show this help message
  56 `
  57 
  58 func main() {
  59     args := os.Args[1:]
  60     if len(args) > 0 {
  61         switch args[0] {
  62         case `-h`, `--h`, `-help`, `--help`:
  63             os.Stdout.WriteString(info[1:])
  64             return
  65         }
  66     }
  67 
  68     if len(args) > 0 && args[0] == `--` {
  69         args = args[1:]
  70     }
  71 
  72     if err := run(os.Stdout, args); err != nil {
  73         os.Stderr.WriteString(err.Error())
  74         os.Stderr.WriteString("\n")
  75         os.Exit(1)
  76     }
  77 }
  78 
  79 func run(w io.Writer, args []string) error {
  80     bw := bufio.NewWriter(w)
  81     defer bw.Flush()
  82 
  83     if len(args) == 0 {
  84         return tcatl(bw, os.Stdin, `-`)
  85     }
  86 
  87     for _, name := range args {
  88         if err := handleFile(bw, name); err != nil {
  89             return err
  90         }
  91     }
  92     return nil
  93 }
  94 
  95 func handleFile(w *bufio.Writer, name string) error {
  96     if name == `` || name == `-` {
  97         return tcatl(w, os.Stdin, `-`)
  98     }
  99 
 100     f, err := os.Open(name)
 101     if err != nil {
 102         return errors.New(`can't read from file named "` + name + `"`)
 103     }
 104     defer f.Close()
 105 
 106     return tcatl(w, f, name)
 107 }
 108 
 109 func tcatl(w *bufio.Writer, r io.Reader, name string) error {
 110     w.WriteString("\x1b[7m")
 111     w.WriteString(name)
 112     writeSpaces(w, 80-utf8.RuneCountInString(name))
 113     w.WriteString("\x1b[0m\n")
 114     if err := w.Flush(); err != nil {
 115         // a write error may be the consequence of stdout being closed,
 116         // perhaps by another app along a pipe
 117         return io.EOF
 118     }
 119 
 120     if catlFast(w, r) != nil {
 121         return io.EOF
 122     }
 123     return nil
 124 }
 125 
 126 func catlFast(w *bufio.Writer, r io.Reader) error {
 127     var buf [32 * 1024]byte
 128     var last byte = '\n'
 129 
 130     for i := 0; true; i++ {
 131         n, err := r.Read(buf[:])
 132         if n > 0 && err == io.EOF {
 133             err = nil
 134         }
 135         if err == io.EOF {
 136             if last != '\n' {
 137                 w.WriteByte('\n')
 138             }
 139             return nil
 140         }
 141 
 142         if err != nil {
 143             return err
 144         }
 145 
 146         chunk := buf[:n]
 147         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 148             chunk = chunk[3:]
 149         }
 150 
 151         if len(chunk) >= 1 {
 152             if _, err := w.Write(chunk); err != nil {
 153                 return io.EOF
 154             }
 155             last = chunk[len(chunk)-1]
 156         }
 157     }
 158 
 159     return nil
 160 }
 161 
 162 // writeSpaces bulk-emits the number of spaces given
 163 func writeSpaces(w *bufio.Writer, n int) {
 164     const spaces = `                                `
 165     for ; n > len(spaces); n -= len(spaces) {
 166         w.WriteString(spaces)
 167     }
 168     if n > 0 {
 169         w.WriteString(spaces[:n])
 170     }
 171 }