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 }