File: dessv.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 dessv.go 30 */ 31 32 package main 33 34 import ( 35 "bufio" 36 "bytes" 37 "errors" 38 "io" 39 "os" 40 ) 41 42 // Note: the code is avoiding using the fmt package to save hundreds of 43 // kilobytes on the resulting executable, which is a noticeable difference. 44 45 const info = ` 46 dessv [filenames...] 47 48 Turn Space(s)-Separated Values (SSV) into Tab-Separated Values (TSV), where 49 both leading and trailing spaces from input lines are ignored. 50 ` 51 52 // errNoMoreOutput is a dummy error whose message is ignored, and which 53 // causes the app to quit immediately and successfully 54 var errNoMoreOutput = errors.New(`no more output`) 55 56 func main() { 57 buffered := false 58 args := os.Args[1:] 59 60 if len(args) > 0 { 61 switch args[0] { 62 case `-b`, `--b`, `-buffered`, `--buffered`: 63 buffered = true 64 args = args[1:] 65 66 case `-h`, `--h`, `-help`, `--help`: 67 os.Stdout.WriteString(info[1:]) 68 return 69 } 70 } 71 72 if len(args) > 0 && args[0] == `--` { 73 args = args[1:] 74 } 75 76 liveLines := !buffered 77 if !buffered { 78 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil { 79 liveLines = false 80 } 81 } 82 83 if err := run(os.Stdout, args, liveLines); isActualError(err) { 84 os.Stderr.WriteString(err.Error()) 85 os.Stderr.WriteString("\n") 86 os.Exit(1) 87 } 88 } 89 90 func run(w io.Writer, args []string, live bool) error { 91 bw := bufio.NewWriter(w) 92 defer bw.Flush() 93 94 if len(args) == 0 { 95 return dessv(bw, os.Stdin, live) 96 } 97 98 for _, name := range args { 99 if err := handleFile(bw, name, live); err != nil { 100 return err 101 } 102 } 103 return nil 104 } 105 106 func handleFile(w *bufio.Writer, name string, live bool) error { 107 if name == `` || name == `-` { 108 return dessv(w, os.Stdin, live) 109 } 110 111 f, err := os.Open(name) 112 if err != nil { 113 return errors.New(`can't read from file named "` + name + `"`) 114 } 115 defer f.Close() 116 117 return dessv(w, f, live) 118 } 119 120 // isActualError is to figure out whether not to ignore an error, and thus 121 // show it as an error message 122 func isActualError(err error) bool { 123 return err != nil && err != errNoMoreOutput 124 } 125 126 func dessv(w *bufio.Writer, r io.Reader, live bool) error { 127 const gb = 1024 * 1024 * 1024 128 sc := bufio.NewScanner(r) 129 sc.Buffer(nil, 8*gb) 130 handleRow := handleRowSSV 131 numTabs := ^0 132 133 for i := 0; sc.Scan(); i++ { 134 s := sc.Bytes() 135 if i == 0 { 136 if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) { 137 s = s[3:] 138 } 139 140 for _, b := range s { 141 if b == '\t' { 142 handleRow = handleRowTSV 143 break 144 } 145 } 146 numTabs = handleRow(w, s, numTabs) 147 } else { 148 handleRow(w, s, numTabs) 149 } 150 151 if w.WriteByte('\n') != nil { 152 return errNoMoreOutput 153 } 154 155 if !live { 156 continue 157 } 158 159 if err := w.Flush(); err != nil { 160 return errNoMoreOutput 161 } 162 } 163 164 return sc.Err() 165 } 166 167 func handleRowSSV(w *bufio.Writer, s []byte, n int) int { 168 for len(s) > 0 && s[0] == ' ' { 169 s = s[1:] 170 } 171 for len(s) > 0 && s[len(s)-1] == ' ' { 172 s = s[:len(s)-1] 173 } 174 175 got := 0 176 177 for got = 0; len(s) > 0; got++ { 178 if got > 0 { 179 w.WriteByte('\t') 180 } 181 182 i := bytes.IndexByte(s, ' ') 183 if i < 0 { 184 w.Write(s) 185 s = nil 186 n-- 187 break 188 } 189 190 w.Write(s[:i]) 191 s = s[i+1:] 192 for len(s) > 0 && s[0] == ' ' { 193 s = s[1:] 194 } 195 n-- 196 } 197 198 w.Write(s) 199 writeTabs(w, n) 200 return got 201 } 202 203 func handleRowTSV(w *bufio.Writer, s []byte, n int) int { 204 got := 0 205 for _, b := range s { 206 if b == '\t' { 207 got++ 208 } 209 } 210 211 w.Write(s) 212 writeTabs(w, n-got) 213 return got 214 } 215 216 func writeTabs(w *bufio.Writer, n int) { 217 for n > 0 { 218 w.WriteByte('\t') 219 n-- 220 } 221 }