File: jsons.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 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 jsons.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "io"
  37     "os"
  38     "strings"
  39 )
  40 
  41 const info = `
  42 jsons [options...] [filenames...]
  43 
  44 JSON Strings turns TSV (tab-separated values) data into a JSON array of
  45 objects whose values are strings or nulls, the latter being used for
  46 missing trailing values.
  47 `
  48 
  49 // noMoreOutput is a custom error-type meant to be deliberately ignored
  50 type noMoreOutput struct{}
  51 
  52 func (nmo noMoreOutput) Error() string {
  53     return `no more output`
  54 }
  55 
  56 func main() {
  57     if len(os.Args) > 1 {
  58         switch os.Args[1] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63     }
  64 
  65     err := run(os.Args[1:])
  66     if _, ok := err.(noMoreOutput); ok {
  67         err = nil
  68     }
  69 
  70     if err != nil {
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 type runConfig struct {
  78     lines int
  79     keys  []string
  80 }
  81 
  82 func run(paths []string) error {
  83     bw := bufio.NewWriter(os.Stdout)
  84     defer bw.Flush()
  85 
  86     dashes := 0
  87     var cfg runConfig
  88 
  89     for _, path := range paths {
  90         if path == `-` {
  91             dashes++
  92             if dashes > 1 {
  93                 continue
  94             }
  95 
  96             if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  97                 return err
  98             }
  99 
 100             continue
 101         }
 102 
 103         if err := handleFile(bw, path, &cfg); err != nil {
 104             return err
 105         }
 106     }
 107 
 108     if len(paths) == 0 {
 109         if err := handleInput(bw, os.Stdin, &cfg); err != nil {
 110             return err
 111         }
 112     }
 113 
 114     if cfg.lines > 1 {
 115         bw.WriteString("\n]\n")
 116     } else {
 117         bw.WriteString("[]\n")
 118     }
 119     return nil
 120 }
 121 
 122 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
 123     f, err := os.Open(path)
 124     if err != nil {
 125         return err
 126     }
 127     defer f.Close()
 128     return handleInput(w, f, cfg)
 129 }
 130 
 131 func escapeKeys(line string) []string {
 132     var keys []string
 133     var sb strings.Builder
 134 
 135     loopTSV(line, func(i int, s string) {
 136         sb.WriteByte('"')
 137         for _, r := range s {
 138             if r == '\\' || r == '"' {
 139                 sb.WriteByte('\\')
 140             }
 141             sb.WriteRune(r)
 142         }
 143         sb.WriteByte('"')
 144 
 145         keys = append(keys, sb.String())
 146         sb.Reset()
 147     })
 148 
 149     return keys
 150 }
 151 
 152 func emitRow(w *bufio.Writer, line string, keys []string) {
 153     j := 0
 154     w.WriteByte('{')
 155 
 156     loopTSV(line, func(i int, s string) {
 157         j = i
 158         if i > 0 {
 159             w.WriteString(", ")
 160         }
 161 
 162         w.WriteString(keys[i])
 163         w.WriteString(": \"")
 164 
 165         for _, r := range s {
 166             if r == '\\' || r == '"' {
 167                 w.WriteByte('\\')
 168             }
 169             w.WriteRune(r)
 170         }
 171         w.WriteByte('"')
 172     })
 173 
 174     for i := j + 1; i < len(keys); i++ {
 175         if i > 0 {
 176             w.WriteString(", ")
 177         }
 178         w.WriteString(keys[i])
 179         w.WriteString(": null")
 180     }
 181     w.WriteByte('}')
 182 }
 183 
 184 func loopTSV(line string, f func(i int, s string)) {
 185     for i := 0; len(line) > 0; i++ {
 186         pos := strings.IndexByte(line, '\t')
 187         if pos < 0 {
 188             f(i, line)
 189             return
 190         }
 191 
 192         f(i, line[:pos])
 193         line = line[pos+1:]
 194     }
 195 }
 196 
 197 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
 198     const gb = 1024 * 1024 * 1024
 199     sc := bufio.NewScanner(r)
 200     sc.Buffer(nil, 8*gb)
 201 
 202     for sc.Scan() {
 203         if cfg.lines == 0 {
 204             cfg.keys = escapeKeys(sc.Text())
 205             w.WriteByte('[')
 206             cfg.lines++
 207             continue
 208         }
 209 
 210         if cfg.lines == 1 {
 211             w.WriteString("\n  ")
 212         } else {
 213             if _, err := w.WriteString(",\n  "); err != nil {
 214                 return noMoreOutput{}
 215             }
 216         }
 217 
 218         emitRow(w, sc.Text(), cfg.keys)
 219         cfg.lines++
 220     }
 221 
 222     return sc.Err()
 223 }