File: catl.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 catl.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "bytes"
  37     "errors"
  38     "io"
  39     "os"
  40 )
  41 
  42 const info = `
  43 catl [options...] [file...]
  44 
  45 
  46 Unlike "cat", conCATenate Lines ensures lines across inputs are never joined
  47 by accident, when an input's last line doesn't end with a line-feed.
  48 
  49 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  50 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
  51 
  52 All (optional) leading options start with either single or double-dash:
  53 
  54     -h, -help    show this help message
  55 `
  56 
  57 func main() {
  58     buffered := false
  59     args := os.Args[1:]
  60 
  61     if len(args) > 0 {
  62         switch args[0] {
  63         case `-b`, `--b`, `-buffered`, `--buffered`:
  64             buffered = true
  65             args = args[1:]
  66 
  67         case `-h`, `--h`, `-help`, `--help`:
  68             os.Stdout.WriteString(info[1:])
  69             return
  70         }
  71     }
  72 
  73     if len(args) > 0 && args[0] == `--` {
  74         args = args[1:]
  75     }
  76 
  77     liveLines := !buffered
  78     if !buffered {
  79         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  80             liveLines = false
  81         }
  82     }
  83 
  84     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  85         os.Stderr.WriteString(err.Error())
  86         os.Stderr.WriteString("\n")
  87         os.Exit(1)
  88     }
  89 }
  90 
  91 func run(w io.Writer, args []string, live bool) error {
  92     bw := bufio.NewWriter(w)
  93     defer bw.Flush()
  94 
  95     dashes := 0
  96     for _, name := range args {
  97         if name == `-` {
  98             dashes++
  99         }
 100         if dashes > 1 {
 101             break
 102         }
 103     }
 104 
 105     if len(args) == 0 {
 106         return catl(bw, os.Stdin, live)
 107     }
 108 
 109     var stdin []byte
 110     gotStdin := false
 111 
 112     for _, name := range args {
 113         if name == `-` {
 114             if dashes == 1 {
 115                 if err := catl(bw, os.Stdin, live); err != nil {
 116                     return err
 117                 }
 118                 continue
 119             }
 120 
 121             if !gotStdin {
 122                 data, err := io.ReadAll(os.Stdin)
 123                 if err != nil {
 124                     return err
 125                 }
 126                 stdin = data
 127                 gotStdin = true
 128             }
 129 
 130             bw.Write(stdin)
 131             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 132                 bw.WriteByte('\n')
 133             }
 134 
 135             if !live {
 136                 continue
 137             }
 138 
 139             if err := bw.Flush(); err != nil {
 140                 return io.EOF
 141             }
 142 
 143             continue
 144         }
 145 
 146         if err := handleFile(bw, name, live); err != nil {
 147             return err
 148         }
 149     }
 150     return nil
 151 }
 152 
 153 func handleFile(w *bufio.Writer, name string, live bool) error {
 154     if name == `` || name == `-` {
 155         return catl(w, os.Stdin, live)
 156     }
 157 
 158     f, err := os.Open(name)
 159     if err != nil {
 160         return errors.New(`can't read from file named "` + name + `"`)
 161     }
 162     defer f.Close()
 163 
 164     return catl(w, f, live)
 165 }
 166 
 167 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 168     if !live {
 169         return catlFast(w, r)
 170     }
 171 
 172     const gb = 1024 * 1024 * 1024
 173     sc := bufio.NewScanner(r)
 174     sc.Buffer(nil, 8*gb)
 175 
 176     for i := 0; sc.Scan(); i++ {
 177         s := sc.Bytes()
 178         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 179             s = s[3:]
 180         }
 181 
 182         w.Write(s)
 183         if w.WriteByte('\n') != nil {
 184             return io.EOF
 185         }
 186 
 187         if err := w.Flush(); err != nil {
 188             return io.EOF
 189         }
 190     }
 191 
 192     return sc.Err()
 193 }
 194 
 195 func catlFast(w *bufio.Writer, r io.Reader) error {
 196     var buf [32 * 1024]byte
 197     var last byte = '\n'
 198 
 199     for i := 0; true; i++ {
 200         n, err := r.Read(buf[:])
 201         if n > 0 && err == io.EOF {
 202             err = nil
 203         }
 204         if err == io.EOF {
 205             if last != '\n' {
 206                 w.WriteByte('\n')
 207             }
 208             return nil
 209         }
 210 
 211         if err != nil {
 212             return err
 213         }
 214 
 215         chunk := buf[:n]
 216         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 217             chunk = chunk[3:]
 218         }
 219 
 220         if len(chunk) >= 1 {
 221             w.Write(chunk)
 222             last = chunk[len(chunk)-1]
 223         }
 224     }
 225 
 226     return nil
 227 }