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