File: json2.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 json2.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "encoding/json"
  37     "errors"
  38     "io"
  39     "os"
  40 )
  41 
  42 const info = `
  43 json2 [filepath...]
  44 
  45 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
  46 each indentation level.
  47 `
  48 
  49 func main() {
  50     args := os.Args[1:]
  51 
  52     if len(args) > 0 {
  53         switch args[0] {
  54         case `-h`, `--h`, `-help`, `--help`:
  55             os.Stdout.WriteString(info[1:])
  56             return
  57         }
  58     }
  59 
  60     if len(args) > 0 && args[0] == `--` {
  61         args = args[1:]
  62     }
  63 
  64     if len(args) > 1 {
  65         const msg = "multiple inputs aren't allowed\n"
  66         os.Stderr.WriteString(msg)
  67         os.Exit(1)
  68     }
  69 
  70     // figure out whether input should come from a named file or from stdin
  71     name := `-`
  72     if len(args) == 1 {
  73         name = args[0]
  74     }
  75 
  76     if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
  77         os.Stderr.WriteString(err.Error())
  78         os.Stderr.WriteString("\n")
  79         os.Exit(1)
  80     }
  81 }
  82 
  83 // handleInput simplifies control-flow for func main
  84 func handleInput(w io.Writer, path string) error {
  85     if path == `-` {
  86         return convert(w, os.Stdin)
  87     }
  88 
  89     f, err := os.Open(path)
  90     if err != nil {
  91         // on windows, file-not-found error messages may mention `CreateFile`,
  92         // even when trying to open files in read-only mode
  93         return errors.New(`can't open file named ` + path)
  94     }
  95     defer f.Close()
  96     return convert(w, f)
  97 }
  98 
  99 // convert simplifies control-flow for func handleInput
 100 func convert(w io.Writer, r io.Reader) error {
 101     bw := bufio.NewWriter(w)
 102     defer bw.Flush()
 103     return json2(bw, r)
 104 }
 105 
 106 // escapedStringBytes helps func handleString treat all string bytes quickly
 107 // and correctly, using their officially-supported JSON escape sequences
 108 //
 109 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 110 var escapedStringBytes = [256][]byte{
 111     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 112     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 113     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 114     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 115     {'\\', 'b'}, {'\\', 't'},
 116     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 117     {'\\', 'f'}, {'\\', 'r'},
 118     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 119     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 120     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 121     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 122     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 123     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 124     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 125     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 126     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 127     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 128     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 129     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 130     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 131     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 132     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 133     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 134     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 135     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 136     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 137     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 138     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 139     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 140     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 141     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 142     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 143     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 144     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 145     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 146     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 147     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 148     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 149     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 150     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 151     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 152     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 153     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 154     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 155 }
 156 
 157 // writeSpaces does what it says, minimizing calls to write-like funcs
 158 func writeSpaces(w *bufio.Writer, n int) {
 159     const spaces = `                                `
 160     if n < 1 {
 161         return
 162     }
 163 
 164     for n >= len(spaces) {
 165         w.WriteString(spaces)
 166         n -= len(spaces)
 167     }
 168     w.WriteString(spaces[:n])
 169 }
 170 
 171 // json2 does it all, given a reader and a writer
 172 func json2(w *bufio.Writer, r io.Reader) error {
 173     dec := json.NewDecoder(r)
 174     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 175     // even if JSON parsers aren't required to guarantee such input-fidelity
 176     // for numbers
 177     dec.UseNumber()
 178 
 179     t, err := dec.Token()
 180     if err == io.EOF {
 181         return errors.New(`input has no JSON values`)
 182     }
 183 
 184     if err = handleToken(w, dec, t, 0, 0); err != nil {
 185         return err
 186     }
 187     // don't forget ending the last line for the last value
 188     w.WriteByte('\n')
 189 
 190     _, err = dec.Token()
 191     if err == io.EOF {
 192         // input is over, so it's a success
 193         return nil
 194     }
 195 
 196     if err == nil {
 197         // a successful `read` is a failure, as it means there are
 198         // trailing JSON tokens
 199         return errors.New(`unexpected trailing data`)
 200     }
 201 
 202     // any other error, perhaps some invalid-JSON-syntax-type error
 203     return err
 204 }
 205 
 206 // handleToken handles recursion for func json2
 207 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 208     switch t := t.(type) {
 209     case json.Delim:
 210         switch t {
 211         case json.Delim('['):
 212             return handleArray(w, dec, pre, level)
 213         case json.Delim('{'):
 214             return handleObject(w, dec, pre, level)
 215         default:
 216             return errors.New(`unsupported JSON syntax ` + string(t))
 217         }
 218 
 219     case nil:
 220         writeSpaces(w, 2*pre)
 221         w.WriteString(`null`)
 222         return nil
 223 
 224     case bool:
 225         writeSpaces(w, 2*pre)
 226         if t {
 227             w.WriteString(`true`)
 228         } else {
 229             w.WriteString(`false`)
 230         }
 231         return nil
 232 
 233     case json.Number:
 234         writeSpaces(w, 2*pre)
 235         w.WriteString(t.String())
 236         return nil
 237 
 238     case string:
 239         return handleString(w, t, pre)
 240 
 241     default:
 242         // return fmt.Errorf(`unsupported token type %T`, t)
 243         return errors.New(`invalid JSON token`)
 244     }
 245 }
 246 
 247 // handleArray handles arrays for func handleToken
 248 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 249     for i := 0; true; i++ {
 250         t, err := dec.Token()
 251         if err != nil {
 252             return err
 253         }
 254 
 255         if t == json.Delim(']') {
 256             if i == 0 {
 257                 writeSpaces(w, 2*pre)
 258                 w.WriteByte('[')
 259                 w.WriteByte(']')
 260             } else {
 261                 w.WriteByte('\n')
 262                 writeSpaces(w, 2*level)
 263                 w.WriteByte(']')
 264             }
 265             return nil
 266         }
 267 
 268         if i == 0 {
 269             writeSpaces(w, 2*pre)
 270             w.WriteByte('[')
 271             w.WriteByte('\n')
 272         } else {
 273             w.WriteByte(',')
 274             w.WriteByte('\n')
 275             if err := w.Flush(); err != nil {
 276                 // a write error may be the consequence of stdout being closed,
 277                 // perhaps by another app along a pipe
 278                 return io.EOF
 279             }
 280         }
 281 
 282         err = handleToken(w, dec, t, level+1, level+1)
 283         if err != nil {
 284             return err
 285         }
 286     }
 287 
 288     // make the compiler happy
 289     return nil
 290 }
 291 
 292 // handleObject handles objects for func handleToken
 293 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 294     for i := 0; true; i++ {
 295         t, err := dec.Token()
 296         if err != nil {
 297             return err
 298         }
 299 
 300         if t == json.Delim('}') {
 301             if i == 0 {
 302                 writeSpaces(w, 2*pre)
 303                 w.WriteByte('{')
 304                 w.WriteByte('}')
 305             } else {
 306                 w.WriteByte('\n')
 307                 writeSpaces(w, 2*level)
 308                 w.WriteByte('}')
 309             }
 310             return nil
 311         }
 312 
 313         if i == 0 {
 314             writeSpaces(w, 2*pre)
 315             w.WriteByte('{')
 316             w.WriteByte('\n')
 317         } else {
 318             w.WriteByte(',')
 319             w.WriteByte('\n')
 320         }
 321 
 322         k, ok := t.(string)
 323         if !ok {
 324             return errors.New(`expected a string for a key-value pair`)
 325         }
 326 
 327         err = handleString(w, k, level+1)
 328         if err != nil {
 329             return err
 330         }
 331 
 332         w.WriteString(": ")
 333 
 334         t, err = dec.Token()
 335         if err == io.EOF {
 336             return errors.New(`expected a value for a key-value pair`)
 337         }
 338 
 339         err = handleToken(w, dec, t, 0, level+1)
 340         if err != nil {
 341             return err
 342         }
 343     }
 344 
 345     // make the compiler happy
 346     return nil
 347 }
 348 
 349 // handleString handles strings for func handleToken, and keys for func
 350 // handleObject
 351 func handleString(w *bufio.Writer, s string, level int) error {
 352     writeSpaces(w, 2*level)
 353     w.WriteByte('"')
 354     for i := range s {
 355         w.Write(escapedStringBytes[s[i]])
 356     }
 357     w.WriteByte('"')
 358     return nil
 359 }